From 790ea8e6f2fbed4a2f0fa614799fe27c6f7b4a78 Mon Sep 17 00:00:00 2001 From: George Ogata Date: Wed, 15 Jul 2015 01:29:22 -0400 Subject: [PATCH 001/502] Support descriptions for enum values in config. The enum property may now specify options as a list of {value:, description:} objects. --- spec/config-spec.coffee | 18 ++++++++++++++++++ src/config.coffee | 29 ++++++++++++++++++++++++++--- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index 5aff0eff2..907faf447 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -1572,6 +1572,14 @@ describe "Config", -> items: type: 'string' enum: ['one', 'two', 'three'] + str_options: + type: 'string' + default: 'one' + enum: [ + value: 'one', description: 'One' + 'two', + value: 'three', description: 'Three' + ] atom.config.setSchema('foo.bar', schema) @@ -1595,3 +1603,13 @@ describe "Config", -> expect(atom.config.set('foo.bar.arr', ['two', 'three'])).toBe true expect(atom.config.get('foo.bar.arr')).toEqual ['two', 'three'] + + it 'will honor the enum when specified as an array', -> + expect(atom.config.set('foo.bar.str_options', 'one')).toBe true + expect(atom.config.get('foo.bar.str_options')).toEqual 'one' + + expect(atom.config.set('foo.bar.str_options', 'two')).toBe true + expect(atom.config.get('foo.bar.str_options')).toEqual 'two' + + expect(atom.config.set('foo.bar.str_options', 'One')).toBe false + expect(atom.config.get('foo.bar.str_options')).toEqual 'two' diff --git a/src/config.coffee b/src/config.coffee index 176390869..0eae7f335 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -235,9 +235,13 @@ ScopeDescriptor = require './scope-descriptor' # # #### enum # -# All types support an `enum` key. The enum key lets you specify all values -# that the config setting can possibly be. `enum` _must_ be an array of values -# of your specified type. Schema: +# All types support an `enum` key, which lets you specify all the values the +# setting can take. `enum` may be an array of allowed values (of the specified +# type), or an array of objects with `value` and `description` properties, where +# the `value` is an allowed value, and the `description` is a descriptive string +# used in the settings view. +# +# In this example, the setting must be one of the 4 integers: # # ```coffee # config: @@ -247,6 +251,20 @@ ScopeDescriptor = require './scope-descriptor' # enum: [2, 4, 6, 8] # ``` # +# In this example, the setting must be either 'foo' or 'bar', which are +# presented using the provided descriptions in the settings pane: +# +# ```coffee +# config: +# someSetting: +# type: 'string' +# default: 'foo' +# enum: [ +# {value: 'foo', description: 'Foo mode. You want this.'} +# {value: 'bar', description: 'Bar mode. Nobody wants that!'} +# ] +# ``` +# # Usage: # # ```coffee @@ -1132,6 +1150,11 @@ Config.addSchemaEnforcers validateEnum: (keyPath, value, schema) -> possibleValues = schema.enum + + if Array.isArray(possibleValues) + possibleValues = possibleValues.map (value) -> + if value.hasOwnProperty('value') then value.value else value + return value unless possibleValues? and Array.isArray(possibleValues) and possibleValues.length for possibleValue in possibleValues From 51a035e31d7ae81ec46946e49be2ca7ee13f4933 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Thu, 15 Oct 2015 15:54:11 +0200 Subject: [PATCH 002/502] Add nodegit to package.json --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index b7d079123..093ce9e6f 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "key-path-helpers": "^0.3.0", "less-cache": "0.22", "marked": "^0.3.4", + "nodegit": "~0.5.0", "normalize-package-data": "^2.0.0", "nslog": "^3", "oniguruma": "^5", From 8e744502fa0fcf427612f26eeb0bc954732db960 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Thu, 15 Oct 2015 15:54:42 +0200 Subject: [PATCH 003/502] start spiking out async repo --- spec/git-repository-async-spec.coffee | 303 ++++++++++++++++++++++++++ src/git-repository-async.js | 23 ++ 2 files changed, 326 insertions(+) create mode 100644 spec/git-repository-async-spec.coffee create mode 100644 src/git-repository-async.js diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee new file mode 100644 index 000000000..4a881ee23 --- /dev/null +++ b/spec/git-repository-async-spec.coffee @@ -0,0 +1,303 @@ +temp = require 'temp' +GitRepositoryAsync = require '../src/git-repository-async' +fs = require 'fs-plus' +path = require 'path' +Task = require '../src/task' +Project = require '../src/project' + +copyRepository = -> + workingDirPath = temp.mkdirSync('atom-working-dir') + fs.copySync(path.join(__dirname, 'fixtures', 'git', 'working-dir'), workingDirPath) + fs.renameSync(path.join(workingDirPath, 'git.git'), path.join(workingDirPath, '.git')) + workingDirPath + +fdescribe "GitRepositoryAsync", -> + repo = null + + # beforeEach -> + # gitPath = path.join(temp.dir, '.git') + # fs.removeSync(gitPath) if fs.isDirectorySync(gitPath) + # + # afterEach -> + # repo.destroy() if repo?.repo? + + describe "@open(path)", -> + + # This just exercises the framework, but I'm trying to match the sync specs to start + it "repo is null when no repository is found", -> + repo = GitRepositoryAsync.open(path.join(temp.dir, 'nogit.txt')) + + waitsFor -> + repo._opening is false + + runs -> + expect(repo.repo).toBe null + + describe ".getPath()", -> + it "returns the repository path for a .git directory path", -> + repo = GitRepositoryAsync.open(path.join(__dirname, 'fixtures', 'git', 'master.git', 'HEAD')) + + waitsForPromise -> + repo.getPath + + runs -> + expect(repo.getPath()).toBe path.join(__dirname, 'fixtures', 'git', 'master.git') + + it "returns the repository path for a repository path", -> + repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'master.git')) + expect(repo.getPath()).toBe path.join(__dirname, 'fixtures', 'git', 'master.git') + + xdescribe ".isPathIgnored(path)", -> + it "returns true for an ignored path", -> + repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'ignore.git')) + expect(repo.isPathIgnored('a.txt')).toBeTruthy() + + it "returns false for a non-ignored path", -> + repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'ignore.git')) + expect(repo.isPathIgnored('b.txt')).toBeFalsy() + + xdescribe ".isPathModified(path)", -> + [repo, filePath, newPath] = [] + + beforeEach -> + workingDirPath = copyRepository() + repo = new GitRepository(workingDirPath) + filePath = path.join(workingDirPath, 'a.txt') + newPath = path.join(workingDirPath, 'new-path.txt') + + xdescribe "when the path is unstaged", -> + it "returns false if the path has not been modified", -> + expect(repo.isPathModified(filePath)).toBeFalsy() + + it "returns true if the path is modified", -> + fs.writeFileSync(filePath, "change") + expect(repo.isPathModified(filePath)).toBeTruthy() + + it "returns true if the path is deleted", -> + fs.removeSync(filePath) + expect(repo.isPathModified(filePath)).toBeTruthy() + + it "returns false if the path is new", -> + expect(repo.isPathModified(newPath)).toBeFalsy() + + xdescribe ".isPathNew(path)", -> + [filePath, newPath] = [] + + beforeEach -> + workingDirPath = copyRepository() + repo = new GitRepository(workingDirPath) + filePath = path.join(workingDirPath, 'a.txt') + newPath = path.join(workingDirPath, 'new-path.txt') + fs.writeFileSync(newPath, "i'm new here") + + xdescribe "when the path is unstaged", -> + it "returns true if the path is new", -> + expect(repo.isPathNew(newPath)).toBeTruthy() + + it "returns false if the path isn't new", -> + expect(repo.isPathNew(filePath)).toBeFalsy() + + xdescribe ".checkoutHead(path)", -> + [filePath] = [] + + beforeEach -> + workingDirPath = copyRepository() + repo = new GitRepository(workingDirPath) + filePath = path.join(workingDirPath, 'a.txt') + + it "no longer reports a path as modified after checkout", -> + expect(repo.isPathModified(filePath)).toBeFalsy() + fs.writeFileSync(filePath, 'ch ch changes') + expect(repo.isPathModified(filePath)).toBeTruthy() + expect(repo.checkoutHead(filePath)).toBeTruthy() + expect(repo.isPathModified(filePath)).toBeFalsy() + + it "restores the contents of the path to the original text", -> + fs.writeFileSync(filePath, 'ch ch changes') + expect(repo.checkoutHead(filePath)).toBeTruthy() + expect(fs.readFileSync(filePath, 'utf8')).toBe '' + + it "fires a status-changed event if the checkout completes successfully", -> + fs.writeFileSync(filePath, 'ch ch changes') + repo.getPathStatus(filePath) + statusHandler = jasmine.createSpy('statusHandler') + repo.onDidChangeStatus statusHandler + repo.checkoutHead(filePath) + expect(statusHandler.callCount).toBe 1 + expect(statusHandler.argsForCall[0][0]).toEqual {path: filePath, pathStatus: 0} + + repo.checkoutHead(filePath) + expect(statusHandler.callCount).toBe 1 + + xdescribe ".checkoutHeadForEditor(editor)", -> + [filePath, editor] = [] + + beforeEach -> + workingDirPath = copyRepository() + repo = new GitRepository(workingDirPath) + filePath = path.join(workingDirPath, 'a.txt') + fs.writeFileSync(filePath, 'ch ch changes') + + waitsForPromise -> + atom.workspace.open(filePath) + + runs -> + editor = atom.workspace.getActiveTextEditor() + + it "displays a confirmation dialog by default", -> + spyOn(atom, 'confirm').andCallFake ({buttons}) -> buttons.OK() + atom.config.set('editor.confirmCheckoutHeadRevision', true) + + repo.checkoutHeadForEditor(editor) + + expect(fs.readFileSync(filePath, 'utf8')).toBe '' + + it "does not display a dialog when confirmation is disabled", -> + spyOn(atom, 'confirm') + atom.config.set('editor.confirmCheckoutHeadRevision', false) + + repo.checkoutHeadForEditor(editor) + + expect(fs.readFileSync(filePath, 'utf8')).toBe '' + expect(atom.confirm).not.toHaveBeenCalled() + + xdescribe ".destroy()", -> + it "throws an exception when any method is called after it is called", -> + repo = new GitRepository(require.resolve('./fixtures/git/master.git/HEAD')) + repo.destroy() + expect(-> repo.getShortHead()).toThrow() + + xdescribe ".getPathStatus(path)", -> + [filePath] = [] + + beforeEach -> + workingDirectory = copyRepository() + repo = new GitRepository(workingDirectory) + filePath = path.join(workingDirectory, 'file.txt') + + it "trigger a status-changed event when the new status differs from the last cached one", -> + statusHandler = jasmine.createSpy("statusHandler") + repo.onDidChangeStatus statusHandler + fs.writeFileSync(filePath, '') + status = repo.getPathStatus(filePath) + expect(statusHandler.callCount).toBe 1 + expect(statusHandler.argsForCall[0][0]).toEqual {path: filePath, pathStatus: status} + + fs.writeFileSync(filePath, 'abc') + status = repo.getPathStatus(filePath) + expect(statusHandler.callCount).toBe 1 + + xdescribe ".getDirectoryStatus(path)", -> + [directoryPath, filePath] = [] + + beforeEach -> + workingDirectory = copyRepository() + repo = new GitRepository(workingDirectory) + directoryPath = path.join(workingDirectory, 'dir') + filePath = path.join(directoryPath, 'b.txt') + + it "gets the status based on the files inside the directory", -> + expect(repo.isStatusModified(repo.getDirectoryStatus(directoryPath))).toBe false + fs.writeFileSync(filePath, 'abc') + repo.getPathStatus(filePath) + expect(repo.isStatusModified(repo.getDirectoryStatus(directoryPath))).toBe true + + xdescribe ".refreshStatus()", -> + [newPath, modifiedPath, cleanPath, originalModifiedPathText] = [] + + beforeEach -> + workingDirectory = copyRepository() + repo = new GitRepository(workingDirectory) + modifiedPath = path.join(workingDirectory, 'file.txt') + newPath = path.join(workingDirectory, 'untracked.txt') + cleanPath = path.join(workingDirectory, 'other.txt') + fs.writeFileSync(cleanPath, 'Full of text') + fs.writeFileSync(newPath, '') + newPath = fs.absolute newPath # specs could be running under symbol path. + + it "returns status information for all new and modified files", -> + fs.writeFileSync(modifiedPath, 'making this path modified') + statusHandler = jasmine.createSpy('statusHandler') + repo.onDidChangeStatuses statusHandler + repo.refreshStatus() + + waitsFor -> + statusHandler.callCount > 0 + + runs -> + expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined() + expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy() + expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy() + + xdescribe "buffer events", -> + [editor] = [] + + beforeEach -> + atom.project.setPaths([copyRepository()]) + + waitsForPromise -> + atom.workspace.open('other.txt').then (o) -> editor = o + + it "emits a status-changed event when a buffer is saved", -> + editor.insertNewline() + + statusHandler = jasmine.createSpy('statusHandler') + atom.project.getRepositories()[0].onDidChangeStatus statusHandler + editor.save() + expect(statusHandler.callCount).toBe 1 + expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} + + it "emits a status-changed event when a buffer is reloaded", -> + fs.writeFileSync(editor.getPath(), 'changed') + + statusHandler = jasmine.createSpy('statusHandler') + atom.project.getRepositories()[0].onDidChangeStatus statusHandler + editor.getBuffer().reload() + expect(statusHandler.callCount).toBe 1 + expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} + editor.getBuffer().reload() + expect(statusHandler.callCount).toBe 1 + + it "emits a status-changed event when a buffer's path changes", -> + fs.writeFileSync(editor.getPath(), 'changed') + + statusHandler = jasmine.createSpy('statusHandler') + atom.project.getRepositories()[0].onDidChangeStatus statusHandler + editor.getBuffer().emitter.emit 'did-change-path' + expect(statusHandler.callCount).toBe 1 + expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} + editor.getBuffer().emitter.emit 'did-change-path' + expect(statusHandler.callCount).toBe 1 + + it "stops listening to the buffer when the repository is destroyed (regression)", -> + atom.project.getRepositories()[0].destroy() + expect(-> editor.save()).not.toThrow() + + xdescribe "when a project is deserialized", -> + [buffer, project2] = [] + + afterEach -> + project2?.destroy() + + it "subscribes to all the serialized buffers in the project", -> + atom.project.setPaths([copyRepository()]) + + waitsForPromise -> + atom.workspace.open('file.txt') + + runs -> + project2 = Project.deserialize(atom.project.serialize()) + buffer = project2.getBuffers()[0] + + waitsFor -> + buffer.loaded + + runs -> + originalContent = buffer.getText() + buffer.append('changes') + + statusHandler = jasmine.createSpy('statusHandler') + project2.getRepositories()[0].onDidChangeStatus statusHandler + buffer.save() + expect(statusHandler.callCount).toBe 1 + expect(statusHandler).toHaveBeenCalledWith {path: buffer.getPath(), pathStatus: 256} diff --git a/src/git-repository-async.js b/src/git-repository-async.js new file mode 100644 index 000000000..66d51743f --- /dev/null +++ b/src/git-repository-async.js @@ -0,0 +1,23 @@ +"use babel"; + +const Git = require('nodegit') + +module.exports = class GitRepositoryAsync { + static open(path) { + // QUESTION: Should this wrap Git.Repository and reject with a nicer message? + return new GitRepositoryAsync(Git.Repository.open(path)) + } + + constructor (openPromise) { + this.repo = null + // this could be replaced with a function + this._opening = true + + openPromise.then( (repo) => { + this.repo = repo + this._opening = false + }).catch( (e) => { + this._opening = false + }) + } +} From b566e47a08bab8ddbb3632b28ebd34239d12d3f9 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Thu, 15 Oct 2015 16:51:39 +0200 Subject: [PATCH 004/502] getPath --- spec/git-repository-async-spec.coffee | 27 ++++++++++++++++++++------- src/git-repository-async.js | 9 +++++++++ 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index 4a881ee23..de752084d 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -34,18 +34,31 @@ fdescribe "GitRepositoryAsync", -> expect(repo.repo).toBe null describe ".getPath()", -> - it "returns the repository path for a .git directory path", -> + # XXX HEAD isn't a git directory.. what's this spec supposed to be about? + xit "returns the repository path for a .git directory path", -> + # Rejects as malformed repo = GitRepositoryAsync.open(path.join(__dirname, 'fixtures', 'git', 'master.git', 'HEAD')) - waitsForPromise -> - repo.getPath + onSuccess = jasmine.createSpy('onSuccess') - runs -> - expect(repo.getPath()).toBe path.join(__dirname, 'fixtures', 'git', 'master.git') + waitsForPromise -> + repo.getPath().then(onSuccess) + + runs -> + expectedPath = path.join(__dirname, 'fixtures', 'git', 'master.git') + expect(onSuccess.mostRecentCall.args[0]).toBe(expectedPath) it "returns the repository path for a repository path", -> - repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'master.git')) - expect(repo.getPath()).toBe path.join(__dirname, 'fixtures', 'git', 'master.git') + repo = GitRepositoryAsync.open(path.join(__dirname, 'fixtures', 'git', 'master.git')) + + onSuccess = jasmine.createSpy('onSuccess') + + waitsForPromise -> + repo.getPath().then(onSuccess) + + runs -> + expectedPath = path.join(__dirname, 'fixtures', 'git', 'master.git') + expect(onSuccess.mostRecentCall.args[0]).toBe(expectedPath) xdescribe ".isPathIgnored(path)", -> it "returns true for an ignored path", -> diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 66d51743f..d36664e09 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -13,11 +13,20 @@ module.exports = class GitRepositoryAsync { // this could be replaced with a function this._opening = true + // Do I use this outside of tests? openPromise.then( (repo) => { this.repo = repo this._opening = false }).catch( (e) => { this._opening = false }) + + this.repoPromise = openPromise + } + + getPath () { + return this.repoPromise.then( (repo) => { + return Promise.resolve(repo.path().replace(/\/$/, '')) + }) } } From bc3e8f02adde26f57844d7b35a34cf34e6fc37dc Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Thu, 15 Oct 2015 17:00:47 +0200 Subject: [PATCH 005/502] isPathIgnored --- spec/git-repository-async-spec.coffee | 29 +++++++++++++++++++-------- src/git-repository-async.js | 6 ++++++ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index de752084d..b80b2940f 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -11,6 +11,9 @@ copyRepository = -> fs.renameSync(path.join(workingDirPath, 'git.git'), path.join(workingDirPath, '.git')) workingDirPath +openFixture = (fixture)-> + GitRepositoryAsync.open(path.join(__dirname, 'fixtures', 'git', fixture)) + fdescribe "GitRepositoryAsync", -> repo = null @@ -49,7 +52,7 @@ fdescribe "GitRepositoryAsync", -> expect(onSuccess.mostRecentCall.args[0]).toBe(expectedPath) it "returns the repository path for a repository path", -> - repo = GitRepositoryAsync.open(path.join(__dirname, 'fixtures', 'git', 'master.git')) + repo = openFixture('master.git') onSuccess = jasmine.createSpy('onSuccess') @@ -60,14 +63,24 @@ fdescribe "GitRepositoryAsync", -> expectedPath = path.join(__dirname, 'fixtures', 'git', 'master.git') expect(onSuccess.mostRecentCall.args[0]).toBe(expectedPath) - xdescribe ".isPathIgnored(path)", -> - it "returns true for an ignored path", -> - repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'ignore.git')) - expect(repo.isPathIgnored('a.txt')).toBeTruthy() + describe ".isPathIgnored(path)", -> + it "resolves true for an ignored path", -> + repo = openFixture('ignore.git') + onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise -> + repo.isPathIgnored('a.txt').then(onSuccess).catch (e) -> console.log e + + runs -> + expect(onSuccess.mostRecentCall.args[0]).toBeTruthy() + + it "resolves false for a non-ignored path", -> + repo = openFixture('ignore.git') + onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise -> + repo.isPathIgnored('b.txt').then(onSuccess) + runs -> + expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() - it "returns false for a non-ignored path", -> - repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'ignore.git')) - expect(repo.isPathIgnored('b.txt')).toBeFalsy() xdescribe ".isPathModified(path)", -> [repo, filePath, newPath] = [] diff --git a/src/git-repository-async.js b/src/git-repository-async.js index d36664e09..e6d5a8f5d 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -29,4 +29,10 @@ module.exports = class GitRepositoryAsync { return Promise.resolve(repo.path().replace(/\/$/, '')) }) } + + isPathIgnored(_path) { + return this.repoPromise.then( (repo) => { + return Promise.resolve(Git.Ignore.pathIsIgnored(repo, _path)) + }) + } } From 62310e6f41b65bc3b91be05f3bab4f350527d63f Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Thu, 15 Oct 2015 18:10:32 +0200 Subject: [PATCH 006/502] isPathModified --- spec/git-repository-async-spec.coffee | 56 ++++++++++++++++++++------- src/git-repository-async.js | 16 ++++++++ 2 files changed, 59 insertions(+), 13 deletions(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index b80b2940f..180988d83 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -1,15 +1,19 @@ temp = require 'temp' GitRepositoryAsync = require '../src/git-repository-async' fs = require 'fs-plus' +os = require 'os' path = require 'path' Task = require '../src/task' Project = require '../src/project' +# Clean up when the process exits +temp.track() + copyRepository = -> workingDirPath = temp.mkdirSync('atom-working-dir') fs.copySync(path.join(__dirname, 'fixtures', 'git', 'working-dir'), workingDirPath) fs.renameSync(path.join(workingDirPath, 'git.git'), path.join(workingDirPath, '.git')) - workingDirPath + fs.realpathSync(workingDirPath) openFixture = (fixture)-> GitRepositoryAsync.open(path.join(__dirname, 'fixtures', 'git', fixture)) @@ -82,29 +86,55 @@ fdescribe "GitRepositoryAsync", -> expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() - xdescribe ".isPathModified(path)", -> - [repo, filePath, newPath] = [] + describe ".isPathModified(path)", -> + [repo, filePath, newPath, emptyPath] = [] beforeEach -> workingDirPath = copyRepository() - repo = new GitRepository(workingDirPath) + repo = new GitRepositoryAsync.open(workingDirPath) filePath = path.join(workingDirPath, 'a.txt') newPath = path.join(workingDirPath, 'new-path.txt') + fs.writeFileSync(newPath, "i'm new here") + emptyPath = path.join(workingDirPath, 'empty-path.txt') - xdescribe "when the path is unstaged", -> - it "returns false if the path has not been modified", -> - expect(repo.isPathModified(filePath)).toBeFalsy() + describe "when the path is unstaged", -> + it "resolves false if the path has not been modified", -> + onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise -> + repo.isPathModified(filePath).then(onSuccess) + runs -> + expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() - it "returns true if the path is modified", -> + it "resolves true if the path is modified", -> fs.writeFileSync(filePath, "change") - expect(repo.isPathModified(filePath)).toBeTruthy() + onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise -> + repo.isPathModified(filePath).then(onSuccess) + runs -> + expect(onSuccess.mostRecentCall.args[0]).toBeTruthy() - it "returns true if the path is deleted", -> + + it "resolves true if the path is deleted", -> fs.removeSync(filePath) - expect(repo.isPathModified(filePath)).toBeTruthy() + onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise -> + repo.isPathModified(filePath).then(onSuccess) + runs -> + expect(onSuccess.mostRecentCall.args[0]).toBeTruthy() - it "returns false if the path is new", -> - expect(repo.isPathModified(newPath)).toBeFalsy() + it "resolves false if the path is new", -> + onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise -> + repo.isPathModified(newPath).then(onSuccess) + runs -> + expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() + + it "resolves false if the path is invalid", -> + onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise -> + repo.isPathModified(emptyPath).then(onSuccess) + runs -> + expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() xdescribe ".isPathNew(path)", -> [filePath, newPath] = [] diff --git a/src/git-repository-async.js b/src/git-repository-async.js index e6d5a8f5d..445e88ccb 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -1,6 +1,7 @@ "use babel"; const Git = require('nodegit') +const path = require('path') module.exports = class GitRepositoryAsync { static open(path) { @@ -35,4 +36,19 @@ module.exports = class GitRepositoryAsync { return Promise.resolve(Git.Ignore.pathIsIgnored(repo, _path)) }) } + + isPathModified(_path) { + // Surely I'm missing a built-in way to do this + var basePath = null + return this.repoPromise.then( (repo) => { + basePath = repo.workdir() + return repo.getStatus() + }).then( (statuses) => { + console.log(statuses.map(function(x){return x.path()})); + ret = statuses.filter((status)=> { + return _path == path.join(basePath, status.path()) && (status.isModified() || status.isDeleted()) + }).length > 0 + return Promise.resolve(ret) + }) + } } From 7679ff93d33aa42cb5e1fa56329fd220a672e9ba Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Thu, 15 Oct 2015 18:26:20 +0200 Subject: [PATCH 007/502] isPathNew --- spec/git-repository-async-spec.coffee | 21 +++++++++++++++------ src/git-repository-async.js | 22 +++++++++++++++++++--- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index 180988d83..0eba6a4f4 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -91,7 +91,7 @@ fdescribe "GitRepositoryAsync", -> beforeEach -> workingDirPath = copyRepository() - repo = new GitRepositoryAsync.open(workingDirPath) + repo = GitRepositoryAsync.open(workingDirPath) filePath = path.join(workingDirPath, 'a.txt') newPath = path.join(workingDirPath, 'new-path.txt') fs.writeFileSync(newPath, "i'm new here") @@ -136,22 +136,31 @@ fdescribe "GitRepositoryAsync", -> runs -> expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() - xdescribe ".isPathNew(path)", -> + describe ".isPathNew(path)", -> [filePath, newPath] = [] beforeEach -> workingDirPath = copyRepository() - repo = new GitRepository(workingDirPath) + repo = GitRepositoryAsync.open(workingDirPath) filePath = path.join(workingDirPath, 'a.txt') newPath = path.join(workingDirPath, 'new-path.txt') fs.writeFileSync(newPath, "i'm new here") - xdescribe "when the path is unstaged", -> + describe "when the path is unstaged", -> it "returns true if the path is new", -> - expect(repo.isPathNew(newPath)).toBeTruthy() + onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise -> + repo.isPathNew(newPath).then(onSuccess) + runs -> + expect(onSuccess.mostRecentCall.args[0]).toBeTruthy() it "returns false if the path isn't new", -> - expect(repo.isPathNew(filePath)).toBeFalsy() + onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise -> + repo.isPathModified(newPath).then(onSuccess) + runs -> + expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() + xdescribe ".checkoutHead(path)", -> [filePath] = [] diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 445e88ccb..6a07c93b3 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -37,16 +37,32 @@ module.exports = class GitRepositoryAsync { }) } - isPathModified(_path) { + _filterStatusesByPath(_path) { // Surely I'm missing a built-in way to do this var basePath = null return this.repoPromise.then( (repo) => { basePath = repo.workdir() return repo.getStatus() }).then( (statuses) => { - console.log(statuses.map(function(x){return x.path()})); + return statuses.filter(function (status) { + return _path == path.join(basePath, status.path()) + }) + }) + } + + isPathModified(_path) { + return this._filterStatusesByPath(_path).then(function(statuses) { ret = statuses.filter((status)=> { - return _path == path.join(basePath, status.path()) && (status.isModified() || status.isDeleted()) + return status.isModified() || status.isDeleted() + }).length > 0 + return Promise.resolve(ret) + }) + } + + isPathNew(_path) { + return this._filterStatusesByPath(_path).then(function(statuses) { + ret = statuses.filter((status)=> { + return status.isNew() }).length > 0 return Promise.resolve(ret) }) From ca4ac209d6c477da21971112d451e3ac74b5e32b Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Fri, 16 Oct 2015 18:49:14 +0200 Subject: [PATCH 008/502] Spike checkoutHead --- src/git-repository-async.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 6a07c93b3..59df07a0b 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -52,8 +52,8 @@ module.exports = class GitRepositoryAsync { isPathModified(_path) { return this._filterStatusesByPath(_path).then(function(statuses) { - ret = statuses.filter((status)=> { - return status.isModified() || status.isDeleted() + var ret = statuses.filter((status)=> { + return status.isModified() }).length > 0 return Promise.resolve(ret) }) @@ -61,10 +61,19 @@ module.exports = class GitRepositoryAsync { isPathNew(_path) { return this._filterStatusesByPath(_path).then(function(statuses) { - ret = statuses.filter((status)=> { + var ret = statuses.filter((status)=> { return status.isNew() }).length > 0 return Promise.resolve(ret) }) } + + checkoutHead (_path) { + return this.repoPromise.then(function (repo) { + var checkoutOptions = new Git.CheckoutOptions() + checkoutOptions.paths = [_path] + checkoutOptions.checkoutStrategy = Git.Checkout.STRATEGY.FORCE | Git.Checkout.STRATEGY.DISABLE_PATHSPEC_MATCH + Git.Checkout.head(repo, checkoutOptions) + }) + } } From 3b15f4b2598ffc7f67d2ecfff05b927e2fc629d5 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Fri, 16 Oct 2015 18:50:43 +0200 Subject: [PATCH 009/502] Conform to standardjs style --- src/git-repository-async.js | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 59df07a0b..aec474f45 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -1,10 +1,10 @@ -"use babel"; +'use babel' const Git = require('nodegit') const path = require('path') module.exports = class GitRepositoryAsync { - static open(path) { + static open (path) { // QUESTION: Should this wrap Git.Repository and reject with a nicer message? return new GitRepositoryAsync(Git.Repository.open(path)) } @@ -15,10 +15,10 @@ module.exports = class GitRepositoryAsync { this._opening = true // Do I use this outside of tests? - openPromise.then( (repo) => { + openPromise.then((repo) => { this.repo = repo this._opening = false - }).catch( (e) => { + }).catch((e) => { this._opening = false }) @@ -26,42 +26,42 @@ module.exports = class GitRepositoryAsync { } getPath () { - return this.repoPromise.then( (repo) => { + return this.repoPromise.then((repo) => { return Promise.resolve(repo.path().replace(/\/$/, '')) }) } - isPathIgnored(_path) { - return this.repoPromise.then( (repo) => { + isPathIgnored (_path) { + return this.repoPromise.then((repo) => { return Promise.resolve(Git.Ignore.pathIsIgnored(repo, _path)) }) } - _filterStatusesByPath(_path) { + _filterStatusesByPath (_path) { // Surely I'm missing a built-in way to do this var basePath = null - return this.repoPromise.then( (repo) => { + return this.repoPromise.then((repo) => { basePath = repo.workdir() return repo.getStatus() - }).then( (statuses) => { + }).then((statuses) => { return statuses.filter(function (status) { - return _path == path.join(basePath, status.path()) + return _path === path.join(basePath, status.path()) }) }) } - isPathModified(_path) { - return this._filterStatusesByPath(_path).then(function(statuses) { - var ret = statuses.filter((status)=> { + isPathModified (_path) { + return this._filterStatusesByPath(_path).then(function (statuses) { + var ret = statuses.filter((status) => { return status.isModified() }).length > 0 return Promise.resolve(ret) }) } - isPathNew(_path) { - return this._filterStatusesByPath(_path).then(function(statuses) { - var ret = statuses.filter((status)=> { + isPathNew (_path) { + return this._filterStatusesByPath(_path).then(function (statuses) { + var ret = statuses.filter((status) => { return status.isNew() }).length > 0 return Promise.resolve(ret) From dfb24ce617d9178cb79bffbdf9c55a7c91cd5799 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Fri, 16 Oct 2015 19:12:08 +0200 Subject: [PATCH 010/502] Clean up init a bit --- spec/git-repository-async-spec.coffee | 4 ++-- src/git-repository-async.js | 22 ++++++++-------------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index 0eba6a4f4..068756a72 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -34,8 +34,8 @@ fdescribe "GitRepositoryAsync", -> it "repo is null when no repository is found", -> repo = GitRepositoryAsync.open(path.join(temp.dir, 'nogit.txt')) - waitsFor -> - repo._opening is false + waitsForPromise {shouldReject: true}, -> + repo.repoPromise runs -> expect(repo.repo).toBe null diff --git a/src/git-repository-async.js b/src/git-repository-async.js index aec474f45..f3916eb19 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -3,26 +3,20 @@ const Git = require('nodegit') const path = require('path') +// GitUtils is temporarily used for ::relativize only, because I don't want +// to port it just yet. TODO: remove +const GitUtils = require('git-utils') + module.exports = class GitRepositoryAsync { static open (path) { // QUESTION: Should this wrap Git.Repository and reject with a nicer message? - return new GitRepositoryAsync(Git.Repository.open(path)) + return new GitRepositoryAsync(path) } - constructor (openPromise) { + constructor (path) { this.repo = null - // this could be replaced with a function - this._opening = true - - // Do I use this outside of tests? - openPromise.then((repo) => { - this.repo = repo - this._opening = false - }).catch((e) => { - this._opening = false - }) - - this.repoPromise = openPromise + this._gitUtilsRepo = GitUtils.open(path) // TODO remove after porting ::relativize + this.repoPromise = Git.Repository.open(path) } getPath () { From df949d3a3dcf3f446380d3ac437dc2d5173c360f Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Fri, 16 Oct 2015 19:24:34 +0200 Subject: [PATCH 011/502] more ::checkoutHead specs --- spec/git-repository-async-spec.coffee | 49 ++++++++++++++++----------- src/git-repository-async.js | 4 +-- 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index 068756a72..7de2a0b01 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -113,15 +113,6 @@ fdescribe "GitRepositoryAsync", -> runs -> expect(onSuccess.mostRecentCall.args[0]).toBeTruthy() - - it "resolves true if the path is deleted", -> - fs.removeSync(filePath) - onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise -> - repo.isPathModified(filePath).then(onSuccess) - runs -> - expect(onSuccess.mostRecentCall.args[0]).toBeTruthy() - it "resolves false if the path is new", -> onSuccess = jasmine.createSpy('onSuccess') waitsForPromise -> @@ -162,27 +153,47 @@ fdescribe "GitRepositoryAsync", -> expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() - xdescribe ".checkoutHead(path)", -> + describe ".checkoutHead(path)", -> [filePath] = [] beforeEach -> workingDirPath = copyRepository() - repo = new GitRepository(workingDirPath) + repo = GitRepositoryAsync.open(workingDirPath) filePath = path.join(workingDirPath, 'a.txt') it "no longer reports a path as modified after checkout", -> - expect(repo.isPathModified(filePath)).toBeFalsy() - fs.writeFileSync(filePath, 'ch ch changes') - expect(repo.isPathModified(filePath)).toBeTruthy() - expect(repo.checkoutHead(filePath)).toBeTruthy() - expect(repo.isPathModified(filePath)).toBeFalsy() + onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise -> + repo.isPathModified(filePath).then(onSuccess) + runs -> + expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() + fs.writeFileSync(filePath, 'ch ch changes') + + onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise -> + repo.isPathModified(filePath).then(onSuccess) + runs -> + expect(onSuccess.mostRecentCall.args[0]).toBeTruthy() + + # Don't need to assert that this succeded because waitsForPromise will + # fail if it was rejected. + waitsForPromise -> + repo.checkoutHead(filePath) + + onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise -> + repo.isPathModified(filePath).then(onSuccess) + runs -> + expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() it "restores the contents of the path to the original text", -> fs.writeFileSync(filePath, 'ch ch changes') - expect(repo.checkoutHead(filePath)).toBeTruthy() - expect(fs.readFileSync(filePath, 'utf8')).toBe '' + waitsForPromise -> + repo.checkoutHead(filePath) + runs -> + expect(fs.readFileSync(filePath, 'utf8')).toBe '' - it "fires a status-changed event if the checkout completes successfully", -> + xit "fires a status-changed event if the checkout completes successfully", -> fs.writeFileSync(filePath, 'ch ch changes') repo.getPathStatus(filePath) statusHandler = jasmine.createSpy('statusHandler') diff --git a/src/git-repository-async.js b/src/git-repository-async.js index f3916eb19..7d7aab570 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -63,9 +63,9 @@ module.exports = class GitRepositoryAsync { } checkoutHead (_path) { - return this.repoPromise.then(function (repo) { + return this.repoPromise.then((repo) => { var checkoutOptions = new Git.CheckoutOptions() - checkoutOptions.paths = [_path] + checkoutOptions.paths = [this._gitUtilsRepo.relativize(_path)] checkoutOptions.checkoutStrategy = Git.Checkout.STRATEGY.FORCE | Git.Checkout.STRATEGY.DISABLE_PATHSPEC_MATCH Git.Checkout.head(repo, checkoutOptions) }) From 58c989455ac82fdf50d4c1afb592933f5310db84 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 19 Oct 2015 13:47:51 +0200 Subject: [PATCH 012/502] Add .eslintrc --- .eslintrc | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .eslintrc diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 000000000..c7d309c6c --- /dev/null +++ b/.eslintrc @@ -0,0 +1,6 @@ +{ + "env": { + "es6": true + }, + "extends": "standard" +} From ab4ba2ca4ca07cb56927da8593d258043a49be48 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 19 Oct 2015 15:15:42 +0200 Subject: [PATCH 013/502] .getPathStatus() --- spec/git-repository-async-spec.coffee | 42 ++++++++++++++++---------- src/git-repository-async.js | 43 +++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 15 deletions(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index 7de2a0b01..2972db635 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -1,5 +1,6 @@ temp = require 'temp' GitRepositoryAsync = require '../src/git-repository-async' +Git = require 'nodegit' fs = require 'fs-plus' os = require 'os' path = require 'path' @@ -18,7 +19,7 @@ copyRepository = -> openFixture = (fixture)-> GitRepositoryAsync.open(path.join(__dirname, 'fixtures', 'git', fixture)) -fdescribe "GitRepositoryAsync", -> +describe "GitRepositoryAsync", -> repo = null # beforeEach -> @@ -154,6 +155,8 @@ fdescribe "GitRepositoryAsync", -> describe ".checkoutHead(path)", -> + # XXX this is failing sporadically with various errors around not finding the + # repo / files in the repo [filePath] = [] beforeEach -> @@ -175,16 +178,17 @@ fdescribe "GitRepositoryAsync", -> runs -> expect(onSuccess.mostRecentCall.args[0]).toBeTruthy() - # Don't need to assert that this succeded because waitsForPromise will - # fail if it was rejected. + # Don't need to assert that this succeded because waitsForPromise should + # fail if it was rejected.. waitsForPromise -> repo.checkoutHead(filePath) - onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise -> - repo.isPathModified(filePath).then(onSuccess) runs -> - expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() + onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise -> + repo.isPathModified(filePath).then(onSuccess) + runs -> + expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() it "restores the contents of the path to the original text", -> fs.writeFileSync(filePath, 'ch ch changes') @@ -243,25 +247,33 @@ fdescribe "GitRepositoryAsync", -> repo.destroy() expect(-> repo.getShortHead()).toThrow() - xdescribe ".getPathStatus(path)", -> + describe ".getPathStatus(path)", -> [filePath] = [] beforeEach -> workingDirectory = copyRepository() - repo = new GitRepository(workingDirectory) + repo = GitRepositoryAsync.open(workingDirectory) filePath = path.join(workingDirectory, 'file.txt') it "trigger a status-changed event when the new status differs from the last cached one", -> statusHandler = jasmine.createSpy("statusHandler") repo.onDidChangeStatus statusHandler fs.writeFileSync(filePath, '') - status = repo.getPathStatus(filePath) - expect(statusHandler.callCount).toBe 1 - expect(statusHandler.argsForCall[0][0]).toEqual {path: filePath, pathStatus: status} - fs.writeFileSync(filePath, 'abc') - status = repo.getPathStatus(filePath) - expect(statusHandler.callCount).toBe 1 + waitsForPromise -> + repo.getPathStatus(filePath) + + runs -> + expect(statusHandler.callCount).toBe 1 + status = Git.Status.STATUS.WT_MODIFIED + expect(statusHandler.argsForCall[0][0]).toEqual {path: filePath, pathStatus: status} + fs.writeFileSync(filePath, 'abc') + + waitsForPromise -> + status = repo.getPathStatus(filePath) + + runs -> + expect(statusHandler.callCount).toBe 1 xdescribe ".getDirectoryStatus(path)", -> [directoryPath, filePath] = [] diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 7d7aab570..d99948afc 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -2,6 +2,7 @@ const Git = require('nodegit') const path = require('path') +const {Emitter, Disposable, CompositeDisposable} = require('event-kit') // GitUtils is temporarily used for ::relativize only, because I don't want // to port it just yet. TODO: remove @@ -15,10 +16,17 @@ module.exports = class GitRepositoryAsync { constructor (path) { this.repo = null + this.emitter = new Emitter() + this.subscriptions = new CompositeDisposable() + this.pathStatusCache = {} this._gitUtilsRepo = GitUtils.open(path) // TODO remove after porting ::relativize this.repoPromise = Git.Repository.open(path) } + destroy () { + this.subscriptions.dispose() + } + getPath () { return this.repoPromise.then((repo) => { return Promise.resolve(repo.path().replace(/\/$/, '')) @@ -38,6 +46,7 @@ module.exports = class GitRepositoryAsync { basePath = repo.workdir() return repo.getStatus() }).then((statuses) => { + console.log('statuses', statuses) return statuses.filter(function (status) { return _path === path.join(basePath, status.path()) }) @@ -70,4 +79,38 @@ module.exports = class GitRepositoryAsync { Git.Checkout.head(repo, checkoutOptions) }) } + + // Returns a Promise that resolves to the status bit of a given path if it has + // one, otherwise 'current'. + getPathStatus (_path) { + var relativePath = this._gitUtilsRepo.relativize(_path) + return this.repoPromise.then((repo) => { + return this._filterStatusesByPath(_path) + }).then((statuses) => { + var cachedStatus = this.pathStatusCache[relativePath] || 0 + var status = statuses[0] ? statuses[0].statusBit() : Git.Status.STATUS.CURRENT + console.log('cachedStatus', cachedStatus, 'status', status) + if (status != cachedStatus) { + this.emitter.emit('did-change-status', {path: _path, pathStatus: status}) + } + this.pathStatusCache[relativePath] = status + return Promise.resolve(status) + }) + } + + // Event subscription + // ================== + + onDidChangeStatus (callback) { + return this.emitter.on('did-change-status', callback) + } + + onDidChangeStatuses (callback) { + return this.emitter.on('did-change-statuses', callback) + } + + onDidDestroy (callback) { + return this.emitter.on('did-destroy', callback) + } + } From 2c96f8ce3d9d79cd82d55c397a06c95a7b61fcef Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 19 Oct 2015 16:10:13 +0200 Subject: [PATCH 014/502] .getDirectoryStatus --- spec/git-repository-async-spec.coffee | 26 +++++++--- src/git-repository-async.js | 75 +++++++++++++++++++++------ 2 files changed, 80 insertions(+), 21 deletions(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index 2972db635..fc9bb3f96 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -275,20 +275,34 @@ describe "GitRepositoryAsync", -> runs -> expect(statusHandler.callCount).toBe 1 - xdescribe ".getDirectoryStatus(path)", -> + describe ".getDirectoryStatus(path)", -> [directoryPath, filePath] = [] beforeEach -> workingDirectory = copyRepository() - repo = new GitRepository(workingDirectory) + repo = GitRepositoryAsync.open(workingDirectory) directoryPath = path.join(workingDirectory, 'dir') filePath = path.join(directoryPath, 'b.txt') it "gets the status based on the files inside the directory", -> - expect(repo.isStatusModified(repo.getDirectoryStatus(directoryPath))).toBe false - fs.writeFileSync(filePath, 'abc') - repo.getPathStatus(filePath) - expect(repo.isStatusModified(repo.getDirectoryStatus(directoryPath))).toBe true + onSuccess = jasmine.createSpy('onSuccess') + onSuccess2 = jasmine.createSpy('onSuccess2') + + waitsForPromise -> + repo.getDirectoryStatus(directoryPath).then(onSuccess) + + runs -> + expect(onSuccess.callCount).toBe 1 + console.log onSuccess.mostRecentCall.args + expect(repo.isStatusModified(onSuccess.mostRecentCall)).toBe false + fs.writeFileSync(filePath, 'abc') + + waitsForPromise -> + repo.getDirectoryStatus(directoryPath).then(onSuccess2) + runs -> + expect(onSuccess2.callCount).toBe 1 + expect(repo.isStatusModified(onSuccess2.argsForCall[0][0])).toBe true + xdescribe ".refreshStatus()", -> [newPath, modifiedPath, cleanPath, originalModifiedPathText] = [] diff --git a/src/git-repository-async.js b/src/git-repository-async.js index d99948afc..b4399e138 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -39,20 +39,6 @@ module.exports = class GitRepositoryAsync { }) } - _filterStatusesByPath (_path) { - // Surely I'm missing a built-in way to do this - var basePath = null - return this.repoPromise.then((repo) => { - basePath = repo.workdir() - return repo.getStatus() - }).then((statuses) => { - console.log('statuses', statuses) - return statuses.filter(function (status) { - return _path === path.join(basePath, status.path()) - }) - }) - } - isPathModified (_path) { return this._filterStatusesByPath(_path).then(function (statuses) { var ret = statuses.filter((status) => { @@ -90,7 +76,7 @@ module.exports = class GitRepositoryAsync { var cachedStatus = this.pathStatusCache[relativePath] || 0 var status = statuses[0] ? statuses[0].statusBit() : Git.Status.STATUS.CURRENT console.log('cachedStatus', cachedStatus, 'status', status) - if (status != cachedStatus) { + if (status !== cachedStatus) { this.emitter.emit('did-change-status', {path: _path, pathStatus: status}) } this.pathStatusCache[relativePath] = status @@ -98,6 +84,65 @@ module.exports = class GitRepositoryAsync { }) } + // Get the status of a directory in the repository's working directory. + // + // * `directoryPath` The {String} path to check. + // + // Returns a promise resolving to a {Number} representing the status. This value can be passed to + // {::isStatusModified} or {::isStatusNew} to get more information. + + getDirectoryStatus (directoryPath) { + var relativePath = this._gitUtilsRepo.relativize(directoryPath) + return this.repoPromise.then((repo) => { + return this._filterStatusesByDirectory(relativePath) + }).then((statuses) => { + return Promise.all(statuses.map(function (s) { return s.statusBit() })).then(function (bits) { + var ret = 0 + var filteredBits = bits.filter(function (b) { return b > 0 }) + if (filteredBits.length > 0) { + ret = filteredBits.pop() + } + return Promise.resolve(ret) + }) + }) + } + + // Utility functions + // ================= + + // TODO fix with bitwise ops + isStatusNew (statusBit) { + return statusBit === Git.Status.STATUS.WT_NEW || statusBit === Git.Status.STATUS.INDEX_NEW + } + + isStatusModified (statusBit) { + return statusBit === Git.Status.STATUS.WT_MODIFIED || statusBit === Git.Status.STATUS.INDEX_MODIFIED + } + + _filterStatusesByPath (_path) { + // Surely I'm missing a built-in way to do this + var basePath = null + return this.repoPromise.then((repo) => { + basePath = repo.workdir() + return repo.getStatus() + }).then((statuses) => { + console.log('statuses', statuses) + return statuses.filter(function (status) { + return _path === path.join(basePath, status.path()) + }) + }) + } + + _filterStatusesByDirectory (directoryPath) { + return this.repoPromise.then(function (repo) { + return repo.getStatus() + }).then(function (statuses) { + var filtered = statuses.filter((status) => { + return status.path().indexOf(directoryPath) === 0 + }) + return Promise.resolve(filtered) + }) + } // Event subscription // ================== From 0e6d30e707fd1b9c1791fd1de19538db4000f927 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 19 Oct 2015 17:04:27 +0200 Subject: [PATCH 015/502] .refreshStatus() (partial implementation) --- spec/git-repository-async-spec.coffee | 15 ++++++----- src/git-repository-async.js | 38 ++++++++++++++++++++++++--- 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index fc9bb3f96..55d4087d8 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -293,7 +293,6 @@ describe "GitRepositoryAsync", -> runs -> expect(onSuccess.callCount).toBe 1 - console.log onSuccess.mostRecentCall.args expect(repo.isStatusModified(onSuccess.mostRecentCall)).toBe false fs.writeFileSync(filePath, 'abc') @@ -304,12 +303,12 @@ describe "GitRepositoryAsync", -> expect(repo.isStatusModified(onSuccess2.argsForCall[0][0])).toBe true - xdescribe ".refreshStatus()", -> + describe ".refreshStatus()", -> [newPath, modifiedPath, cleanPath, originalModifiedPathText] = [] beforeEach -> workingDirectory = copyRepository() - repo = new GitRepository(workingDirectory) + repo = GitRepositoryAsync.open(workingDirectory) modifiedPath = path.join(workingDirectory, 'file.txt') newPath = path.join(workingDirectory, 'untracked.txt') cleanPath = path.join(workingDirectory, 'other.txt') @@ -320,13 +319,15 @@ describe "GitRepositoryAsync", -> it "returns status information for all new and modified files", -> fs.writeFileSync(modifiedPath, 'making this path modified') statusHandler = jasmine.createSpy('statusHandler') + onSuccess = jasmine.createSpy('onSuccess') repo.onDidChangeStatuses statusHandler - repo.refreshStatus() - - waitsFor -> - statusHandler.callCount > 0 + waitsForPromise -> + repo.refreshStatus().then(onSuccess) runs -> + # Callers will use the promise returned by refreshStatus, not the + # cache directly + expect(onSuccess.mostRecentCall.args[0]).toEqual(repo.pathStatusCache) expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined() expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy() expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy() diff --git a/src/git-repository-async.js b/src/git-repository-async.js index b4399e138..34f91de15 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -4,9 +4,13 @@ const Git = require('nodegit') const path = require('path') const {Emitter, Disposable, CompositeDisposable} = require('event-kit') +// Temporary requires +// ================== // GitUtils is temporarily used for ::relativize only, because I don't want // to port it just yet. TODO: remove const GitUtils = require('git-utils') +// Just using this for _.isEqual and _.object, we should impl our own here +const _ = require('underscore-plus') module.exports = class GitRepositoryAsync { static open (path) { @@ -75,7 +79,6 @@ module.exports = class GitRepositoryAsync { }).then((statuses) => { var cachedStatus = this.pathStatusCache[relativePath] || 0 var status = statuses[0] ? statuses[0].statusBit() : Git.Status.STATUS.CURRENT - console.log('cachedStatus', cachedStatus, 'status', status) if (status !== cachedStatus) { this.emitter.emit('did-change-status', {path: _path, pathStatus: status}) } @@ -93,6 +96,7 @@ module.exports = class GitRepositoryAsync { getDirectoryStatus (directoryPath) { var relativePath = this._gitUtilsRepo.relativize(directoryPath) + // XXX _filterSBD already gets repoPromise return this.repoPromise.then((repo) => { return this._filterStatusesByDirectory(relativePath) }).then((statuses) => { @@ -107,16 +111,43 @@ module.exports = class GitRepositoryAsync { }) } + // Refreshes the git status. Note: the sync GitRepository class does this with + // a separate process, let's see if we can avoid that. + refreshStatus () { + // TODO add upstream, branch, and submodule tracking + return this.repoPromise.then((repo) => { + return repo.getStatus() + }).then((statuses) => { + // update the status cache + return Promise.all(statuses.map((status) => { + return [status.path(), status.statusBit()] + })).then((statusesByPath) => { + var newPathStatusCache = _.object(statusesByPath) + return Promise.resolve(newPathStatusCache) + }) + }).then((newPathStatusCache) => { + if (!_.isEqual(this.pathStatusCache, newPathStatusCache)) { + this.emitter.emit('did-change-statuses') + } + this.pathStatusCache = newPathStatusCache + return Promise.resolve(newPathStatusCache) + }) + } + // Utility functions // ================= + getCachedPathStatus (_path) { + return this.pathStatusCache[this._gitUtilsRepo.relativize(_path)] + } + // TODO fix with bitwise ops isStatusNew (statusBit) { - return statusBit === Git.Status.STATUS.WT_NEW || statusBit === Git.Status.STATUS.INDEX_NEW + return Object.is(statusBit, Git.Status.STATUS.WT_NEW) || Object.is(statusBit, Git.Status.STATUS.INDEX_NEW) } isStatusModified (statusBit) { - return statusBit === Git.Status.STATUS.WT_MODIFIED || statusBit === Git.Status.STATUS.INDEX_MODIFIED + return Object.is(statusBit, Git.Status.STATUS.WT_MODIFIED) || Object.is(statusBit, Git.Status.STATUS.INDEX_MODIFIED) } _filterStatusesByPath (_path) { @@ -126,7 +157,6 @@ module.exports = class GitRepositoryAsync { basePath = repo.workdir() return repo.getStatus() }).then((statuses) => { - console.log('statuses', statuses) return statuses.filter(function (status) { return _path === path.join(basePath, status.path()) }) From 60180677a20d4f49548de99812245aa13e1630a1 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Tue, 20 Oct 2015 13:59:58 +0200 Subject: [PATCH 016/502] add .async attribute to GitRepository --- spec/git-spec.coffee | 10 ++++++++++ src/git-repository.coffee | 3 +++ 2 files changed, 13 insertions(+) diff --git a/spec/git-spec.coffee b/spec/git-spec.coffee index a72cd47cd..edabd0722 100644 --- a/spec/git-spec.coffee +++ b/spec/git-spec.coffee @@ -25,6 +25,16 @@ describe "GitRepository", -> it "returns null when no repository is found", -> expect(GitRepository.open(path.join(temp.dir, 'nogit.txt'))).toBeNull() + describe ".async", -> + it "returns a GitRepositoryAsync for the same repo", -> + repoPath = path.join(__dirname, 'fixtures', 'git', 'master.git') + repo = new GitRepository(repoPath) + onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise -> + repo.async.getPath().then(onSuccess) + runs -> + expect(onSuccess.mostRecentCall.args[0]).toBe(repoPath) + describe "new GitRepository(path)", -> it "throws an exception when no repository is found", -> expect(-> new GitRepository(path.join(temp.dir, 'nogit.txt'))).toThrow() diff --git a/src/git-repository.coffee b/src/git-repository.coffee index 064b86dd3..724142264 100644 --- a/src/git-repository.coffee +++ b/src/git-repository.coffee @@ -3,6 +3,7 @@ _ = require 'underscore-plus' {Emitter, Disposable, CompositeDisposable} = require 'event-kit' fs = require 'fs-plus' +GitRepositoryAsync = require './git-repository-async' GitUtils = require 'git-utils' Task = require './task' @@ -75,6 +76,8 @@ class GitRepository unless @repo? throw new Error("No Git repository found searching path: #{path}") + @async = GitRepositoryAsync.open(path) + @statuses = {} @upstream = {ahead: 0, behind: 0} for submodulePath, submoduleRepo of @repo.submodules From 2ff283a6b0b820d855b6a466bd61a06177832712 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Tue, 20 Oct 2015 16:01:35 +0200 Subject: [PATCH 017/502] Async buffer events --- spec/git-repository-async-spec.coffee | 57 +++++++++++++++++++------- src/git-repository-async.js | 58 ++++++++++++++++++++++++--- src/git-repository.coffee | 6 ++- 3 files changed, 101 insertions(+), 20 deletions(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index 55d4087d8..3bab16bf7 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -332,7 +332,11 @@ describe "GitRepositoryAsync", -> expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy() expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy() - xdescribe "buffer events", -> + # This tests the async implementation's events directly, but ultimately I + # think we want users to just be able to subscribe to events on GitRepository + # and have them bubble up from async-land + + describe "buffer events", -> [editor] = [] beforeEach -> @@ -345,32 +349,57 @@ describe "GitRepositoryAsync", -> editor.insertNewline() statusHandler = jasmine.createSpy('statusHandler') - atom.project.getRepositories()[0].onDidChangeStatus statusHandler + repo = atom.project.getRepositories()[0] + repo.async.onDidChangeStatus statusHandler editor.save() - expect(statusHandler.callCount).toBe 1 - expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} + waitsFor -> + statusHandler.callCount == 1 + runs -> + expect(statusHandler.callCount).toBe 1 + expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} it "emits a status-changed event when a buffer is reloaded", -> fs.writeFileSync(editor.getPath(), 'changed') statusHandler = jasmine.createSpy('statusHandler') - atom.project.getRepositories()[0].onDidChangeStatus statusHandler + atom.project.getRepositories()[0].async.onDidChangeStatus statusHandler editor.getBuffer().reload() - expect(statusHandler.callCount).toBe 1 - expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} - editor.getBuffer().reload() - expect(statusHandler.callCount).toBe 1 + reloadHandler = jasmine.createSpy 'reloadHandler' + + waitsFor -> + statusHandler.callCount == 1 + runs -> + expect(statusHandler.callCount).toBe 1 + expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} + buffer = editor.getBuffer() + buffer.onDidReload(reloadHandler) + buffer.reload() + + waitsFor -> + reloadHandler.callCount == 1 + runs -> + expect(statusHandler.callCount).toBe 1 it "emits a status-changed event when a buffer's path changes", -> fs.writeFileSync(editor.getPath(), 'changed') statusHandler = jasmine.createSpy('statusHandler') - atom.project.getRepositories()[0].onDidChangeStatus statusHandler + atom.project.getRepositories()[0].async.onDidChangeStatus statusHandler editor.getBuffer().emitter.emit 'did-change-path' - expect(statusHandler.callCount).toBe 1 - expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} - editor.getBuffer().emitter.emit 'did-change-path' - expect(statusHandler.callCount).toBe 1 + waitsFor -> + statusHandler.callCount == 1 + runs -> + expect(statusHandler.callCount).toBe 1 + expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} + + pathHandler = jasmine.createSpy('pathHandler') + buffer = editor.getBuffer() + buffer.onDidChangePath pathHandler + buffer.emitter.emit 'did-change-path' + waitsFor -> + pathHandler.callCount == 1 + runs -> + expect(statusHandler.callCount).toBe 1 it "stops listening to the buffer when the repository is destroyed (regression)", -> atom.project.getRepositories()[0].destroy() diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 34f91de15..cbc040bbc 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -13,18 +13,35 @@ const GitUtils = require('git-utils') const _ = require('underscore-plus') module.exports = class GitRepositoryAsync { - static open (path) { + static open (path, options = {}) { // QUESTION: Should this wrap Git.Repository and reject with a nicer message? - return new GitRepositoryAsync(path) + return new GitRepositoryAsync(path, options) } - constructor (path) { + constructor (path, options) { this.repo = null this.emitter = new Emitter() this.subscriptions = new CompositeDisposable() this.pathStatusCache = {} this._gitUtilsRepo = GitUtils.open(path) // TODO remove after porting ::relativize this.repoPromise = Git.Repository.open(path) + + var {project, refreshOnWindowFocus} = options + this.project = project + if (refreshOnWindowFocus === undefined) { + refreshOnWindowFocus = true + } + if (refreshOnWindowFocus) { + // TODO + } + + if (this.project) { + this.subscriptions.add(this.project.onDidAddBuffer((buffer) => { + this.subscribeToBuffer(buffer) + })) + + this.project.getBuffers().forEach((buffer) => { this.subscribeToBuffer(buffer) }) + } } destroy () { @@ -73,6 +90,7 @@ module.exports = class GitRepositoryAsync { // Returns a Promise that resolves to the status bit of a given path if it has // one, otherwise 'current'. getPathStatus (_path) { + console.log('getting path status for', _path) var relativePath = this._gitUtilsRepo.relativize(_path) return this.repoPromise.then((repo) => { return this._filterStatusesByPath(_path) @@ -80,6 +98,7 @@ module.exports = class GitRepositoryAsync { var cachedStatus = this.pathStatusCache[relativePath] || 0 var status = statuses[0] ? statuses[0].statusBit() : Git.Status.STATUS.CURRENT if (status !== cachedStatus) { + console.log('async emitting', {path: _path, pathStatus: status}) this.emitter.emit('did-change-status', {path: _path, pathStatus: status}) } this.pathStatusCache[relativePath] = status @@ -134,8 +153,37 @@ module.exports = class GitRepositoryAsync { }) } - // Utility functions - // ================= + // Section: Private + // ================ + + subscribeToBuffer (buffer) { + var getBufferPathStatus = () => { + var _path = buffer.getPath() + var bufferSubscriptions = new CompositeDisposable() + + if (_path) { + // We don't need to do anything with this promise, we just want the + // emitted event side effect + this.getPathStatus(_path) + } + + bufferSubscriptions.add( + buffer.onDidSave(getBufferPathStatus), + buffer.onDidReload(getBufferPathStatus), + buffer.onDidChangePath(getBufferPathStatus) + ) + + bufferSubscriptions.add(() => { + buffer.onDidDestroy(() => { + bufferSubscriptions.dispose() + this.subscriptions.remove(bufferSubscriptions) + }) + }) + + this.subscriptions.add(bufferSubscriptions) + return + } + } getCachedPathStatus (_path) { return this.pathStatusCache[this._gitUtilsRepo.relativize(_path)] diff --git a/src/git-repository.coffee b/src/git-repository.coffee index 724142264..a2e28b127 100644 --- a/src/git-repository.coffee +++ b/src/git-repository.coffee @@ -76,7 +76,7 @@ class GitRepository unless @repo? throw new Error("No Git repository found searching path: #{path}") - @async = GitRepositoryAsync.open(path) + @async = GitRepositoryAsync.open(path, options) @statuses = {} @upstream = {ahead: 0, behind: 0} @@ -319,6 +319,9 @@ class GitRepository # Returns a {Number} representing the status. This value can be passed to # {::isStatusModified} or {::isStatusNew} to get more information. getPathStatus: (path) -> + # Trigger events emitted on the async repo as well + @async.getPathStatus(path) + repo = @getRepo(path) relativePath = @relativize(path) currentPathStatus = @statuses[relativePath] ? 0 @@ -479,6 +482,7 @@ class GitRepository # Refreshes the current git status in an outside process and asynchronously # updates the relevant properties. refreshStatus: -> + @async.refreshStatus() @handlerPath ?= require.resolve('./repository-status-handler') @statusTask?.terminate() From 716a96f81494d349ddf8ec9f969f891465f709d4 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Tue, 20 Oct 2015 16:02:34 +0200 Subject: [PATCH 018/502] Errant logs --- src/git-repository-async.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index cbc040bbc..543d1e610 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -90,7 +90,6 @@ module.exports = class GitRepositoryAsync { // Returns a Promise that resolves to the status bit of a given path if it has // one, otherwise 'current'. getPathStatus (_path) { - console.log('getting path status for', _path) var relativePath = this._gitUtilsRepo.relativize(_path) return this.repoPromise.then((repo) => { return this._filterStatusesByPath(_path) @@ -98,7 +97,6 @@ module.exports = class GitRepositoryAsync { var cachedStatus = this.pathStatusCache[relativePath] || 0 var status = statuses[0] ? statuses[0].statusBit() : Git.Status.STATUS.CURRENT if (status !== cachedStatus) { - console.log('async emitting', {path: _path, pathStatus: status}) this.emitter.emit('did-change-status', {path: _path, pathStatus: status}) } this.pathStatusCache[relativePath] = status From fbd0c3677c60bcdbf485baee03ec4176e075d0aa Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Tue, 20 Oct 2015 16:40:11 +0200 Subject: [PATCH 019/502] Fix up deserialize specs --- spec/git-repository-async-spec.coffee | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index 3bab16bf7..581872053 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -405,7 +405,7 @@ describe "GitRepositoryAsync", -> atom.project.getRepositories()[0].destroy() expect(-> editor.save()).not.toThrow() - xdescribe "when a project is deserialized", -> + describe "when a project is deserialized", -> [buffer, project2] = [] afterEach -> @@ -429,7 +429,10 @@ describe "GitRepositoryAsync", -> buffer.append('changes') statusHandler = jasmine.createSpy('statusHandler') - project2.getRepositories()[0].onDidChangeStatus statusHandler + project2.getRepositories()[0].async.onDidChangeStatus statusHandler buffer.save() - expect(statusHandler.callCount).toBe 1 - expect(statusHandler).toHaveBeenCalledWith {path: buffer.getPath(), pathStatus: 256} + waitsFor -> + statusHandler.callCount == 1 + runs -> + expect(statusHandler.callCount).toBe 1 + expect(statusHandler).toHaveBeenCalledWith {path: buffer.getPath(), pathStatus: 256} From 057f0817e392e3c5e7895628bb755958b2ac5236 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Tue, 20 Oct 2015 16:40:11 +0200 Subject: [PATCH 020/502] Fix up deserialize specs From 95b3ac806f3b72c0d854e71a4009eef5495c8fd6 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Tue, 20 Oct 2015 16:42:20 +0200 Subject: [PATCH 021/502] un-nest waitsForPromise --- spec/git-repository-async-spec.coffee | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index 581872053..8ca7c861f 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -182,13 +182,13 @@ describe "GitRepositoryAsync", -> # fail if it was rejected.. waitsForPromise -> repo.checkoutHead(filePath) - runs -> onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise -> - repo.isPathModified(filePath).then(onSuccess) - runs -> - expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() + + waitsForPromise -> + repo.isPathModified(filePath).then(onSuccess) + runs -> + expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() it "restores the contents of the path to the original text", -> fs.writeFileSync(filePath, 'ch ch changes') From 49969288bb54ff8a47aac232c00b43345e2366eb Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Tue, 20 Oct 2015 16:42:20 +0200 Subject: [PATCH 022/502] un-nest waitsForPromise From 196fb35b6553fd978839916994fc3b857ff3279c Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Tue, 20 Oct 2015 16:52:43 +0200 Subject: [PATCH 023/502] Add missing return in checkouthead --- src/git-repository-async.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 543d1e610..e3bf0e1ab 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -83,7 +83,7 @@ module.exports = class GitRepositoryAsync { var checkoutOptions = new Git.CheckoutOptions() checkoutOptions.paths = [this._gitUtilsRepo.relativize(_path)] checkoutOptions.checkoutStrategy = Git.Checkout.STRATEGY.FORCE | Git.Checkout.STRATEGY.DISABLE_PATHSPEC_MATCH - Git.Checkout.head(repo, checkoutOptions) + return Git.Checkout.head(repo, checkoutOptions) }) } From 8ff31bd3dcc5ab0b2f677af3fa94ec6a667460eb Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Tue, 20 Oct 2015 16:53:32 +0200 Subject: [PATCH 024/502] Remove no longer relevant comment --- spec/git-repository-async-spec.coffee | 2 -- 1 file changed, 2 deletions(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index 8ca7c861f..f5adf23ea 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -155,8 +155,6 @@ describe "GitRepositoryAsync", -> describe ".checkoutHead(path)", -> - # XXX this is failing sporadically with various errors around not finding the - # repo / files in the repo [filePath] = [] beforeEach -> From 4c0c732766f19f1cd4d82894e66f4d3ce4046172 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Tue, 20 Oct 2015 17:03:08 +0200 Subject: [PATCH 025/502] Get path status after checkout --- spec/git-repository-async-spec.coffee | 24 ++++++++++++++++-------- src/git-repository-async.js | 2 ++ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index f5adf23ea..1506d3453 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -195,17 +195,25 @@ describe "GitRepositoryAsync", -> runs -> expect(fs.readFileSync(filePath, 'utf8')).toBe '' - xit "fires a status-changed event if the checkout completes successfully", -> + it "fires a did-change-status event if the checkout completes successfully", -> fs.writeFileSync(filePath, 'ch ch changes') - repo.getPathStatus(filePath) statusHandler = jasmine.createSpy('statusHandler') - repo.onDidChangeStatus statusHandler - repo.checkoutHead(filePath) - expect(statusHandler.callCount).toBe 1 - expect(statusHandler.argsForCall[0][0]).toEqual {path: filePath, pathStatus: 0} - repo.checkoutHead(filePath) - expect(statusHandler.callCount).toBe 1 + waitsForPromise -> + repo.getPathStatus(filePath) + runs -> + repo.onDidChangeStatus statusHandler + + waitsForPromise -> + repo.checkoutHead(filePath) + runs -> + expect(statusHandler.callCount).toBe 1 + expect(statusHandler.argsForCall[0][0]).toEqual {path: filePath, pathStatus: 0} + + waitsForPromise -> + repo.checkoutHead(filePath) + runs -> + expect(statusHandler.callCount).toBe 1 xdescribe ".checkoutHeadForEditor(editor)", -> [filePath, editor] = [] diff --git a/src/git-repository-async.js b/src/git-repository-async.js index e3bf0e1ab..47f1c0a9b 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -84,6 +84,8 @@ module.exports = class GitRepositoryAsync { checkoutOptions.paths = [this._gitUtilsRepo.relativize(_path)] checkoutOptions.checkoutStrategy = Git.Checkout.STRATEGY.FORCE | Git.Checkout.STRATEGY.DISABLE_PATHSPEC_MATCH return Git.Checkout.head(repo, checkoutOptions) + }).then(() => { + return this.getPathStatus(_path) }) } From bea002bddbafbc0991cd287ed9e8dfde5d764720 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Tue, 20 Oct 2015 18:02:47 +0200 Subject: [PATCH 026/502] Incomplete implementation of checkoutHeadForEditor --- spec/git-repository-async-spec.coffee | 13 +++++----- src/git-repository-async.js | 34 +++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index 1506d3453..81dd27cd4 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -215,12 +215,12 @@ describe "GitRepositoryAsync", -> runs -> expect(statusHandler.callCount).toBe 1 - xdescribe ".checkoutHeadForEditor(editor)", -> + fdescribe ".checkoutHeadForEditor(editor)", -> [filePath, editor] = [] beforeEach -> workingDirPath = copyRepository() - repo = new GitRepository(workingDirPath) + repo = repo = GitRepositoryAsync.open(workingDirPath) filePath = path.join(workingDirPath, 'a.txt') fs.writeFileSync(filePath, 'ch ch changes') @@ -242,10 +242,11 @@ describe "GitRepositoryAsync", -> spyOn(atom, 'confirm') atom.config.set('editor.confirmCheckoutHeadRevision', false) - repo.checkoutHeadForEditor(editor) - - expect(fs.readFileSync(filePath, 'utf8')).toBe '' - expect(atom.confirm).not.toHaveBeenCalled() + waitsForPromise -> + repo.checkoutHeadForEditor(editor) + runs -> + expect(fs.readFileSync(filePath, 'utf8')).toBe '' + expect(atom.confirm).not.toHaveBeenCalled() xdescribe ".destroy()", -> it "throws an exception when any method is called after it is called", -> diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 47f1c0a9b..53e592998 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -1,4 +1,5 @@ 'use babel' +const atom = window.atom const Git = require('nodegit') const path = require('path') @@ -89,6 +90,39 @@ module.exports = class GitRepositoryAsync { }) } + checkoutHeadForEditor (editor) { + var filePath = editor.getPath() + if (!filePath) { + return Promise.reject('editor.filePath() is empty') + } + + var fileName = path.basename(filePath) + var checkoutHead = () => { + if (editor.buffer.isModified()) { + editor.buffer.reload() + } + return this.checkoutHead(filePath) + } + + var confirmCheckout = function () { + // This is bad tho + return Promise.resolve(atom.confirm({ + message: 'Confirm Checkout HEAD Revision', + detailedMessage: `Are you sure you want to discard all changes to "${fileName}" since the last Git commit?`, + buttons: { + OK: checkoutHead, + Cancel: null + } + })) + } + + if (atom.config.get('editor.confirmCheckoutHeadRevision')) { + return confirmCheckout() + } else { + return checkoutHead() + } + } + // Returns a Promise that resolves to the status bit of a given path if it has // one, otherwise 'current'. getPathStatus (_path) { From b359049b9d590cbaf3391a291cc2334ce4cc21c1 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Tue, 20 Oct 2015 18:05:54 +0200 Subject: [PATCH 027/502] Fix confirm dialog spec --- spec/git-repository-async-spec.coffee | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index 81dd27cd4..667da12ef 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -215,7 +215,7 @@ describe "GitRepositoryAsync", -> runs -> expect(statusHandler.callCount).toBe 1 - fdescribe ".checkoutHeadForEditor(editor)", -> + describe ".checkoutHeadForEditor(editor)", -> [filePath, editor] = [] beforeEach -> @@ -234,9 +234,10 @@ describe "GitRepositoryAsync", -> spyOn(atom, 'confirm').andCallFake ({buttons}) -> buttons.OK() atom.config.set('editor.confirmCheckoutHeadRevision', true) - repo.checkoutHeadForEditor(editor) - - expect(fs.readFileSync(filePath, 'utf8')).toBe '' + waitsForPromise -> + repo.checkoutHeadForEditor(editor) + runs -> + expect(fs.readFileSync(filePath, 'utf8')).toBe '' it "does not display a dialog when confirmation is disabled", -> spyOn(atom, 'confirm') From 7b87764992057d9798e6c7aa6c9c659974f53440 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Wed, 21 Oct 2015 17:09:32 +0200 Subject: [PATCH 028/502] this breaks under the new test env setup --- src/git-repository-async.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 53e592998..41ca6d05e 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -1,5 +1,4 @@ 'use babel' -const atom = window.atom const Git = require('nodegit') const path = require('path') From 9f0def76f53125d03bdfc5ed64acb64e76da1ca2 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Wed, 21 Oct 2015 17:39:36 +0200 Subject: [PATCH 029/502] Project.serialize is now Project::serialize - cf https://github.com/atom/atom/commit/26f0ef5424ba3e7e45dcb5a9a56f47348119678d --- spec/git-repository-async-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index 667da12ef..5cd8e22d1 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -426,7 +426,7 @@ describe "GitRepositoryAsync", -> atom.workspace.open('file.txt') runs -> - project2 = Project.deserialize(atom.project.serialize()) + project2 = atom.project.deserialize(atom.project.serialize(), atom.deserializers) buffer = project2.getBuffers()[0] waitsFor -> From 11ccb980fed0fbe178ceb418ef9498404917dc73 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Wed, 21 Oct 2015 17:53:24 +0200 Subject: [PATCH 030/502] use deserialize api right --- spec/git-repository-async-spec.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index 5cd8e22d1..6c4f54639 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -426,7 +426,8 @@ describe "GitRepositoryAsync", -> atom.workspace.open('file.txt') runs -> - project2 = atom.project.deserialize(atom.project.serialize(), atom.deserializers) + project2 = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + project2.deserialize(atom.project.serialize(), atom.deserializers) buffer = project2.getBuffers()[0] waitsFor -> From c2520f490956c8757cb426ac243a142bbb4fed9e Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Thu, 22 Oct 2015 15:42:42 +0200 Subject: [PATCH 031/502] Remove confirmation from git-repo-async to match git-repo cf https://github.com/atom/atom/commit/f9a269ed995c4151678aef787573fe44657e d6dd --- src/git-repository-async.js | 41 +++++++++++-------------------------- 1 file changed, 12 insertions(+), 29 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 41ca6d05e..2f45a016a 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -90,36 +90,19 @@ module.exports = class GitRepositoryAsync { } checkoutHeadForEditor (editor) { - var filePath = editor.getPath() - if (!filePath) { - return Promise.reject('editor.filePath() is empty') - } - - var fileName = path.basename(filePath) - var checkoutHead = () => { - if (editor.buffer.isModified()) { - editor.buffer.reload() - } - return this.checkoutHead(filePath) - } - - var confirmCheckout = function () { - // This is bad tho - return Promise.resolve(atom.confirm({ - message: 'Confirm Checkout HEAD Revision', - detailedMessage: `Are you sure you want to discard all changes to "${fileName}" since the last Git commit?`, - buttons: { - OK: checkoutHead, - Cancel: null + return new Promise(function (resolve, reject) { + var filePath = editor.getPath() + if (filePath) { + if (editor.buffer.isModified()) { + editor.buffer.reload() } - })) - } - - if (atom.config.get('editor.confirmCheckoutHeadRevision')) { - return confirmCheckout() - } else { - return checkoutHead() - } + resolve(filePath) + } else { + reject() + } + }).then((filePath) => { + return this.checkoutHead(filePath) + }) } // Returns a Promise that resolves to the status bit of a given path if it has From 02d8ead883dfd32cb9274fafa296225bfce40462 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Thu, 22 Oct 2015 15:49:21 +0200 Subject: [PATCH 032/502] Checkout head asyncly This is the first tentative step in actually using the nodegit wrapper. --- src/text-editor.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 30d5694c4..786965d41 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -646,7 +646,7 @@ class TextEditor extends Model checkoutHead = => @project.repositoryForDirectory(new Directory(path.dirname(filePath))) .then (repository) => - repository?.checkoutHeadForEditor(this) + repository?.async.checkoutHeadForEditor(this) if @config.get('editor.confirmCheckoutHeadRevision') @applicationDelegate.confirm From da91be304eabe9464bb2737eefb9d3a91b20ef02 Mon Sep 17 00:00:00 2001 From: Fazle Arefin Date: Fri, 23 Oct 2015 11:31:01 +1100 Subject: [PATCH 033/502] Updated README: Fedora 22 dnf install requires pathname of package --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 580f52294..d5c70ec1e 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ repeat these steps to upgrade to future releases. Currently only a 64-bit version is available. 1. Download `atom.x86_64.rpm` from the [Atom releases page](https://github.com/atom/atom/releases/latest). -2. Run `sudo dnf install atom.x86_64.rpm` on the downloaded package. +2. Run `sudo dnf install ./atom.x86_64.rpm` on the downloaded package. 3. Launch Atom using the installed `atom` command. The Linux version does not currently automatically update so you will need to From 22ff8c5a9ee71ed22e3e4af4d76a5a21de4afc0c Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Fri, 23 Oct 2015 12:42:00 +0200 Subject: [PATCH 034/502] don't return a promise here --- src/git-repository-async.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 2f45a016a..e1e9e55cf 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -62,10 +62,9 @@ module.exports = class GitRepositoryAsync { isPathModified (_path) { return this._filterStatusesByPath(_path).then(function (statuses) { - var ret = statuses.filter((status) => { + return statuses.filter((status) => { return status.isModified() }).length > 0 - return Promise.resolve(ret) }) } From 1d2834f2ade80a1ce7208193f5c7b47bdd9068d1 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Fri, 23 Oct 2015 12:42:16 +0200 Subject: [PATCH 035/502] .isProjectAtRoot --- src/git-repository-async.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index e1e9e55cf..b475f3467 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -251,4 +251,21 @@ module.exports = class GitRepositoryAsync { return this.emitter.on('did-destroy', callback) } + // + // Section: Repository Details + // + + // Returns a {Promise} that resolves true if at the root, false if in a + // subfolder of the repository. + isProjectAtRoot () { + if (this.projectAtRoot === undefined) { + this.projectAtRoot = Promise.resolve(() => { + return this.repoPromise.then((repo) => { + return this.project.relativize(repo.workdir) + }) + }) + } + + return this.projectAtRoot + } } From e01a699d74a1b86de21c2ce5893fff8d7d7641ee Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 26 Oct 2015 15:17:48 +0100 Subject: [PATCH 036/502] Export GitRepositoryAsync from the Atom global Mostly to have a handle to the nodegit repo statuses. --- exports/atom.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/exports/atom.coffee b/exports/atom.coffee index c3601e1cc..59ac090d1 100644 --- a/exports/atom.coffee +++ b/exports/atom.coffee @@ -8,6 +8,7 @@ module.exports = BufferedNodeProcess: require '../src/buffered-node-process' BufferedProcess: require '../src/buffered-process' GitRepository: require '../src/git-repository' + GitRepositoryAsync: require '../src/git-repository-async' Notification: require '../src/notification' TextBuffer: TextBuffer Point: Point From 03045674b86491f9ed04f364300052c5c6247a75 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 26 Oct 2015 15:18:11 +0100 Subject: [PATCH 037/502] Export nodegit from GitRepositoryAsync --- src/git-repository-async.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index b475f3467..52e974eb0 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -18,6 +18,10 @@ module.exports = class GitRepositoryAsync { return new GitRepositoryAsync(path, options) } + static get Git () { + return Git + } + constructor (path, options) { this.repo = null this.emitter = new Emitter() From 86f6b6217676a3424b1acff85cc970d27e4d606f Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 26 Oct 2015 15:18:37 +0100 Subject: [PATCH 038/502] Correctly determine new/mod/del status as per git-utils --- src/git-repository-async.js | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 52e974eb0..ada5fbd61 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -4,6 +4,11 @@ const Git = require('nodegit') const path = require('path') const {Emitter, Disposable, CompositeDisposable} = require('event-kit') +const modifiedStatusFlags = Git.Status.STATUS.WT_MODIFIED | Git.Status.STATUS.INDEX_MODIFIED | Git.Status.STATUS.WT_DELETED | Git.Status.STATUS.INDEX_DELETED | Git.Status.STATUS.WT_TYPECHANGE | Git.Status.STATUS.INDEX_TYPECHANGE +const newStatusFlags = Git.Status.STATUS.WT_NEW | Git.Status.STATUS.INDEX_NEW +const deletedStatusFlags = Git.Status.STATUS.WT_DELETED | Git.Status.STATUS.INDEX_DELETED +const indexStatusFlags = Git.Status.STATUS.INDEX_NEW | Git.Status.STATUS.INDEX_MODIFIED | Git.Status.STATUS.INDEX_DELETED | Git.Status.STATUS.INDEX_RENAMED | Git.Status.STATUS.INDEX_TYPECHANGE + // Temporary requires // ================== // GitUtils is temporarily used for ::relativize only, because I don't want @@ -133,18 +138,21 @@ module.exports = class GitRepositoryAsync { // {::isStatusModified} or {::isStatusNew} to get more information. getDirectoryStatus (directoryPath) { - var relativePath = this._gitUtilsRepo.relativize(directoryPath) + let relativePath = this._gitUtilsRepo.relativize(directoryPath) // XXX _filterSBD already gets repoPromise return this.repoPromise.then((repo) => { return this._filterStatusesByDirectory(relativePath) }).then((statuses) => { return Promise.all(statuses.map(function (s) { return s.statusBit() })).then(function (bits) { - var ret = 0 - var filteredBits = bits.filter(function (b) { return b > 0 }) + let directoryStatus = 0 + let filteredBits = bits.filter(function (b) { return b > 0 }) if (filteredBits.length > 0) { - ret = filteredBits.pop() + filteredBits.forEach(function (bit) { + directoryStatus |= bit + }) } - return Promise.resolve(ret) + + return directoryStatus }) }) } @@ -208,13 +216,24 @@ module.exports = class GitRepositoryAsync { return this.pathStatusCache[this._gitUtilsRepo.relativize(_path)] } - // TODO fix with bitwise ops isStatusNew (statusBit) { - return Object.is(statusBit, Git.Status.STATUS.WT_NEW) || Object.is(statusBit, Git.Status.STATUS.INDEX_NEW) + return (statusBit & newStatusFlags) > 0 } isStatusModified (statusBit) { - return Object.is(statusBit, Git.Status.STATUS.WT_MODIFIED) || Object.is(statusBit, Git.Status.STATUS.INDEX_MODIFIED) + return (statusBit & modifiedStatusFlags) > 0 + } + + isStatusStaged (statusBit) { + return (statusBit & indexStatusFlags) > 0 + } + + isStatusIgnored (statusBit) { + return (statusBit & (1 << 14)) > 0 + } + + isStatusDeleted (statusBit) { + return (statusBit & deletedStatusFlags) > 0 } _filterStatusesByPath (_path) { From e39fe437ec269033ac2dc0fae66f1b16c556ba67 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 26 Oct 2015 15:28:48 +0100 Subject: [PATCH 039/502] Replace a bunch of Promise.resolve return values with plain values --- src/git-repository-async.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index ada5fbd61..f5239fbbd 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -59,13 +59,13 @@ module.exports = class GitRepositoryAsync { getPath () { return this.repoPromise.then((repo) => { - return Promise.resolve(repo.path().replace(/\/$/, '')) + return repo.path().replace(/\/$/, '') }) } isPathIgnored (_path) { return this.repoPromise.then((repo) => { - return Promise.resolve(Git.Ignore.pathIsIgnored(repo, _path)) + Git.Ignore.pathIsIgnored(repo, _path) }) } @@ -79,10 +79,9 @@ module.exports = class GitRepositoryAsync { isPathNew (_path) { return this._filterStatusesByPath(_path).then(function (statuses) { - var ret = statuses.filter((status) => { + return statuses.filter((status) => { return status.isNew() }).length > 0 - return Promise.resolve(ret) }) } @@ -126,7 +125,7 @@ module.exports = class GitRepositoryAsync { this.emitter.emit('did-change-status', {path: _path, pathStatus: status}) } this.pathStatusCache[relativePath] = status - return Promise.resolve(status) + return status }) } @@ -169,14 +168,14 @@ module.exports = class GitRepositoryAsync { return [status.path(), status.statusBit()] })).then((statusesByPath) => { var newPathStatusCache = _.object(statusesByPath) - return Promise.resolve(newPathStatusCache) + return newPathStatusCache }) }).then((newPathStatusCache) => { if (!_.isEqual(this.pathStatusCache, newPathStatusCache)) { this.emitter.emit('did-change-statuses') } this.pathStatusCache = newPathStatusCache - return Promise.resolve(newPathStatusCache) + return newPathStatusCache }) } @@ -256,7 +255,7 @@ module.exports = class GitRepositoryAsync { var filtered = statuses.filter((status) => { return status.path().indexOf(directoryPath) === 0 }) - return Promise.resolve(filtered) + return filtered }) } // Event subscription From 4a478f7f84dbac3ee0ff25e1ca5efe6fab9bd327 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 26 Oct 2015 16:35:48 +0100 Subject: [PATCH 040/502] ES6 style fixes --- src/git-repository-async.js | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index f5239fbbd..79ebf359f 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -35,7 +35,7 @@ module.exports = class GitRepositoryAsync { this._gitUtilsRepo = GitUtils.open(path) // TODO remove after porting ::relativize this.repoPromise = Git.Repository.open(path) - var {project, refreshOnWindowFocus} = options + let {project, refreshOnWindowFocus} = options this.project = project if (refreshOnWindowFocus === undefined) { refreshOnWindowFocus = true @@ -87,7 +87,7 @@ module.exports = class GitRepositoryAsync { checkoutHead (_path) { return this.repoPromise.then((repo) => { - var checkoutOptions = new Git.CheckoutOptions() + let checkoutOptions = new Git.CheckoutOptions() checkoutOptions.paths = [this._gitUtilsRepo.relativize(_path)] checkoutOptions.checkoutStrategy = Git.Checkout.STRATEGY.FORCE | Git.Checkout.STRATEGY.DISABLE_PATHSPEC_MATCH return Git.Checkout.head(repo, checkoutOptions) @@ -98,7 +98,7 @@ module.exports = class GitRepositoryAsync { checkoutHeadForEditor (editor) { return new Promise(function (resolve, reject) { - var filePath = editor.getPath() + let filePath = editor.getPath() if (filePath) { if (editor.buffer.isModified()) { editor.buffer.reload() @@ -115,12 +115,12 @@ module.exports = class GitRepositoryAsync { // Returns a Promise that resolves to the status bit of a given path if it has // one, otherwise 'current'. getPathStatus (_path) { - var relativePath = this._gitUtilsRepo.relativize(_path) + let relativePath = this._gitUtilsRepo.relativize(_path) return this.repoPromise.then((repo) => { return this._filterStatusesByPath(_path) }).then((statuses) => { - var cachedStatus = this.pathStatusCache[relativePath] || 0 - var status = statuses[0] ? statuses[0].statusBit() : Git.Status.STATUS.CURRENT + let cachedStatus = this.pathStatusCache[relativePath] || 0 + let status = statuses[0] ? statuses[0].statusBit() : Git.Status.STATUS.CURRENT if (status !== cachedStatus) { this.emitter.emit('did-change-status', {path: _path, pathStatus: status}) } @@ -167,8 +167,7 @@ module.exports = class GitRepositoryAsync { return Promise.all(statuses.map((status) => { return [status.path(), status.statusBit()] })).then((statusesByPath) => { - var newPathStatusCache = _.object(statusesByPath) - return newPathStatusCache + return _.object(statusesByPath) }) }).then((newPathStatusCache) => { if (!_.isEqual(this.pathStatusCache, newPathStatusCache)) { @@ -183,9 +182,9 @@ module.exports = class GitRepositoryAsync { // ================ subscribeToBuffer (buffer) { - var getBufferPathStatus = () => { - var _path = buffer.getPath() - var bufferSubscriptions = new CompositeDisposable() + let getBufferPathStatus = () => { + let _path = buffer.getPath() + let bufferSubscriptions = new CompositeDisposable() if (_path) { // We don't need to do anything with this promise, we just want the @@ -237,7 +236,7 @@ module.exports = class GitRepositoryAsync { _filterStatusesByPath (_path) { // Surely I'm missing a built-in way to do this - var basePath = null + let basePath = null return this.repoPromise.then((repo) => { basePath = repo.workdir() return repo.getStatus() @@ -252,10 +251,9 @@ module.exports = class GitRepositoryAsync { return this.repoPromise.then(function (repo) { return repo.getStatus() }).then(function (statuses) { - var filtered = statuses.filter((status) => { + return statuses.filter((status) => { return status.path().indexOf(directoryPath) === 0 }) - return filtered }) } // Event subscription From abd41d120804f1d6c53078390f7baf149fd875ba Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 26 Oct 2015 16:35:57 +0100 Subject: [PATCH 041/502] Add .isSubmodule --- src/git-repository-async.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 79ebf359f..9fd8a9e1d 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -288,4 +288,21 @@ module.exports = class GitRepositoryAsync { return this.projectAtRoot } + + // Returns a {Promise} that resolves true if the given path is a submodule in + // the repository. + isSubmodule (_path) { + return this.repoPromise.then(function (repo) { + return repo.openIndex() + }).then(function (index) { + let entry = index.getByPath(_path) + let submoduleMode = 57344 // TODO compose this from libgit2 constants + + if (entry.mode === submoduleMode) { + return true + } else { + return false + } + }) + } } From 806047bd270094fba668929a36bd45c4c68f5491 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 26 Oct 2015 16:38:09 +0100 Subject: [PATCH 042/502] Add missing return --- src/git-repository-async.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 9fd8a9e1d..6c8fdd996 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -65,7 +65,7 @@ module.exports = class GitRepositoryAsync { isPathIgnored (_path) { return this.repoPromise.then((repo) => { - Git.Ignore.pathIsIgnored(repo, _path) + return Git.Ignore.pathIsIgnored(repo, _path) }) } From 4bbf435ba86b750cc632731c2b20e6b5f89564d2 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Wed, 28 Oct 2015 11:44:57 +0100 Subject: [PATCH 043/502] drop eslintrc, add babel-eslint and standard --- .eslintrc | 6 ------ package.json | 4 ++++ 2 files changed, 4 insertions(+), 6 deletions(-) delete mode 100644 .eslintrc diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index c7d309c6c..000000000 --- a/.eslintrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "env": { - "es6": true - }, - "extends": "standard" -} diff --git a/package.json b/package.json index 6f1970297..d0dd7bbb0 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,10 @@ "underscore-plus": "^1.6.6", "yargs": "^3.23.0" }, + "devDependencies" : { + "babel-eslint": "^4.1.3", + "standard": "^5.3.1" + }, "packageDependencies": { "atom-dark-syntax": "0.27.0", "atom-dark-ui": "0.51.0", From 64514fad7e19ba935f19a8155602faa5f6f54d07 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Fri, 30 Oct 2015 15:29:34 +0100 Subject: [PATCH 044/502] Nest waitsFors to avoid jasmine 1.3 async queue flakiness, cc @nathansobo --- spec/git-repository-async-spec.coffee | 116 +++++++++++++++----------- 1 file changed, 65 insertions(+), 51 deletions(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index 6c4f54639..ae79d6130 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -350,68 +350,82 @@ describe "GitRepositoryAsync", -> beforeEach -> atom.project.setPaths([copyRepository()]) - waitsForPromise -> - atom.workspace.open('other.txt').then (o) -> editor = o - it "emits a status-changed event when a buffer is saved", -> - editor.insertNewline() - - statusHandler = jasmine.createSpy('statusHandler') - repo = atom.project.getRepositories()[0] - repo.async.onDidChangeStatus statusHandler - editor.save() - waitsFor -> - statusHandler.callCount == 1 + waitsForPromise -> + atom.workspace.open('other.txt').then (o) -> + editor = o runs -> - expect(statusHandler.callCount).toBe 1 - expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} + editor.insertNewline() + statusHandler = jasmine.createSpy('statusHandler') + repo = atom.project.getRepositories()[0] + repo.async.onDidChangeStatus statusHandler + editor.save() + + waitsFor -> + statusHandler.callCount >= 1 + runs -> + console.log "in run block" + expect(statusHandler.callCount).toBe 1 + expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} it "emits a status-changed event when a buffer is reloaded", -> - fs.writeFileSync(editor.getPath(), 'changed') - - statusHandler = jasmine.createSpy('statusHandler') - atom.project.getRepositories()[0].async.onDidChangeStatus statusHandler - editor.getBuffer().reload() - reloadHandler = jasmine.createSpy 'reloadHandler' - - waitsFor -> - statusHandler.callCount == 1 + waitsForPromise -> + atom.workspace.open('other.txt').then (o) -> + editor = o runs -> - expect(statusHandler.callCount).toBe 1 - expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} - buffer = editor.getBuffer() - buffer.onDidReload(reloadHandler) - buffer.reload() + fs.writeFileSync(editor.getPath(), 'changed') - waitsFor -> - reloadHandler.callCount == 1 - runs -> - expect(statusHandler.callCount).toBe 1 + statusHandler = jasmine.createSpy('statusHandler') + atom.project.getRepositories()[0].async.onDidChangeStatus statusHandler + editor.getBuffer().reload() + reloadHandler = jasmine.createSpy 'reloadHandler' + + waitsFor -> + statusHandler.callCount >= 1 + runs -> + expect(statusHandler.callCount).toBe 1 + expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} + buffer = editor.getBuffer() + buffer.onDidReload(reloadHandler) + buffer.reload() + + waitsFor -> + reloadHandler.callCount >= 1 + runs -> + expect(statusHandler.callCount).toBe 1 it "emits a status-changed event when a buffer's path changes", -> - fs.writeFileSync(editor.getPath(), 'changed') - - statusHandler = jasmine.createSpy('statusHandler') - atom.project.getRepositories()[0].async.onDidChangeStatus statusHandler - editor.getBuffer().emitter.emit 'did-change-path' - waitsFor -> - statusHandler.callCount == 1 + waitsForPromise -> + atom.workspace.open('other.txt').then (o) -> + editor = o runs -> - expect(statusHandler.callCount).toBe 1 - expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} + fs.writeFileSync(editor.getPath(), 'changed') - pathHandler = jasmine.createSpy('pathHandler') - buffer = editor.getBuffer() - buffer.onDidChangePath pathHandler - buffer.emitter.emit 'did-change-path' - waitsFor -> - pathHandler.callCount == 1 - runs -> - expect(statusHandler.callCount).toBe 1 + statusHandler = jasmine.createSpy('statusHandler') + atom.project.getRepositories()[0].async.onDidChangeStatus statusHandler + editor.getBuffer().emitter.emit 'did-change-path' + waitsFor -> + statusHandler.callCount >= 1 + runs -> + expect(statusHandler.callCount).toBe 1 + expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} + + pathHandler = jasmine.createSpy('pathHandler') + buffer = editor.getBuffer() + buffer.onDidChangePath pathHandler + buffer.emitter.emit 'did-change-path' + waitsFor -> + pathHandler.callCount >= 1 + runs -> + expect(statusHandler.callCount).toBe 1 it "stops listening to the buffer when the repository is destroyed (regression)", -> - atom.project.getRepositories()[0].destroy() - expect(-> editor.save()).not.toThrow() + waitsForPromise -> + atom.workspace.open('other.txt').then (o) -> + editor = o + runs -> + atom.project.getRepositories()[0].destroy() + expect(-> editor.save()).not.toThrow() describe "when a project is deserialized", -> [buffer, project2] = [] @@ -441,7 +455,7 @@ describe "GitRepositoryAsync", -> project2.getRepositories()[0].async.onDidChangeStatus statusHandler buffer.save() waitsFor -> - statusHandler.callCount == 1 + statusHandler.callCount >= 1 runs -> expect(statusHandler.callCount).toBe 1 expect(statusHandler).toHaveBeenCalledWith {path: buffer.getPath(), pathStatus: 256} From 454d6a537063f92bddfd06f593ec275fa4acc5cb Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Fri, 30 Oct 2015 15:32:52 +0100 Subject: [PATCH 045/502] :fire: errant log --- spec/git-repository-async-spec.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index ae79d6130..e02797074 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -364,7 +364,6 @@ describe "GitRepositoryAsync", -> waitsFor -> statusHandler.callCount >= 1 runs -> - console.log "in run block" expect(statusHandler.callCount).toBe 1 expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} From dcef6b301f188fb264ec71a8c474f10746eef795 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Fri, 30 Oct 2015 15:41:19 +0100 Subject: [PATCH 046/502] Formatting --- spec/git-repository-async-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index e02797074..03c0e624b 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -353,7 +353,7 @@ describe "GitRepositoryAsync", -> it "emits a status-changed event when a buffer is saved", -> waitsForPromise -> atom.workspace.open('other.txt').then (o) -> - editor = o + editor = o runs -> editor.insertNewline() statusHandler = jasmine.createSpy('statusHandler') From 13d51c54bbca5a29cfa0c9c7504029cead537d7e Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Fri, 30 Oct 2015 15:55:15 +0100 Subject: [PATCH 047/502] fix subscribeToBuffer method I ported this from coffee incorrectly. --- src/git-repository-async.js | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 6c8fdd996..cb41dde77 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -182,32 +182,30 @@ module.exports = class GitRepositoryAsync { // ================ subscribeToBuffer (buffer) { + let bufferSubscriptions = new CompositeDisposable() + let getBufferPathStatus = () => { let _path = buffer.getPath() - let bufferSubscriptions = new CompositeDisposable() - if (_path) { // We don't need to do anything with this promise, we just want the // emitted event side effect this.getPathStatus(_path) } - - bufferSubscriptions.add( - buffer.onDidSave(getBufferPathStatus), - buffer.onDidReload(getBufferPathStatus), - buffer.onDidChangePath(getBufferPathStatus) - ) - - bufferSubscriptions.add(() => { - buffer.onDidDestroy(() => { - bufferSubscriptions.dispose() - this.subscriptions.remove(bufferSubscriptions) - }) - }) - - this.subscriptions.add(bufferSubscriptions) - return } + + bufferSubscriptions.add( + buffer.onDidSave(getBufferPathStatus), + buffer.onDidReload(getBufferPathStatus), + buffer.onDidChangePath(getBufferPathStatus), + buffer.onDidDestroy(() => { + bufferSubscriptions.dispose() + this.subscriptions.remove(bufferSubscriptions) + }) + ) + + + this.subscriptions.add(bufferSubscriptions) + return } getCachedPathStatus (_path) { From 500e77dbd9a3756cf3747cf10c4880f95f87a768 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Fri, 30 Oct 2015 17:37:53 +0100 Subject: [PATCH 048/502] Start async spec over in JS --- spec/git-repository-async-spec.js | 41 +++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 spec/git-repository-async-spec.js diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js new file mode 100644 index 000000000..e45a9dc40 --- /dev/null +++ b/spec/git-repository-async-spec.js @@ -0,0 +1,41 @@ +'use babel' + +const path = require('path') +const temp = require('temp') + +const GitRepositoryAsync = require('../src/git-repository-async') + +const openFixture = (fixture) => { + GitRepositoryAsync.open(path.join(__dirname, 'fixtures', 'git', fixture)) +} + + +fdescribe('GitRepositoryAsync', () => { + describe('@open(path)', () => { + it('repo is null when no repository is found', () => { + let repo = GitRepositoryAsync.open(path.join(temp.dir, 'nogit.txt')) + + waitsForPromise({shouldReject: true}, () => { + return repo.repoPromise + }) + + runs(() => { + expect(repo.repo).toBe(null) + }) + }) + }) + + describe('.getPath()', () => { + it('returns the repository path for a repository path', () => { + let repo = openFixture('master.git') + let onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise(repo.getPath().then(onSuccess)) + + runs(() => { + expect(onSuccess.mostRecentCall.args[0]).toBe( + path.join(__dirname, 'fixtures', 'git', 'master.git') + ) + }) + }) + }) +}) From 41d3d7a0c037a72afd22eff2980d03d6c9d20211 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Fri, 30 Oct 2015 19:11:28 +0100 Subject: [PATCH 049/502] Start adding the troublesome buffer-events first --- spec/git-repository-async-spec.js | 46 ++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index e45a9dc40..d3cb9aaa2 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -1,5 +1,6 @@ 'use babel' +const fs = require('fs-plus') const path = require('path') const temp = require('temp') @@ -9,8 +10,15 @@ const openFixture = (fixture) => { GitRepositoryAsync.open(path.join(__dirname, 'fixtures', 'git', fixture)) } +const copyRepository = () => { + let workingDirPath = temp.mkdirSync('atom-working-dir') + fs.copySync(path.join(__dirname, 'fixtures', 'git', 'working-dir'), workingDirPath) + fs.renameSync(path.join(workingDirPath, 'git.git'), path.join(workingDirPath, '.git')) + return fs.realpathSync(workingDirPath) +} -fdescribe('GitRepositoryAsync', () => { + +describe('GitRepositoryAsync', () => { describe('@open(path)', () => { it('repo is null when no repository is found', () => { let repo = GitRepositoryAsync.open(path.join(temp.dir, 'nogit.txt')) @@ -26,6 +34,8 @@ fdescribe('GitRepositoryAsync', () => { }) describe('.getPath()', () => { + xit('returns the repository path for a .git directory path') + it('returns the repository path for a repository path', () => { let repo = openFixture('master.git') let onSuccess = jasmine.createSpy('onSuccess') @@ -38,4 +48,38 @@ fdescribe('GitRepositoryAsync', () => { }) }) }) + + fdescribe('buffer events', () => { + beforeEach(() => { + // This is sync, should be fine in a beforeEach + atom.project.setPaths([copyRepository()]) + }) + + it('emits a status-changed events when a buffer is saved', () => { + let editor, called + waitsForPromise(function () { + return atom.workspace.open('other.txt').then((o) => { + editor = o + }) + }) + + runs(() => { + editor.insertNewline() + let repo = atom.project.getRepositories()[0] + repo.async.onDidChangeStatus((c) => { + called = c + }) + editor.save() + }) + + waitsFor(() => { + return Boolean(called) + }) + + runs(() => { + expect(called).toEqual({path: editor.getPath(), pathStatus: 256}) + }) + }) + }) + }) From 9081a891e6629f79e47986f4bacadf7ca2f8b56a Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Fri, 30 Oct 2015 20:19:12 +0100 Subject: [PATCH 050/502] add status-changed on reload spec --- spec/git-repository-async-spec.js | 33 +++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index d3cb9aaa2..c9f9bc9d5 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -80,6 +80,39 @@ describe('GitRepositoryAsync', () => { expect(called).toEqual({path: editor.getPath(), pathStatus: 256}) }) }) + + it('emits a status-changed event when a buffer is reloaded', () => { + let editor + let statusHandler = jasmine.createSpy('statusHandler') + let reloadHandler = jasmine.createSpy('reloadHandler') + + waitsForPromise(function () { + return atom.workspace.open('other.txt').then((o) => { + editor = o + }) + }) + + runs(() => { + fs.writeFileSync(editor.getPath(), 'changed') + atom.project.getRepositories()[0].async.onDidChangeStatus(statusHandler) + editor.getBuffer().reload() + }) + + waitsFor(() => { + return statusHandler.callCount > 0 + }) + + runs(() => { + expect(statusHandler.callCount).toBe(1) + expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) + let buffer = editor.getBuffer() + buffer.onDidReload(reloadHandler) + buffer.reload() + }) + + waitsFor(() => { return reloadHandler.callCount > 0 }) + runs(() => { expect(statusHandler.callCount).toBe(1) }) + }) }) }) From 0c965f94390c4827018384a9ff83e3a017e6e689 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 2 Nov 2015 12:59:50 +0100 Subject: [PATCH 051/502] Flesh out ::destroy() --- src/git-repository-async.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index cb41dde77..a88ee6e6e 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -54,7 +54,15 @@ module.exports = class GitRepositoryAsync { } destroy () { - this.subscriptions.dispose() + if (this.emitter) { + this.emitter.emit('did-destroy') + this.emitter.dispose() + this.emitter = null + } + if (this.subscriptions) { + this.subscriptions.dispose() + this.subscriptions = null + } } getPath () { From da0b129c83147d7bb8799398218089c272bb5bb6 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Tue, 3 Nov 2015 16:36:24 +0100 Subject: [PATCH 052/502] Add standardjs config to package.json --- package.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/package.json b/package.json index 2de9c360f..2959a8020 100644 --- a/package.json +++ b/package.json @@ -157,5 +157,21 @@ "scripts": { "preinstall": "node -e 'process.exit(0)'", "test": "node script/test" + }, + "standard": { + "parser": "babel-eslint", + "globals": [ + "atom", + "afterEach", + "beforeEach", + "describe", + "expect", + "it", + "jasmine", + "runs", + "spyOn", + "waitsFor", + "waitsForPromise" + ] } } From 47edd1b9849e123c42e29398e0f77ffbfe2644d9 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Tue, 3 Nov 2015 16:39:01 +0100 Subject: [PATCH 053/502] =?UTF-8?q?Use=20async=E2=80=99s=20destroy=20callb?= =?UTF-8?q?ack=20in=20repo=20provider.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/git-repository-provider.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/git-repository-provider.coffee b/src/git-repository-provider.coffee index 593324d0c..463e2bda2 100644 --- a/src/git-repository-provider.coffee +++ b/src/git-repository-provider.coffee @@ -77,7 +77,7 @@ class GitRepositoryProvider unless repo repo = GitRepository.open(gitDirPath, {@project, @config}) return null unless repo - repo.onDidDestroy(=> delete @pathToRepository[gitDirPath]) + repo.async.onDidDestroy(=> delete @pathToRepository[gitDirPath]) @pathToRepository[gitDirPath] = repo repo.refreshIndex() repo.refreshStatus() From 4f06509fde3ae23e214a8c8f9102acc9fa20fa1a Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Tue, 3 Nov 2015 16:39:26 +0100 Subject: [PATCH 054/502] Destroy async repo from sync repo --- src/git-repository.coffee | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/git-repository.coffee b/src/git-repository.coffee index 9b46cf04d..46d379f98 100644 --- a/src/git-repository.coffee +++ b/src/git-repository.coffee @@ -120,6 +120,10 @@ class GitRepository @subscriptions.dispose() @subscriptions = null + if @async? + @async.destroy() + @async = null + # Public: Invoke the given callback when this GitRepository's destroy() method # is invoked. # From 009951e757ced8d4927d049ebdd210dfd16df525 Mon Sep 17 00:00:00 2001 From: joshaber Date: Fri, 6 Nov 2015 14:18:59 -0500 Subject: [PATCH 055/502] Get these tests going. --- spec/git-repository-async-spec.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index c9f9bc9d5..2a34a83b8 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -4,10 +4,12 @@ const fs = require('fs-plus') const path = require('path') const temp = require('temp') +temp.track() + const GitRepositoryAsync = require('../src/git-repository-async') const openFixture = (fixture) => { - GitRepositoryAsync.open(path.join(__dirname, 'fixtures', 'git', fixture)) + return GitRepositoryAsync.open(path.join(__dirname, 'fixtures', 'git', fixture)) } const copyRepository = () => { @@ -18,7 +20,7 @@ const copyRepository = () => { } -describe('GitRepositoryAsync', () => { +fdescribe('GitRepositoryAsync', () => { describe('@open(path)', () => { it('repo is null when no repository is found', () => { let repo = GitRepositoryAsync.open(path.join(temp.dir, 'nogit.txt')) @@ -39,7 +41,7 @@ describe('GitRepositoryAsync', () => { it('returns the repository path for a repository path', () => { let repo = openFixture('master.git') let onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise(repo.getPath().then(onSuccess)) + waitsForPromise(() => repo.getPath().then(onSuccess)) runs(() => { expect(onSuccess.mostRecentCall.args[0]).toBe( @@ -49,7 +51,7 @@ describe('GitRepositoryAsync', () => { }) }) - fdescribe('buffer events', () => { + describe('buffer events', () => { beforeEach(() => { // This is sync, should be fine in a beforeEach atom.project.setPaths([copyRepository()]) From d5181361a0509ac59afa6f4c6e9479e7a8953f1c Mon Sep 17 00:00:00 2001 From: joshaber Date: Fri, 6 Nov 2015 21:39:23 -0500 Subject: [PATCH 056/502] Async/await a lot of things. --- spec/git-repository-async-spec.coffee | 8 +- spec/git-repository-async-spec.js | 379 ++++++++++++++++++++++---- 2 files changed, 337 insertions(+), 50 deletions(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index 03c0e624b..b8df9b364 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -215,7 +215,7 @@ describe "GitRepositoryAsync", -> runs -> expect(statusHandler.callCount).toBe 1 - describe ".checkoutHeadForEditor(editor)", -> + xdescribe ".checkoutHeadForEditor(editor)", -> [filePath, editor] = [] beforeEach -> @@ -230,7 +230,7 @@ describe "GitRepositoryAsync", -> runs -> editor = atom.workspace.getActiveTextEditor() - it "displays a confirmation dialog by default", -> + xit "displays a confirmation dialog by default", -> spyOn(atom, 'confirm').andCallFake ({buttons}) -> buttons.OK() atom.config.set('editor.confirmCheckoutHeadRevision', true) @@ -239,7 +239,7 @@ describe "GitRepositoryAsync", -> runs -> expect(fs.readFileSync(filePath, 'utf8')).toBe '' - it "does not display a dialog when confirmation is disabled", -> + xit "does not display a dialog when confirmation is disabled", -> spyOn(atom, 'confirm') atom.config.set('editor.confirmCheckoutHeadRevision', false) @@ -393,7 +393,7 @@ describe "GitRepositoryAsync", -> runs -> expect(statusHandler.callCount).toBe 1 - it "emits a status-changed event when a buffer's path changes", -> + fit "emits a status-changed event when a buffer's path changes", -> waitsForPromise -> atom.workspace.open('other.txt').then (o) -> editor = o diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 2a34a83b8..d602d3e06 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -3,6 +3,8 @@ const fs = require('fs-plus') const path = require('path') const temp = require('temp') +const Git = require('nodegit') +const CompositeDisposable = require('event-kit').CompositeDisposable temp.track() @@ -19,8 +21,41 @@ const copyRepository = () => { return fs.realpathSync(workingDirPath) } +async function waitBetter(fn) { + const p = new Promise() + let first = true + const check = () => { + if (fn()) { + p.resolve() + first = false + return true + } else if (first) { + first = false + return false + } else { + p.reject() + return false + } + } + + if (!check()) { + window.setTimeout(check, 500) + } + return p +} + +fdescribe('GitRepositoryAsync-js', () => { + let subscriptions + + beforeEach(() => { + jasmine.useRealClock() + subscriptions = new CompositeDisposable() + }) + + afterEach(() => { + subscriptions.dispose() + }) -fdescribe('GitRepositoryAsync', () => { describe('@open(path)', () => { it('repo is null when no repository is found', () => { let repo = GitRepositoryAsync.open(path.join(temp.dir, 'nogit.txt')) @@ -51,70 +86,322 @@ fdescribe('GitRepositoryAsync', () => { }) }) + describe('.isPathIgnored(path)', () => { + it('resolves true for an ignored path', () => { + let repo = openFixture('ignore.git') + let onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise(() => repo.isPathIgnored('a.txt').then(onSuccess).catch(e => console.log(e))) + + runs(() => expect(onSuccess.mostRecentCall.args[0]).toBeTruthy()) + }) + + it('resolves false for a non-ignored path', () => { + let repo = openFixture('ignore.git') + let onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise(() => repo.isPathIgnored('b.txt').then(onSuccess)) + runs(() => expect(onSuccess.mostRecentCall.args[0]).toBeFalsy()) + }) + }) + + describe('.isPathModified(path)', () => { + let repo, filePath, newPath, emptyPath + + beforeEach(() => { + let workingDirPath = copyRepository() + repo = GitRepositoryAsync.open(workingDirPath) + filePath = path.join(workingDirPath, 'a.txt') + newPath = path.join(workingDirPath, 'new-path.txt') + fs.writeFileSync(newPath, "i'm new here") + emptyPath = path.join(workingDirPath, 'empty-path.txt') + }) + + describe('when the path is unstaged', () => { + it('resolves false if the path has not been modified', () => { + let onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise(() => repo.isPathModified(filePath).then(onSuccess)) + runs(() => expect(onSuccess.mostRecentCall.args[0]).toBeFalsy()) + }) + + it('resolves true if the path is modified', () => { + fs.writeFileSync(filePath, "change") + let onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise(() => repo.isPathModified(filePath).then(onSuccess)) + runs(() => expect(onSuccess.mostRecentCall.args[0]).toBeTruthy()) + }) + + it('resolves false if the path is new', () => { + let onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise(() => repo.isPathModified(newPath).then(onSuccess)) + runs(() => expect(onSuccess.mostRecentCall.args[0]).toBeFalsy()) + }) + + it('resolves false if the path is invalid', () => { + let onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise(() => repo.isPathModified(emptyPath).then(onSuccess)) + runs(() => expect(onSuccess.mostRecentCall.args[0]).toBeFalsy()) + }) + }) + }) + + describe('.isPathNew(path)', () => { + let filePath, newPath, repo + + beforeEach(() => { + let workingDirPath = copyRepository() + repo = GitRepositoryAsync.open(workingDirPath) + filePath = path.join(workingDirPath, 'a.txt') + newPath = path.join(workingDirPath, 'new-path.txt') + fs.writeFileSync(newPath, "i'm new here") + }) + + describe('when the path is unstaged', () => { + it('returns true if the path is new', () => { + let onSuccess = jasmine.createSpy('onSuccess') + waitsForPromise(() => repo.isPathNew(newPath).then(onSuccess)) + runs(() => expect(onSuccess.mostRecentCall.args[0]).toBeTruthy()) + }) + + it("returns false if the path isn't new", async () => { + let onSuccess = jasmine.createSpy('onSuccess') + + let modified = await repo.isPathModified(newPath).then(onSuccess) + expect(modified).toBeFalsy() + }) + }) + }) + + describe('.checkoutHead(path)', () => { + let filePath, repo + + beforeEach(() => { + let workingDirPath = copyRepository() + repo = GitRepositoryAsync.open(workingDirPath) + filePath = path.join(workingDirPath, 'a.txt') + }) + + it('no longer reports a path as modified after checkout', async () => { + let modified = await repo.isPathModified(filePath) + expect(modified).toBeFalsy() + fs.writeFileSync(filePath, 'ch ch changes') + + modified = await repo.isPathModified(filePath) + expect(modified).toBeTruthy() + + // Don't need to assert that this succeded because waitsForPromise should + // fail if it was rejected.. + await repo.checkoutHead(filePath) + + modified = await repo.isPathModified(filePath) + expect(modified).toBeFalsy() + }) + + it('restores the contents of the path to the original text', async () => { + fs.writeFileSync(filePath, 'ch ch changes') + await repo.checkoutHead(filePath) + xxpect(fs.readFileSync(filePath, 'utf8')).toBe('') + }) + + it('fires a did-change-status event if the checkout completes successfully', async () => { + fs.writeFileSync(filePath, 'ch ch changes') + + await repo.getPathStatus(filePath) + + let statusHandler = jasmine.createSpy('statusHandler') + subscriptions.add(repo.onDidChangeStatus(statusHandler)) + + await repo.checkoutHead(filePath) + + expect(statusHandler.callCount).toBe(1) + expect(statusHandler.argsForCall[0][0]).toEqual({path: filePath, pathStatus: 0}) + + await repo.checkoutHead(filePath) + expect(statusHandler.callCount).toBe(1) + }) + }) + + xdescribe('.checkoutHeadForEditor(editor)', () => { + let filePath, editor, repo + + beforeEach(() => { + let workingDirPath = copyRepository() + repo = GitRepositoryAsync.open(workingDirPath) + filePath = path.join(workingDirPath, 'a.txt') + fs.writeFileSync(filePath, 'ch ch changes') + + waitsForPromise(() => atom.workspace.open(filePath)) + runs(() => editor = atom.workspace.getActiveTextEditor()) + }) + + xit('displays a confirmation dialog by default', () => { + spyOn(atom, 'confirm').andCallFake(buttons, () => buttons[0].OK()) + atom.config.set('editor.confirmCheckoutHeadRevision', true) + + waitsForPromise(() => repo.checkoutHeadForEditor(editor)) + runs(() => expect(fs.readFileSync(filePath, 'utf8')).toBe('')) + }) + + xit('does not display a dialog when confirmation is disabled', () => { + spyOn(atom, 'confirm') + atom.config.set('editor.confirmCheckoutHeadRevision', false) + + waitsForPromise(() => repo.checkoutHeadForEditor(editor)) + runs(() => { + expect(fs.readFileSync(filePath, 'utf8')).toBe('') + expect(atom.confirm).not.toHaveBeenCalled() + }) + }) + }) + + describe('.getPathStatus(path)', () => { + let filePath, repo + + beforeEach(() => { + let workingDirectory = copyRepository() + repo = GitRepositoryAsync.open(workingDirectory) + filePath = path.join(workingDirectory, 'file.txt') + }) + + it('trigger a status-changed event when the new status differs from the last cached one', async () => { + let statusHandler = jasmine.createSpy("statusHandler") + subscriptions.add(repo.onDidChangeStatus(statusHandler)) + fs.writeFileSync(filePath, '') + + await repo.getPathStatus(filePath) + + expect(statusHandler.callCount).toBe(1) + let status = Git.Status.STATUS.WT_MODIFIED + expect(statusHandler.argsForCall[0][0]).toEqual({path: filePath, pathStatus: status}) + fs.writeFileSync(filePath, 'abc') + + await repo.getPathStatus(filePath) + expect(statusHandler.callCount).toBe(1) + }) + }) + + describe('.getDirectoryStatus(path)', () => { + let directoryPath, filePath, repo + + beforeEach(() => { + let workingDirectory = copyRepository() + repo = GitRepositoryAsync.open(workingDirectory) + directoryPath = path.join(workingDirectory, 'dir') + filePath = path.join(directoryPath, 'b.txt') + }) + + it('gets the status based on the files inside the directory', async () => { + let result = await repo.getDirectoryStatus(directoryPath) + expect(repo.isStatusModified(result)).toBe(false) + + fs.writeFileSync(filePath, 'abc') + + await repo.getPathStatus(filePath) + + result = await repo.getDirectoryStatus(directoryPath) + expect(repo.isStatusModified(result)).toBe(true) + }) + }) + + describe('.refreshStatus()', () => { + let newPath, modifiedPath, cleanPath, originalModifiedPathText, repo + + beforeEach(() => { + let workingDirectory = copyRepository() + repo = GitRepositoryAsync.open(workingDirectory) + modifiedPath = path.join(workingDirectory, 'file.txt') + newPath = path.join(workingDirectory, 'untracked.txt') + cleanPath = path.join(workingDirectory, 'other.txt') + fs.writeFileSync(cleanPath, 'Full of text') + fs.writeFileSync(newPath, '') + newPath = fs.absolute(newPath) // specs could be running under symbol path. + }) + + it('returns status information for all new and modified files', async () => { + fs.writeFileSync(modifiedPath, 'making this path modified') + await repo.refreshStatus() + + expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined() + expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy() + expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy() + }) + }) + describe('buffer events', () => { beforeEach(() => { // This is sync, should be fine in a beforeEach atom.project.setPaths([copyRepository()]) }) - it('emits a status-changed events when a buffer is saved', () => { - let editor, called - waitsForPromise(function () { - return atom.workspace.open('other.txt').then((o) => { - editor = o - }) - }) + it('emits a status-changed events when a buffer is saved', async () => { + let editor = await atom.workspace.open('other.txt') - runs(() => { - editor.insertNewline() - let repo = atom.project.getRepositories()[0] - repo.async.onDidChangeStatus((c) => { - called = c - }) - editor.save() - }) + editor.insertNewline() - waitsFor(() => { - return Boolean(called) - }) + let repository = atom.project.getRepositories()[0].async + let called + subscriptions.add(repository.onDidChangeStatus(c => called = c)) + editor.save() - runs(() => { - expect(called).toEqual({path: editor.getPath(), pathStatus: 256}) - }) + await waitBetter(() => Boolean(called)) + expect(called).toEqual({path: editor.getPath(), pathStatus: 256}) }) - it('emits a status-changed event when a buffer is reloaded', () => { - let editor + it('emits a status-changed event when a buffer is reloaded', async () => { let statusHandler = jasmine.createSpy('statusHandler') let reloadHandler = jasmine.createSpy('reloadHandler') - waitsForPromise(function () { - return atom.workspace.open('other.txt').then((o) => { - editor = o - }) - }) + let editor = await atom.workspace.open('other.txt') - runs(() => { - fs.writeFileSync(editor.getPath(), 'changed') - atom.project.getRepositories()[0].async.onDidChangeStatus(statusHandler) - editor.getBuffer().reload() - }) + fs.writeFileSync(editor.getPath(), 'changed') - waitsFor(() => { - return statusHandler.callCount > 0 - }) + let repository = atom.project.getRepositories()[0].async + subscriptions.add(repository.onDidChangeStatus(statusHandler)) + editor.getBuffer().reload() - runs(() => { - expect(statusHandler.callCount).toBe(1) - expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) - let buffer = editor.getBuffer() - buffer.onDidReload(reloadHandler) - buffer.reload() - }) + await waitBetter(() => statusHandler.callCount > 0) - waitsFor(() => { return reloadHandler.callCount > 0 }) - runs(() => { expect(statusHandler.callCount).toBe(1) }) + expect(statusHandler.callCount).toBe(1) + expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) + + let buffer = editor.getBuffer() + subscriptions.add(buffer.onDidReload(reloadHandler)) + buffer.reload() + + await waitBetter(() => reloadHandler.callCount > 0) + + expect(statusHandler.callCount).toBe(1) }) + + it("emits a status-changed event when a buffer's path changes", async () => { + let editor = await atom.workspace.open('other.txt') + + fs.writeFileSync(editor.getPath(), 'changed') + + let statusHandler = jasmine.createSpy('statusHandler') + let repository = atom.project.getRepositories()[0].async + subscriptions.add(repository.onDidChangeStatus(statusHandler)) + editor.getBuffer().emitter.emit('did-change-path') + await waitBetter(() => statusHandler.callCount >= 1) + + expect(statusHandler.callCount).toBe(1) + expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) + + let pathHandler = jasmine.createSpy('pathHandler') + let buffer = editor.getBuffer() + subscriptions.add(buffer.onDidChangePath(pathHandler)) + buffer.emitter.emit('did-change-path') + await waitBetter(() => pathHandler.callCount >= 1) + expect(statusHandler.callCount).toBe(1) + }) + + // it('stops listening to the buffer when the repository is destroyed (regression)', () => { + // waitsForPromise(() => { + // atom.workspace.open('other.txt').then(o => editor = o) + // }) + // runs(() => { + // atom.project.getRepositories()[0].destroy() + // expect(-> editor.save()).not.toThrow() + // }) + // }) }) }) From c076a047e4b021aec2bc6337b7bab896819816d5 Mon Sep 17 00:00:00 2001 From: joshaber Date: Sat, 7 Nov 2015 01:31:04 -0500 Subject: [PATCH 057/502] Async harder. --- spec/git-repository-async-spec.coffee | 2 +- spec/git-repository-async-spec.js | 180 +++++++++++--------------- 2 files changed, 80 insertions(+), 102 deletions(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index b8df9b364..4c25a3ec4 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -393,7 +393,7 @@ describe "GitRepositoryAsync", -> runs -> expect(statusHandler.callCount).toBe 1 - fit "emits a status-changed event when a buffer's path changes", -> + it "emits a status-changed event when a buffer's path changes", -> waitsForPromise -> atom.workspace.open('other.txt').then (o) -> editor = o diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index d602d3e06..e197a1369 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -21,90 +21,69 @@ const copyRepository = () => { return fs.realpathSync(workingDirPath) } -async function waitBetter(fn) { +async function terribleWait(fn) { const p = new Promise() - let first = true - const check = () => { - if (fn()) { - p.resolve() - first = false - return true - } else if (first) { - first = false - return false - } else { - p.reject() - return false - } - } + waitsFor(fn) + runs(() => p.resolve()) +} - if (!check()) { - window.setTimeout(check, 500) - } - return p +// Git uses heuristics to avoid having to hash a file to tell if it changed. One +// of those is mtime. So our tests are running Super Fast, we could end up +// changing a file multiple times within the same mtime tick, which could lead +// git to think the file didn't change at all. So sometimes we'll need to sleep. +function terribleSleep() { + for (let _ of range(1, 1000)) { ; } } fdescribe('GitRepositoryAsync-js', () => { - let subscriptions - - beforeEach(() => { - jasmine.useRealClock() - subscriptions = new CompositeDisposable() - }) + let repo afterEach(() => { - subscriptions.dispose() + if (repo != null) repo.destroy() }) describe('@open(path)', () => { - it('repo is null when no repository is found', () => { - let repo = GitRepositoryAsync.open(path.join(temp.dir, 'nogit.txt')) + it('repo is null when no repository is found', async () => { + repo = GitRepositoryAsync.open(path.join(temp.dir, 'nogit.txt')) - waitsForPromise({shouldReject: true}, () => { - return repo.repoPromise - }) + let threw = false + try { + await repo.repoPromise + } catch (e) { + threw = true + } - runs(() => { - expect(repo.repo).toBe(null) - }) + expect(threw).toBeTruthy() + expect(repo.repo).toBe(null) }) }) describe('.getPath()', () => { xit('returns the repository path for a .git directory path') - it('returns the repository path for a repository path', () => { - let repo = openFixture('master.git') - let onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise(() => repo.getPath().then(onSuccess)) - - runs(() => { - expect(onSuccess.mostRecentCall.args[0]).toBe( - path.join(__dirname, 'fixtures', 'git', 'master.git') - ) - }) + it('returns the repository path for a repository path', async () => { + repo = openFixture('master.git') + let path = await repo.getPath() + expect(path).toBe(path.join(__dirname, 'fixtures', 'git', 'master.git')) }) }) describe('.isPathIgnored(path)', () => { - it('resolves true for an ignored path', () => { - let repo = openFixture('ignore.git') - let onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise(() => repo.isPathIgnored('a.txt').then(onSuccess).catch(e => console.log(e))) - - runs(() => expect(onSuccess.mostRecentCall.args[0]).toBeTruthy()) + it('resolves true for an ignored path', async () => { + repo = openFixture('ignore.git') + let ignored = await repo.isPathIgnored('a.txt') + expect(ignored).toBeTruthy() }) - it('resolves false for a non-ignored path', () => { - let repo = openFixture('ignore.git') - let onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise(() => repo.isPathIgnored('b.txt').then(onSuccess)) - runs(() => expect(onSuccess.mostRecentCall.args[0]).toBeFalsy()) + it('resolves false for a non-ignored path', async () => { + repo = openFixture('ignore.git') + let ignored = await repo.isPathIgnored('b.txt') + expect(ignored).toBeFalsy() }) }) describe('.isPathModified(path)', () => { - let repo, filePath, newPath, emptyPath + let filePath, newPath, emptyPath beforeEach(() => { let workingDirPath = copyRepository() @@ -116,35 +95,31 @@ fdescribe('GitRepositoryAsync-js', () => { }) describe('when the path is unstaged', () => { - it('resolves false if the path has not been modified', () => { - let onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise(() => repo.isPathModified(filePath).then(onSuccess)) - runs(() => expect(onSuccess.mostRecentCall.args[0]).toBeFalsy()) + it('resolves false if the path has not been modified', async () => { + let modified = await repo.isPathModified(filePath) + expect(modified).toBeFalsy() }) - it('resolves true if the path is modified', () => { + it('resolves true if the path is modified', async () => { fs.writeFileSync(filePath, "change") - let onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise(() => repo.isPathModified(filePath).then(onSuccess)) - runs(() => expect(onSuccess.mostRecentCall.args[0]).toBeTruthy()) + let modified = await repo.isPathModified(filePath) + expect(modified).toBeTruthy() }) - it('resolves false if the path is new', () => { - let onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise(() => repo.isPathModified(newPath).then(onSuccess)) - runs(() => expect(onSuccess.mostRecentCall.args[0]).toBeFalsy()) + it('resolves false if the path is new', async () => { + let modified = await repo.isPathModified(newPath) + expect(modified).toBeFalsy() }) - it('resolves false if the path is invalid', () => { - let onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise(() => repo.isPathModified(emptyPath).then(onSuccess)) - runs(() => expect(onSuccess.mostRecentCall.args[0]).toBeFalsy()) + it('resolves false if the path is invalid', async () => { + let modified = await repo.isPathModified(emptyPath) + expect(modified).toBeFalsy() }) }) }) describe('.isPathNew(path)', () => { - let filePath, newPath, repo + let filePath, newPath beforeEach(() => { let workingDirPath = copyRepository() @@ -155,23 +130,20 @@ fdescribe('GitRepositoryAsync-js', () => { }) describe('when the path is unstaged', () => { - it('returns true if the path is new', () => { - let onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise(() => repo.isPathNew(newPath).then(onSuccess)) - runs(() => expect(onSuccess.mostRecentCall.args[0]).toBeTruthy()) + it('returns true if the path is new', async () => { + let isNew = await repo.isPathNew(newPath) + expect(isNew).toBeTruthy() }) it("returns false if the path isn't new", async () => { - let onSuccess = jasmine.createSpy('onSuccess') - - let modified = await repo.isPathModified(newPath).then(onSuccess) + let modified = await repo.isPathModified(newPath) expect(modified).toBeFalsy() }) }) }) describe('.checkoutHead(path)', () => { - let filePath, repo + let filePath beforeEach(() => { let workingDirPath = copyRepository() @@ -182,14 +154,15 @@ fdescribe('GitRepositoryAsync-js', () => { it('no longer reports a path as modified after checkout', async () => { let modified = await repo.isPathModified(filePath) expect(modified).toBeFalsy() + fs.writeFileSync(filePath, 'ch ch changes') + terribleSleep() modified = await repo.isPathModified(filePath) expect(modified).toBeTruthy() - // Don't need to assert that this succeded because waitsForPromise should - // fail if it was rejected.. await repo.checkoutHead(filePath) + terribleSleep() modified = await repo.isPathModified(filePath) expect(modified).toBeFalsy() @@ -207,10 +180,11 @@ fdescribe('GitRepositoryAsync-js', () => { await repo.getPathStatus(filePath) let statusHandler = jasmine.createSpy('statusHandler') - subscriptions.add(repo.onDidChangeStatus(statusHandler)) + repo.onDidChangeStatus(statusHandler) await repo.checkoutHead(filePath) + await terribleWait(() => statusHandler.callCount > 0) expect(statusHandler.callCount).toBe(1) expect(statusHandler.argsForCall[0][0]).toEqual({path: filePath, pathStatus: 0}) @@ -220,7 +194,7 @@ fdescribe('GitRepositoryAsync-js', () => { }) xdescribe('.checkoutHeadForEditor(editor)', () => { - let filePath, editor, repo + let filePath, editor beforeEach(() => { let workingDirPath = copyRepository() @@ -253,7 +227,7 @@ fdescribe('GitRepositoryAsync-js', () => { }) describe('.getPathStatus(path)', () => { - let filePath, repo + let filePath beforeEach(() => { let workingDirectory = copyRepository() @@ -263,11 +237,13 @@ fdescribe('GitRepositoryAsync-js', () => { it('trigger a status-changed event when the new status differs from the last cached one', async () => { let statusHandler = jasmine.createSpy("statusHandler") - subscriptions.add(repo.onDidChangeStatus(statusHandler)) + repo.onDidChangeStatus(statusHandler) fs.writeFileSync(filePath, '') await repo.getPathStatus(filePath) + await terribleWait(() => statusHandler.callCount > 0) + expect(statusHandler.callCount).toBe(1) let status = Git.Status.STATUS.WT_MODIFIED expect(statusHandler.argsForCall[0][0]).toEqual({path: filePath, pathStatus: status}) @@ -279,7 +255,7 @@ fdescribe('GitRepositoryAsync-js', () => { }) describe('.getDirectoryStatus(path)', () => { - let directoryPath, filePath, repo + let directoryPath, filePath beforeEach(() => { let workingDirectory = copyRepository() @@ -289,12 +265,14 @@ fdescribe('GitRepositoryAsync-js', () => { }) it('gets the status based on the files inside the directory', async () => { + await repo.checkoutHead(filePath) + terribleSleep() + let result = await repo.getDirectoryStatus(directoryPath) expect(repo.isStatusModified(result)).toBe(false) fs.writeFileSync(filePath, 'abc') - - await repo.getPathStatus(filePath) + terribleSleep() result = await repo.getDirectoryStatus(directoryPath) expect(repo.isStatusModified(result)).toBe(true) @@ -302,7 +280,7 @@ fdescribe('GitRepositoryAsync-js', () => { }) describe('.refreshStatus()', () => { - let newPath, modifiedPath, cleanPath, originalModifiedPathText, repo + let newPath, modifiedPath, cleanPath, originalModifiedPathText beforeEach(() => { let workingDirectory = copyRepository() @@ -338,10 +316,10 @@ fdescribe('GitRepositoryAsync-js', () => { let repository = atom.project.getRepositories()[0].async let called - subscriptions.add(repository.onDidChangeStatus(c => called = c)) + repository.onDidChangeStatus(c => called = c) editor.save() - await waitBetter(() => Boolean(called)) + await terribleWait(() => Boolean(called)) expect(called).toEqual({path: editor.getPath(), pathStatus: 256}) }) @@ -354,19 +332,19 @@ fdescribe('GitRepositoryAsync-js', () => { fs.writeFileSync(editor.getPath(), 'changed') let repository = atom.project.getRepositories()[0].async - subscriptions.add(repository.onDidChangeStatus(statusHandler)) + repository.onDidChangeStatus(statusHandler) editor.getBuffer().reload() - await waitBetter(() => statusHandler.callCount > 0) + await terribleWait(() => statusHandler.callCount > 0) expect(statusHandler.callCount).toBe(1) expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) let buffer = editor.getBuffer() - subscriptions.add(buffer.onDidReload(reloadHandler)) + buffer.onDidReload(reloadHandler) buffer.reload() - await waitBetter(() => reloadHandler.callCount > 0) + await terribleWait(() => reloadHandler.callCount > 0) expect(statusHandler.callCount).toBe(1) }) @@ -378,18 +356,18 @@ fdescribe('GitRepositoryAsync-js', () => { let statusHandler = jasmine.createSpy('statusHandler') let repository = atom.project.getRepositories()[0].async - subscriptions.add(repository.onDidChangeStatus(statusHandler)) + repository.onDidChangeStatus(statusHandler) editor.getBuffer().emitter.emit('did-change-path') - await waitBetter(() => statusHandler.callCount >= 1) + await terribleWait(() => statusHandler.callCount > 0) expect(statusHandler.callCount).toBe(1) expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) let pathHandler = jasmine.createSpy('pathHandler') let buffer = editor.getBuffer() - subscriptions.add(buffer.onDidChangePath(pathHandler)) + buffer.onDidChangePath(pathHandler) buffer.emitter.emit('did-change-path') - await waitBetter(() => pathHandler.callCount >= 1) + await terribleWait(() => pathHandler.callCount > 0) expect(statusHandler.callCount).toBe(1) }) From 87da3579e8a0ac8f831660b89eeeeb7d1ebc98ed Mon Sep 17 00:00:00 2001 From: joshaber Date: Sat, 7 Nov 2015 01:35:57 -0500 Subject: [PATCH 058/502] Cite it. --- spec/git-repository-async-spec.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index e197a1369..094139305 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -27,10 +27,11 @@ async function terribleWait(fn) { runs(() => p.resolve()) } -// Git uses heuristics to avoid having to hash a file to tell if it changed. One -// of those is mtime. So our tests are running Super Fast, we could end up -// changing a file multiple times within the same mtime tick, which could lead -// git to think the file didn't change at all. So sometimes we'll need to sleep. +// Git uses heuristics to avoid having to needlessly hash a file to tell if it +// changed. One of those is mtime. If our tests are running Super Fast, we could +// end up changing a file multiple times within the same mtime tick, which could +// lead git to think the file didn't change at all. So sometimes we'll need to +// sleep. (https://www.kernel.org/pub/software/scm/git/docs/technical/racy-git.txt) function terribleSleep() { for (let _ of range(1, 1000)) { ; } } From 995d912d760d54401a001edb419c6031a7e21192 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 9 Nov 2015 13:35:31 +0100 Subject: [PATCH 059/502] remove devDependencies due to license issues --- package.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/package.json b/package.json index b55d24502..74805410b 100644 --- a/package.json +++ b/package.json @@ -58,10 +58,6 @@ "underscore-plus": "^1.6.6", "yargs": "^3.23.0" }, - "devDependencies" : { - "babel-eslint": "^4.1.3", - "standard": "^5.3.1" - }, "packageDependencies": { "atom-dark-syntax": "0.27.0", "atom-dark-ui": "0.51.0", From 60f9180ec36550f0b2e8d00dcd606bf7733b8804 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 9 Nov 2015 14:26:24 +0100 Subject: [PATCH 060/502] add license overrides for shelljs and log-driver --- build/tasks/license-overrides.coffee | 41 ++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/build/tasks/license-overrides.coffee b/build/tasks/license-overrides.coffee index 287d44b63..e716f76a2 100644 --- a/build/tasks/license-overrides.coffee +++ b/build/tasks/license-overrides.coffee @@ -82,3 +82,44 @@ module.exports = 'core-js@0.4.10': license: 'MIT' source: 'http://rock.mit-license.org linked in source files and bower.json says MIT' + 'log-driver@1.2.4': + license: 'ISC' + source: 'LICENSE file in the repository' + sourceText: """ + Copyright (c) 2014, Gregg Caines, gregg@caines.ca + + Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + """ + 'shelljs@0.3.0': + license: 'BSD' + source: 'LICENSE file in repository - 3-clause BSD (aka BSD-new)' + sourceText: """ + Copyright (c) 2012, Artur Adib + All rights reserved. + + You may use this project under the terms of the New BSD license as follows: + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Artur Adib nor the + names of the contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL ARTUR ADIB BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + """ From 7598be861bb6531599a50d8fe4ee0e9d53ac7ca3 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 9 Nov 2015 15:08:03 +0100 Subject: [PATCH 061/502] Add empty ignore array to standard config - This version of grunt-standard requires SOMETHING to be set here, or it dies hard because it tries to push undefined into the array of ignored paths --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 74805410b..33f0fcc8b 100644 --- a/package.json +++ b/package.json @@ -156,6 +156,7 @@ "test": "node script/test" }, "standard": { + "ignore": [], "parser": "babel-eslint", "globals": [ "atom", From 93046f550f56011130b4071dec54ce4723f8d282 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 9 Nov 2015 15:13:00 +0100 Subject: [PATCH 062/502] Clean up lint errors from `./script/grunt standard:src` --- src/git-repository-async.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index a88ee6e6e..48fedc888 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -2,7 +2,7 @@ const Git = require('nodegit') const path = require('path') -const {Emitter, Disposable, CompositeDisposable} = require('event-kit') +const {Emitter, CompositeDisposable} = require('event-kit') const modifiedStatusFlags = Git.Status.STATUS.WT_MODIFIED | Git.Status.STATUS.INDEX_MODIFIED | Git.Status.STATUS.WT_DELETED | Git.Status.STATUS.INDEX_DELETED | Git.Status.STATUS.WT_TYPECHANGE | Git.Status.STATUS.INDEX_TYPECHANGE const newStatusFlags = Git.Status.STATUS.WT_NEW | Git.Status.STATUS.INDEX_NEW @@ -211,7 +211,6 @@ module.exports = class GitRepositoryAsync { }) ) - this.subscriptions.add(bufferSubscriptions) return } From ff4f26e1ec7a8863d1be2ff0fdaf1c68135280ec Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 9 Nov 2015 15:38:24 +0100 Subject: [PATCH 063/502] coffeelint fix --- spec/git-repository-async-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee index 4c25a3ec4..c6402d1fb 100644 --- a/spec/git-repository-async-spec.coffee +++ b/spec/git-repository-async-spec.coffee @@ -16,7 +16,7 @@ copyRepository = -> fs.renameSync(path.join(workingDirPath, 'git.git'), path.join(workingDirPath, '.git')) fs.realpathSync(workingDirPath) -openFixture = (fixture)-> +openFixture = (fixture) -> GitRepositoryAsync.open(path.join(__dirname, 'fixtures', 'git', fixture)) describe "GitRepositoryAsync", -> From 14d470a5e5e554b6c39c16653bb7f1db436c7c5a Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 9 Nov 2015 16:01:35 +0100 Subject: [PATCH 064/502] unforce spec --- spec/git-repository-async-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 094139305..a8c35b60a 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -36,7 +36,7 @@ function terribleSleep() { for (let _ of range(1, 1000)) { ; } } -fdescribe('GitRepositoryAsync-js', () => { +describe('GitRepositoryAsync-js', () => { let repo afterEach(() => { From bdc8a38045f75da7b8491dfff854db8fe722ebf2 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 9 Nov 2015 16:15:55 +0100 Subject: [PATCH 065/502] add final buffer event spec --- spec/git-repository-async-spec.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index a8c35b60a..e104be5e2 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -372,15 +372,16 @@ describe('GitRepositoryAsync-js', () => { expect(statusHandler.callCount).toBe(1) }) - // it('stops listening to the buffer when the repository is destroyed (regression)', () => { - // waitsForPromise(() => { - // atom.workspace.open('other.txt').then(o => editor = o) - // }) - // runs(() => { - // atom.project.getRepositories()[0].destroy() - // expect(-> editor.save()).not.toThrow() - // }) - // }) + it('stops listening to the buffer when the repository is destroyed (regression)', function () { + let editor + waitsForPromise(async function () { + editor = await atom.workspace.open('other.txt') + }) + runs(function () { + atom.project.getRepositories()[0].destroy() + expect(function () { editor.save() }).not.toThrow() + }) + }) }) }) From a9dae48ac4a64eb075d2eb0ebd47201fe7dafcf2 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 9 Nov 2015 10:39:02 -0500 Subject: [PATCH 066/502] It's better if we actually run the tests. --- spec/git-repository-async-spec.js | 117 ++++++++++++++---------------- 1 file changed, 54 insertions(+), 63 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index a8c35b60a..6456185a6 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -10,11 +10,11 @@ temp.track() const GitRepositoryAsync = require('../src/git-repository-async') -const openFixture = (fixture) => { +function openFixture(fixture) { return GitRepositoryAsync.open(path.join(__dirname, 'fixtures', 'git', fixture)) } -const copyRepository = () => { +function copyRepository() { let workingDirPath = temp.mkdirSync('atom-working-dir') fs.copySync(path.join(__dirname, 'fixtures', 'git', 'working-dir'), workingDirPath) fs.renameSync(path.join(workingDirPath, 'git.git'), path.join(workingDirPath, '.git')) @@ -23,20 +23,17 @@ const copyRepository = () => { async function terribleWait(fn) { const p = new Promise() - waitsFor(fn) - runs(() => p.resolve()) + p.resolve() + expect(fn()).toBeTruthy() } -// Git uses heuristics to avoid having to needlessly hash a file to tell if it -// changed. One of those is mtime. If our tests are running Super Fast, we could -// end up changing a file multiple times within the same mtime tick, which could -// lead git to think the file didn't change at all. So sometimes we'll need to -// sleep. (https://www.kernel.org/pub/software/scm/git/docs/technical/racy-git.txt) -function terribleSleep() { - for (let _ of range(1, 1000)) { ; } +function asyncIt(name, fn) { + it(name, () => { + waitsForPromise(fn) + }) } -describe('GitRepositoryAsync-js', () => { +fdescribe('GitRepositoryAsync-js', () => { let repo afterEach(() => { @@ -44,39 +41,41 @@ describe('GitRepositoryAsync-js', () => { }) describe('@open(path)', () => { - it('repo is null when no repository is found', async () => { - repo = GitRepositoryAsync.open(path.join(temp.dir, 'nogit.txt')) + it('repo is null when no repository is found', () => { + waitsForPromise(async () => { + repo = GitRepositoryAsync.open(path.join(temp.dir, 'nogit.txt')) - let threw = false - try { - await repo.repoPromise - } catch (e) { - threw = true - } + let threw = false + try { + await repo.repoPromise + } catch(e) { + threw = true + } - expect(threw).toBeTruthy() - expect(repo.repo).toBe(null) + expect(threw).toBeTruthy() + expect(repo.repo).toBe(null) + }) }) }) describe('.getPath()', () => { xit('returns the repository path for a .git directory path') - it('returns the repository path for a repository path', async () => { + asyncIt('returns the repository path for a repository path', async () => { repo = openFixture('master.git') - let path = await repo.getPath() - expect(path).toBe(path.join(__dirname, 'fixtures', 'git', 'master.git')) + let repoPath = await repo.getPath() + expect(repoPath).toBe(path.join(__dirname, 'fixtures', 'git', 'master.git')) }) }) describe('.isPathIgnored(path)', () => { - it('resolves true for an ignored path', async () => { + asyncIt('resolves true for an ignored path', async () => { repo = openFixture('ignore.git') let ignored = await repo.isPathIgnored('a.txt') expect(ignored).toBeTruthy() }) - it('resolves false for a non-ignored path', async () => { + asyncIt('resolves false for a non-ignored path', async () => { repo = openFixture('ignore.git') let ignored = await repo.isPathIgnored('b.txt') expect(ignored).toBeFalsy() @@ -96,23 +95,23 @@ describe('GitRepositoryAsync-js', () => { }) describe('when the path is unstaged', () => { - it('resolves false if the path has not been modified', async () => { + asyncIt('resolves false if the path has not been modified', async () => { let modified = await repo.isPathModified(filePath) expect(modified).toBeFalsy() }) - it('resolves true if the path is modified', async () => { + asyncIt('resolves true if the path is modified', async () => { fs.writeFileSync(filePath, "change") let modified = await repo.isPathModified(filePath) expect(modified).toBeTruthy() }) - it('resolves false if the path is new', async () => { + asyncIt('resolves false if the path is new', async () => { let modified = await repo.isPathModified(newPath) expect(modified).toBeFalsy() }) - it('resolves false if the path is invalid', async () => { + asyncIt('resolves false if the path is invalid', async () => { let modified = await repo.isPathModified(emptyPath) expect(modified).toBeFalsy() }) @@ -131,12 +130,12 @@ describe('GitRepositoryAsync-js', () => { }) describe('when the path is unstaged', () => { - it('returns true if the path is new', async () => { + asyncIt('returns true if the path is new', async () => { let isNew = await repo.isPathNew(newPath) expect(isNew).toBeTruthy() }) - it("returns false if the path isn't new", async () => { + asyncIt("returns false if the path isn't new", async () => { let modified = await repo.isPathModified(newPath) expect(modified).toBeFalsy() }) @@ -152,30 +151,28 @@ describe('GitRepositoryAsync-js', () => { filePath = path.join(workingDirPath, 'a.txt') }) - it('no longer reports a path as modified after checkout', async () => { + asyncIt('no longer reports a path as modified after checkout', async () => { let modified = await repo.isPathModified(filePath) expect(modified).toBeFalsy() fs.writeFileSync(filePath, 'ch ch changes') - terribleSleep() modified = await repo.isPathModified(filePath) expect(modified).toBeTruthy() await repo.checkoutHead(filePath) - terribleSleep() modified = await repo.isPathModified(filePath) expect(modified).toBeFalsy() }) - it('restores the contents of the path to the original text', async () => { + asyncIt('restores the contents of the path to the original text', async () => { fs.writeFileSync(filePath, 'ch ch changes') await repo.checkoutHead(filePath) - xxpect(fs.readFileSync(filePath, 'utf8')).toBe('') + expect(fs.readFileSync(filePath, 'utf8')).toBe('') }) - it('fires a did-change-status event if the checkout completes successfully', async () => { + asyncIt('fires a did-change-status event if the checkout completes successfully', async () => { fs.writeFileSync(filePath, 'ch ch changes') await repo.getPathStatus(filePath) @@ -185,7 +182,7 @@ describe('GitRepositoryAsync-js', () => { await repo.checkoutHead(filePath) - await terribleWait(() => statusHandler.callCount > 0) + // await terribleWait(() => statusHandler.callCount > 0) expect(statusHandler.callCount).toBe(1) expect(statusHandler.argsForCall[0][0]).toEqual({path: filePath, pathStatus: 0}) @@ -236,14 +233,14 @@ describe('GitRepositoryAsync-js', () => { filePath = path.join(workingDirectory, 'file.txt') }) - it('trigger a status-changed event when the new status differs from the last cached one', async () => { + asyncIt('trigger a status-changed event when the new status differs from the last cached one', async () => { let statusHandler = jasmine.createSpy("statusHandler") repo.onDidChangeStatus(statusHandler) fs.writeFileSync(filePath, '') await repo.getPathStatus(filePath) - await terribleWait(() => statusHandler.callCount > 0) + // await terribleWait(() => statusHandler.callCount > 0) expect(statusHandler.callCount).toBe(1) let status = Git.Status.STATUS.WT_MODIFIED @@ -265,15 +262,13 @@ describe('GitRepositoryAsync-js', () => { filePath = path.join(directoryPath, 'b.txt') }) - it('gets the status based on the files inside the directory', async () => { + asyncIt('gets the status based on the files inside the directory', async () => { await repo.checkoutHead(filePath) - terribleSleep() let result = await repo.getDirectoryStatus(directoryPath) expect(repo.isStatusModified(result)).toBe(false) fs.writeFileSync(filePath, 'abc') - terribleSleep() result = await repo.getDirectoryStatus(directoryPath) expect(repo.isStatusModified(result)).toBe(true) @@ -294,7 +289,7 @@ describe('GitRepositoryAsync-js', () => { newPath = fs.absolute(newPath) // specs could be running under symbol path. }) - it('returns status information for all new and modified files', async () => { + asyncIt('returns status information for all new and modified files', async () => { fs.writeFileSync(modifiedPath, 'making this path modified') await repo.refreshStatus() @@ -310,7 +305,7 @@ describe('GitRepositoryAsync-js', () => { atom.project.setPaths([copyRepository()]) }) - it('emits a status-changed events when a buffer is saved', async () => { + asyncIt('emits a status-changed event when a buffer is saved', async () => { let editor = await atom.workspace.open('other.txt') editor.insertNewline() @@ -320,11 +315,11 @@ describe('GitRepositoryAsync-js', () => { repository.onDidChangeStatus(c => called = c) editor.save() - await terribleWait(() => Boolean(called)) + // await terribleWait(() => Boolean(called)) expect(called).toEqual({path: editor.getPath(), pathStatus: 256}) }) - it('emits a status-changed event when a buffer is reloaded', async () => { + asyncIt('emits a status-changed event when a buffer is reloaded', async () => { let statusHandler = jasmine.createSpy('statusHandler') let reloadHandler = jasmine.createSpy('reloadHandler') @@ -336,7 +331,7 @@ describe('GitRepositoryAsync-js', () => { repository.onDidChangeStatus(statusHandler) editor.getBuffer().reload() - await terribleWait(() => statusHandler.callCount > 0) + // await terribleWait(() => statusHandler.callCount > 0) expect(statusHandler.callCount).toBe(1) expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) @@ -345,12 +340,12 @@ describe('GitRepositoryAsync-js', () => { buffer.onDidReload(reloadHandler) buffer.reload() - await terribleWait(() => reloadHandler.callCount > 0) + // await terribleWait(() => reloadHandler.callCount > 0) expect(statusHandler.callCount).toBe(1) }) - it("emits a status-changed event when a buffer's path changes", async () => { + asyncIt("emits a status-changed event when a buffer's path changes", async () => { let editor = await atom.workspace.open('other.txt') fs.writeFileSync(editor.getPath(), 'changed') @@ -359,7 +354,7 @@ describe('GitRepositoryAsync-js', () => { let repository = atom.project.getRepositories()[0].async repository.onDidChangeStatus(statusHandler) editor.getBuffer().emitter.emit('did-change-path') - await terribleWait(() => statusHandler.callCount > 0) + // await terribleWait(() => statusHandler.callCount > 0) expect(statusHandler.callCount).toBe(1) expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) @@ -368,19 +363,15 @@ describe('GitRepositoryAsync-js', () => { let buffer = editor.getBuffer() buffer.onDidChangePath(pathHandler) buffer.emitter.emit('did-change-path') - await terribleWait(() => pathHandler.callCount > 0) + // await terribleWait(() => pathHandler.callCount > 0) expect(statusHandler.callCount).toBe(1) }) - // it('stops listening to the buffer when the repository is destroyed (regression)', () => { - // waitsForPromise(() => { - // atom.workspace.open('other.txt').then(o => editor = o) - // }) - // runs(() => { - // atom.project.getRepositories()[0].destroy() - // expect(-> editor.save()).not.toThrow() - // }) - // }) + asyncIt('stops listening to the buffer when the repository is destroyed (regression)', async () => { + let editor = await atom.workspace.open('other.txt') + atom.project.getRepositories()[0].destroy() + expect(() => editor.save()).not.toThrow() + }) }) }) From e3af723c9b1fc093a65785835396576119a30ac7 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 9 Nov 2015 16:40:44 +0100 Subject: [PATCH 067/502] Copy relativize and getPathStatus from #9469 --- spec/git-repository-async-spec.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index e104be5e2..ee1ec830b 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -384,4 +384,6 @@ describe('GitRepositoryAsync-js', () => { }) }) + xdescribe('GitRepositoryAsync::relativize(filePath)') + }) From 0322d8d3dada0334edba74768ce4b54725ca7757 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 9 Nov 2015 16:48:02 +0100 Subject: [PATCH 068/502] Use our own ::relativize This allows us to remove the git-utils import. --- src/git-repository-async.js | 105 +++++++++++++++++++++++++++++++----- 1 file changed, 92 insertions(+), 13 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 48fedc888..532c89292 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -9,11 +9,6 @@ const newStatusFlags = Git.Status.STATUS.WT_NEW | Git.Status.STATUS.INDEX_NEW const deletedStatusFlags = Git.Status.STATUS.WT_DELETED | Git.Status.STATUS.INDEX_DELETED const indexStatusFlags = Git.Status.STATUS.INDEX_NEW | Git.Status.STATUS.INDEX_MODIFIED | Git.Status.STATUS.INDEX_DELETED | Git.Status.STATUS.INDEX_RENAMED | Git.Status.STATUS.INDEX_TYPECHANGE -// Temporary requires -// ================== -// GitUtils is temporarily used for ::relativize only, because I don't want -// to port it just yet. TODO: remove -const GitUtils = require('git-utils') // Just using this for _.isEqual and _.object, we should impl our own here const _ = require('underscore-plus') @@ -32,7 +27,6 @@ module.exports = class GitRepositoryAsync { this.emitter = new Emitter() this.subscriptions = new CompositeDisposable() this.pathStatusCache = {} - this._gitUtilsRepo = GitUtils.open(path) // TODO remove after porting ::relativize this.repoPromise = Git.Repository.open(path) let {project, refreshOnWindowFocus} = options @@ -96,7 +90,7 @@ module.exports = class GitRepositoryAsync { checkoutHead (_path) { return this.repoPromise.then((repo) => { let checkoutOptions = new Git.CheckoutOptions() - checkoutOptions.paths = [this._gitUtilsRepo.relativize(_path)] + checkoutOptions.paths = [this.relativize(_path, repo.workdir())] checkoutOptions.checkoutStrategy = Git.Checkout.STRATEGY.FORCE | Git.Checkout.STRATEGY.DISABLE_PATHSPEC_MATCH return Git.Checkout.head(repo, checkoutOptions) }).then(() => { @@ -120,20 +114,32 @@ module.exports = class GitRepositoryAsync { }) } + // Returns a Promise that resolves to the status bit of a given path if it has + // one, otherwise 'current'. // Returns a Promise that resolves to the status bit of a given path if it has // one, otherwise 'current'. getPathStatus (_path) { - let relativePath = this._gitUtilsRepo.relativize(_path) + let relativePath return this.repoPromise.then((repo) => { + relativePath = this.relativize(_path, repo.workdir()) return this._filterStatusesByPath(_path) - }).then((statuses) => { + }).then(statuses => { let cachedStatus = this.pathStatusCache[relativePath] || 0 let status = statuses[0] ? statuses[0].statusBit() : Git.Status.STATUS.CURRENT + + if (status > 0) { + this.pathStatusCache[relativePath] = status + } else { + delete this.pathStatusCache[relativePath] + } + if (status !== cachedStatus) { this.emitter.emit('did-change-status', {path: _path, pathStatus: status}) } - this.pathStatusCache[relativePath] = status + return status + }).catch(e => { + console.trace(e) }) } @@ -145,10 +151,9 @@ module.exports = class GitRepositoryAsync { // {::isStatusModified} or {::isStatusNew} to get more information. getDirectoryStatus (directoryPath) { - let relativePath = this._gitUtilsRepo.relativize(directoryPath) // XXX _filterSBD already gets repoPromise return this.repoPromise.then((repo) => { - return this._filterStatusesByDirectory(relativePath) + return this._filterStatusesByDirectory(this.relativize(directoryPath, repo.workdir())) }).then((statuses) => { return Promise.all(statuses.map(function (s) { return s.statusBit() })).then(function (bits) { let directoryStatus = 0 @@ -189,6 +194,78 @@ module.exports = class GitRepositoryAsync { // Section: Private // ================ + relativizeAsync (_path) { + this.repoPromise.then((repo) => { + return this.relativize(_path, repo.workdir()) + }) + } + + relativize (_path, workingDirectory) { + if (!Boolean(workingDirectory)) { + workingDirectory = this.getWorkingDirectorySync() // TODO + } + // Cargo-culted from git-utils. Could use a refactor maybe. Short circuits everywhere! + if (!_path) { + return _path + } + + if (process.platform === 'win32') { + _path = _path.replace(/\\/g, '/') + } else { + if (_path[0] !== '/') { + return _path + } + } + + if (this.isCaseInsensitive) { + let lowerCasePath = _path.toLowerCase() + + if (workingDirectory) { + workingDirectory = workingDirectory.toLowerCase() + if (lowerCasePath.indexOf(`${workingDirectory}/`) === 0) { + return _path.substring(workingDirectory.length + 1) + } else { + if (lowerCasePath === workingDirectory) { + return '' + } + } + } + + if (this.openedWorkingDirectory) { + workingDirectory = this.openedWorkingDirectory.toLowerCase() + if (lowerCasePath.indexOf(`${workingDirectory}/`) === 0) { + return _path.substring(workingDirectory.length + 1) + } else { + if (lowerCasePath === workingDirectory) { + return '' + } + } + } + } else { + workingDirectory = this.getWorkingDirectory() // TODO + if (workingDirectory) { + if (_path.indexOf(`${workingDirectory}/`) === 0) { + return _path.substring(workingDirectory.length + 1) + } else { + if (_path === workingDirectory) { + return '' + } + } + } + + if (this.openedWorkingDirectory) { + if (_path.indexOf(`${this.openedWorkingDirectory}/`) === 0) { + return _path.substring(this.openedWorkingDirectory.length + 1) + } else { + if (_path === this.openedWorkingDirectory) { + return '' + } + } + } + } + return _path + } + subscribeToBuffer (buffer) { let bufferSubscriptions = new CompositeDisposable() @@ -216,7 +293,9 @@ module.exports = class GitRepositoryAsync { } getCachedPathStatus (_path) { - return this.pathStatusCache[this._gitUtilsRepo.relativize(_path)] + return this.repoPromise.then((repo) => { + return this.pathStatusCache[this.relativize(_path, repo.workdir())] + }) } isStatusNew (statusBit) { From d8985c8175f25e6f40026c07980846c68d4da935 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 9 Nov 2015 16:51:02 +0100 Subject: [PATCH 069/502] await getCachedPathStatus in specs --- spec/git-repository-async-spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 7543430e7..f8a12fb4f 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -293,9 +293,9 @@ fdescribe('GitRepositoryAsync-js', () => { fs.writeFileSync(modifiedPath, 'making this path modified') await repo.refreshStatus() - expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined() - expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy() - expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy() + expect(await repo.getCachedPathStatus(cleanPath)).toBeUndefined() + expect(repo.isStatusNew(await repo.getCachedPathStatus(newPath))).toBeTruthy() + expect(repo.isStatusModified(await repo.getCachedPathStatus(modifiedPath))).toBeTruthy() }) }) From 8f59693d839f11f8f792c4271648d9e83d4541ba Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 9 Nov 2015 10:41:17 -0500 Subject: [PATCH 070/502] Use asyncIt here too. --- spec/git-repository-async-spec.js | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index f8a12fb4f..44cc4fd0b 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -41,20 +41,18 @@ fdescribe('GitRepositoryAsync-js', () => { }) describe('@open(path)', () => { - it('repo is null when no repository is found', () => { - waitsForPromise(async () => { - repo = GitRepositoryAsync.open(path.join(temp.dir, 'nogit.txt')) + asyncIt('repo is null when no repository is found', async () => { + repo = GitRepositoryAsync.open(path.join(temp.dir, 'nogit.txt')) - let threw = false - try { - await repo.repoPromise - } catch(e) { - threw = true - } + let threw = false + try { + await repo.repoPromise + } catch(e) { + threw = true + } - expect(threw).toBeTruthy() - expect(repo.repo).toBe(null) - }) + expect(threw).toBeTruthy() + expect(repo.repo).toBe(null) }) }) From cbc3dfb555d4b73901676f5610eb7733fd5fccf3 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 9 Nov 2015 10:51:28 -0500 Subject: [PATCH 071/502] :fire: terribleWait. --- spec/git-repository-async-spec.js | 61 +++++++++++++++---------------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 44cc4fd0b..cc2fe42d9 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -21,18 +21,18 @@ function copyRepository() { return fs.realpathSync(workingDirPath) } -async function terribleWait(fn) { - const p = new Promise() - p.resolve() - expect(fn()).toBeTruthy() -} - function asyncIt(name, fn) { it(name, () => { waitsForPromise(fn) }) } +function xasyncIt(name, fn) { + xit(name, () => { + waitsForPromise(fn) + }) +} + fdescribe('GitRepositoryAsync-js', () => { let repo @@ -180,7 +180,6 @@ fdescribe('GitRepositoryAsync-js', () => { await repo.checkoutHead(filePath) - // await terribleWait(() => statusHandler.callCount > 0) expect(statusHandler.callCount).toBe(1) expect(statusHandler.argsForCall[0][0]).toEqual({path: filePath, pathStatus: 0}) @@ -238,8 +237,6 @@ fdescribe('GitRepositoryAsync-js', () => { await repo.getPathStatus(filePath) - // await terribleWait(() => statusHandler.callCount > 0) - expect(statusHandler.callCount).toBe(1) let status = Git.Status.STATUS.WT_MODIFIED expect(statusHandler.argsForCall[0][0]).toEqual({path: filePath, pathStatus: status}) @@ -313,8 +310,8 @@ fdescribe('GitRepositoryAsync-js', () => { repository.onDidChangeStatus(c => called = c) editor.save() - // await terribleWait(() => Boolean(called)) - expect(called).toEqual({path: editor.getPath(), pathStatus: 256}) + waitsFor(() => Boolean(called)) + runs(() => expect(called).toEqual({path: editor.getPath(), pathStatus: 256})) }) asyncIt('emits a status-changed event when a buffer is reloaded', async () => { @@ -329,18 +326,18 @@ fdescribe('GitRepositoryAsync-js', () => { repository.onDidChangeStatus(statusHandler) editor.getBuffer().reload() - // await terribleWait(() => statusHandler.callCount > 0) + waitsFor(() => statusHandler.callCount > 0) + runs(() => { + expect(statusHandler.callCount).toBe(1) + expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) - expect(statusHandler.callCount).toBe(1) - expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) + let buffer = editor.getBuffer() + buffer.onDidReload(reloadHandler) + buffer.reload() - let buffer = editor.getBuffer() - buffer.onDidReload(reloadHandler) - buffer.reload() - - // await terribleWait(() => reloadHandler.callCount > 0) - - expect(statusHandler.callCount).toBe(1) + waitsFor(() => reloadHandler.callCount > 0) + runs(() => expect(statusHandler.callCount).toBe(1)) + }) }) asyncIt("emits a status-changed event when a buffer's path changes", async () => { @@ -352,20 +349,22 @@ fdescribe('GitRepositoryAsync-js', () => { let repository = atom.project.getRepositories()[0].async repository.onDidChangeStatus(statusHandler) editor.getBuffer().emitter.emit('did-change-path') - // await terribleWait(() => statusHandler.callCount > 0) + waitsFor(() => statusHandler.callCount > 0) + runs(() => { + expect(statusHandler.callCount).toBe(1) + expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) - expect(statusHandler.callCount).toBe(1) - expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) + let pathHandler = jasmine.createSpy('pathHandler') + let buffer = editor.getBuffer() + buffer.onDidChangePath(pathHandler) + buffer.emitter.emit('did-change-path') - let pathHandler = jasmine.createSpy('pathHandler') - let buffer = editor.getBuffer() - buffer.onDidChangePath(pathHandler) - buffer.emitter.emit('did-change-path') - // await terribleWait(() => pathHandler.callCount > 0) - expect(statusHandler.callCount).toBe(1) + waitsFor(() => pathHandler.callCount > 0) + runs(() => expect(statusHandler.callCount).toBe(1)) + }) }) - asyncIt('stops listening to the buffer when the repository is destroyed (regression)', async () => { + xasyncIt('stops listening to the buffer when the repository is destroyed (regression)', async () => { let editor = await atom.workspace.open('other.txt') atom.project.getRepositories()[0].destroy() expect(() => editor.save()).not.toThrow() From 3d2ed074707cc3e8983850e5cdf2a108ddceabf1 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 9 Nov 2015 16:56:51 +0100 Subject: [PATCH 072/502] Revert "Use our own ::relativize" This reverts commit 0322d8d3dada0334edba74768ce4b54725ca7757. --- src/git-repository-async.js | 105 +++++------------------------------- 1 file changed, 13 insertions(+), 92 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 532c89292..48fedc888 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -9,6 +9,11 @@ const newStatusFlags = Git.Status.STATUS.WT_NEW | Git.Status.STATUS.INDEX_NEW const deletedStatusFlags = Git.Status.STATUS.WT_DELETED | Git.Status.STATUS.INDEX_DELETED const indexStatusFlags = Git.Status.STATUS.INDEX_NEW | Git.Status.STATUS.INDEX_MODIFIED | Git.Status.STATUS.INDEX_DELETED | Git.Status.STATUS.INDEX_RENAMED | Git.Status.STATUS.INDEX_TYPECHANGE +// Temporary requires +// ================== +// GitUtils is temporarily used for ::relativize only, because I don't want +// to port it just yet. TODO: remove +const GitUtils = require('git-utils') // Just using this for _.isEqual and _.object, we should impl our own here const _ = require('underscore-plus') @@ -27,6 +32,7 @@ module.exports = class GitRepositoryAsync { this.emitter = new Emitter() this.subscriptions = new CompositeDisposable() this.pathStatusCache = {} + this._gitUtilsRepo = GitUtils.open(path) // TODO remove after porting ::relativize this.repoPromise = Git.Repository.open(path) let {project, refreshOnWindowFocus} = options @@ -90,7 +96,7 @@ module.exports = class GitRepositoryAsync { checkoutHead (_path) { return this.repoPromise.then((repo) => { let checkoutOptions = new Git.CheckoutOptions() - checkoutOptions.paths = [this.relativize(_path, repo.workdir())] + checkoutOptions.paths = [this._gitUtilsRepo.relativize(_path)] checkoutOptions.checkoutStrategy = Git.Checkout.STRATEGY.FORCE | Git.Checkout.STRATEGY.DISABLE_PATHSPEC_MATCH return Git.Checkout.head(repo, checkoutOptions) }).then(() => { @@ -114,32 +120,20 @@ module.exports = class GitRepositoryAsync { }) } - // Returns a Promise that resolves to the status bit of a given path if it has - // one, otherwise 'current'. // Returns a Promise that resolves to the status bit of a given path if it has // one, otherwise 'current'. getPathStatus (_path) { - let relativePath + let relativePath = this._gitUtilsRepo.relativize(_path) return this.repoPromise.then((repo) => { - relativePath = this.relativize(_path, repo.workdir()) return this._filterStatusesByPath(_path) - }).then(statuses => { + }).then((statuses) => { let cachedStatus = this.pathStatusCache[relativePath] || 0 let status = statuses[0] ? statuses[0].statusBit() : Git.Status.STATUS.CURRENT - - if (status > 0) { - this.pathStatusCache[relativePath] = status - } else { - delete this.pathStatusCache[relativePath] - } - if (status !== cachedStatus) { this.emitter.emit('did-change-status', {path: _path, pathStatus: status}) } - + this.pathStatusCache[relativePath] = status return status - }).catch(e => { - console.trace(e) }) } @@ -151,9 +145,10 @@ module.exports = class GitRepositoryAsync { // {::isStatusModified} or {::isStatusNew} to get more information. getDirectoryStatus (directoryPath) { + let relativePath = this._gitUtilsRepo.relativize(directoryPath) // XXX _filterSBD already gets repoPromise return this.repoPromise.then((repo) => { - return this._filterStatusesByDirectory(this.relativize(directoryPath, repo.workdir())) + return this._filterStatusesByDirectory(relativePath) }).then((statuses) => { return Promise.all(statuses.map(function (s) { return s.statusBit() })).then(function (bits) { let directoryStatus = 0 @@ -194,78 +189,6 @@ module.exports = class GitRepositoryAsync { // Section: Private // ================ - relativizeAsync (_path) { - this.repoPromise.then((repo) => { - return this.relativize(_path, repo.workdir()) - }) - } - - relativize (_path, workingDirectory) { - if (!Boolean(workingDirectory)) { - workingDirectory = this.getWorkingDirectorySync() // TODO - } - // Cargo-culted from git-utils. Could use a refactor maybe. Short circuits everywhere! - if (!_path) { - return _path - } - - if (process.platform === 'win32') { - _path = _path.replace(/\\/g, '/') - } else { - if (_path[0] !== '/') { - return _path - } - } - - if (this.isCaseInsensitive) { - let lowerCasePath = _path.toLowerCase() - - if (workingDirectory) { - workingDirectory = workingDirectory.toLowerCase() - if (lowerCasePath.indexOf(`${workingDirectory}/`) === 0) { - return _path.substring(workingDirectory.length + 1) - } else { - if (lowerCasePath === workingDirectory) { - return '' - } - } - } - - if (this.openedWorkingDirectory) { - workingDirectory = this.openedWorkingDirectory.toLowerCase() - if (lowerCasePath.indexOf(`${workingDirectory}/`) === 0) { - return _path.substring(workingDirectory.length + 1) - } else { - if (lowerCasePath === workingDirectory) { - return '' - } - } - } - } else { - workingDirectory = this.getWorkingDirectory() // TODO - if (workingDirectory) { - if (_path.indexOf(`${workingDirectory}/`) === 0) { - return _path.substring(workingDirectory.length + 1) - } else { - if (_path === workingDirectory) { - return '' - } - } - } - - if (this.openedWorkingDirectory) { - if (_path.indexOf(`${this.openedWorkingDirectory}/`) === 0) { - return _path.substring(this.openedWorkingDirectory.length + 1) - } else { - if (_path === this.openedWorkingDirectory) { - return '' - } - } - } - } - return _path - } - subscribeToBuffer (buffer) { let bufferSubscriptions = new CompositeDisposable() @@ -293,9 +216,7 @@ module.exports = class GitRepositoryAsync { } getCachedPathStatus (_path) { - return this.repoPromise.then((repo) => { - return this.pathStatusCache[this.relativize(_path, repo.workdir())] - }) + return this.pathStatusCache[this._gitUtilsRepo.relativize(_path)] } isStatusNew (statusBit) { From 371c68e65e05843cd4a9ef641184a9eba1f8adb7 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Mon, 9 Nov 2015 16:57:08 +0100 Subject: [PATCH 073/502] Revert "await getCachedPathStatus in specs" This reverts commit d8985c8175f25e6f40026c07980846c68d4da935. --- spec/git-repository-async-spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index cc2fe42d9..d02d31dd2 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -288,9 +288,9 @@ fdescribe('GitRepositoryAsync-js', () => { fs.writeFileSync(modifiedPath, 'making this path modified') await repo.refreshStatus() - expect(await repo.getCachedPathStatus(cleanPath)).toBeUndefined() - expect(repo.isStatusNew(await repo.getCachedPathStatus(newPath))).toBeTruthy() - expect(repo.isStatusModified(await repo.getCachedPathStatus(modifiedPath))).toBeTruthy() + expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined() + expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy() + expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy() }) }) From 4b8bad2b0bfc5482fb8389ef10d6615588379100 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 9 Nov 2015 12:10:49 -0500 Subject: [PATCH 074/502] Focus async its. --- spec/git-repository-async-spec.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index d02d31dd2..2470d24d3 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -27,6 +27,12 @@ function asyncIt(name, fn) { }) } +function fasyncIt(name, fn) { + fit(name, () => { + waitsForPromise(fn) + }) +} + function xasyncIt(name, fn) { xit(name, () => { waitsForPromise(fn) From 9da71cf7c0f23b2dc27590096665b5885b3f1c6f Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 9 Nov 2015 12:14:50 -0500 Subject: [PATCH 075/502] Wait for the first refresh to complete. --- spec/git-repository-async-spec.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 2470d24d3..8b02c86c6 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -302,8 +302,15 @@ fdescribe('GitRepositoryAsync-js', () => { describe('buffer events', () => { beforeEach(() => { - // This is sync, should be fine in a beforeEach atom.project.setPaths([copyRepository()]) + + // When the path is added to the project, the repository is refreshed. We + // need to wait for that to complete before the tests continue so that + // we're in a known state. + let repository = atom.project.getRepositories()[0].async + let statusHandler = jasmine.createSpy('statusHandler') + repository.onDidChangeStatuses(statusHandler) + waitsFor(() => statusHandler.callCount > 0) }) asyncIt('emits a status-changed event when a buffer is saved', async () => { From 51b15a6c00414da450099252bf61cf8cc3da6544 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 9 Nov 2015 12:42:12 -0500 Subject: [PATCH 076/502] Cleanup --- spec/git-repository-async-spec.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 8b02c86c6..106d02540 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -4,12 +4,11 @@ const fs = require('fs-plus') const path = require('path') const temp = require('temp') const Git = require('nodegit') -const CompositeDisposable = require('event-kit').CompositeDisposable - -temp.track() const GitRepositoryAsync = require('../src/git-repository-async') +temp.track() + function openFixture(fixture) { return GitRepositoryAsync.open(path.join(__dirname, 'fixtures', 'git', fixture)) } @@ -302,7 +301,8 @@ fdescribe('GitRepositoryAsync-js', () => { describe('buffer events', () => { beforeEach(() => { - atom.project.setPaths([copyRepository()]) + const workingDirectory = copyRepository() + atom.project.setPaths([workingDirectory]) // When the path is added to the project, the repository is refreshed. We // need to wait for that to complete before the tests continue so that From b97a7fb2c395fc8a8e43ad2a6a4a02868ed3f2f6 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 9 Nov 2015 12:42:27 -0500 Subject: [PATCH 077/502] Unfocus --- spec/git-repository-async-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 106d02540..a09e4f13a 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -38,7 +38,7 @@ function xasyncIt(name, fn) { }) } -fdescribe('GitRepositoryAsync-js', () => { +describe('GitRepositoryAsync-js', () => { let repo afterEach(() => { From c11f8f77ba081de5369f90d98741afb15d4e7d4b Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 9 Nov 2015 13:41:53 -0500 Subject: [PATCH 078/502] Re-enable that test. --- spec/git-repository-async-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index a09e4f13a..e44653b02 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -377,7 +377,7 @@ describe('GitRepositoryAsync-js', () => { }) }) - xasyncIt('stops listening to the buffer when the repository is destroyed (regression)', async () => { + asyncIt('stops listening to the buffer when the repository is destroyed (regression)', async () => { let editor = await atom.workspace.open('other.txt') atom.project.getRepositories()[0].destroy() expect(() => editor.save()).not.toThrow() From f0283df7d5f850ffd7185fff1c48af22ec713269 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 9 Nov 2015 14:24:15 -0500 Subject: [PATCH 079/502] Re-add this test. --- spec/git-repository-async-spec.js | 41 +++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index e44653b02..0e94d360a 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -6,6 +6,7 @@ const temp = require('temp') const Git = require('nodegit') const GitRepositoryAsync = require('../src/git-repository-async') +const Project = require('../src/project') temp.track() @@ -384,6 +385,46 @@ describe('GitRepositoryAsync-js', () => { }) }) + describe('when a project is deserialized', () => { + let project2 + + beforeEach(() => { + atom.project.setPaths([copyRepository()]) + + let repository = atom.project.getRepositories()[0].async + let statusHandler = jasmine.createSpy('statusHandler') + repository.onDidChangeStatuses(statusHandler) + waitsFor(() => statusHandler.callCount > 0) + }) + + afterEach(() => { + if (project2) project2.destroy() + }) + + asyncIt('subscribes to all the serialized buffers in the project', async () => { + await atom.workspace.open('file.txt') + + project2 = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + project2.deserialize(atom.project.serialize(), atom.deserializers) + let buffer = project2.getBuffers()[0] + + waitsFor(() => buffer.loaded) + runs(() => { + buffer.append('changes') + + let statusHandler = jasmine.createSpy('statusHandler') + project2.getRepositories()[0].async.onDidChangeStatus(statusHandler) + buffer.save() + + waitsFor(() => statusHandler.callCount > 0) + runs(() => { + expect(statusHandler.callCount).toBe(1) + expect(statusHandler).toHaveBeenCalledWith({path: buffer.getPath(), pathStatus: 256}) + }) + }) + }) + }) + xdescribe('GitRepositoryAsync::relativize(filePath)') }) From 0b378b0f87ca75d5e23ee82acfd97662e6d9514f Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 9 Nov 2015 21:11:29 -0500 Subject: [PATCH 080/502] Use node-pre-gyp to grab binaries of nodegit. --- package.json | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 33f0fcc8b..e688fd686 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "key-path-helpers": "^0.4.0", "less-cache": "0.22", "marked": "^0.3.4", + "node-pre-gyp": "^0.6.15", "nodegit": "~0.5.0", "normalize-package-data": "^2.0.0", "nslog": "^3", @@ -158,18 +159,18 @@ "standard": { "ignore": [], "parser": "babel-eslint", - "globals": [ - "atom", - "afterEach", - "beforeEach", - "describe", - "expect", - "it", - "jasmine", - "runs", - "spyOn", - "waitsFor", - "waitsForPromise" - ] + "globals": [ + "atom", + "afterEach", + "beforeEach", + "describe", + "expect", + "it", + "jasmine", + "runs", + "spyOn", + "waitsFor", + "waitsForPromise" + ] } } From 628e810758765836acc78922c1c72c6cba77ab83 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 10 Nov 2015 15:23:36 -0500 Subject: [PATCH 081/502] Expose the promise for refreshStatus. --- src/git-repository.coffee | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/src/git-repository.coffee b/src/git-repository.coffee index 46d379f98..9ff0771b3 100644 --- a/src/git-repository.coffee +++ b/src/git-repository.coffee @@ -470,24 +470,31 @@ class GitRepository # Refreshes the current git status in an outside process and asynchronously # updates the relevant properties. + # + # Returns a promise that resolves when the repository has been refreshed. refreshStatus: -> - @async.refreshStatus() - @handlerPath ?= require.resolve('./repository-status-handler') + asyncRefresh = @async.refreshStatus() + syncRefresh = new Promise (resolve, reject) => + @handlerPath ?= require.resolve('./repository-status-handler') - @statusTask?.terminate() - @statusTask = Task.once @handlerPath, @getPath(), ({statuses, upstream, branch, submodules}) => - statusesUnchanged = _.isEqual(statuses, @statuses) and - _.isEqual(upstream, @upstream) and - _.isEqual(branch, @branch) and - _.isEqual(submodules, @submodules) + @statusTask?.terminate() + @statusTask = Task.once @handlerPath, @getPath(), ({statuses, upstream, branch, submodules}) => + statusesUnchanged = _.isEqual(statuses, @statuses) and + _.isEqual(upstream, @upstream) and + _.isEqual(branch, @branch) and + _.isEqual(submodules, @submodules) - @statuses = statuses - @upstream = upstream - @branch = branch - @submodules = submodules + @statuses = statuses + @upstream = upstream + @branch = branch + @submodules = submodules - for submodulePath, submoduleRepo of @getRepo().submodules - submoduleRepo.upstream = submodules[submodulePath]?.upstream ? {ahead: 0, behind: 0} + for submodulePath, submoduleRepo of @getRepo().submodules + submoduleRepo.upstream = submodules[submodulePath]?.upstream ? {ahead: 0, behind: 0} - unless statusesUnchanged - @emitter.emit 'did-change-statuses' + resolve() + + unless statusesUnchanged + @emitter.emit 'did-change-statuses' + + return Promise.all([asyncRefresh, syncRefresh]) From 55cc08215dd08ba053824fa4e68b5f759b3239fb Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 10 Nov 2015 15:23:47 -0500 Subject: [PATCH 082/502] Be even less racy. --- spec/git-repository-async-spec.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 0e94d360a..38ee1b8bc 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -307,11 +307,11 @@ describe('GitRepositoryAsync-js', () => { // When the path is added to the project, the repository is refreshed. We // need to wait for that to complete before the tests continue so that - // we're in a known state. - let repository = atom.project.getRepositories()[0].async - let statusHandler = jasmine.createSpy('statusHandler') - repository.onDidChangeStatuses(statusHandler) - waitsFor(() => statusHandler.callCount > 0) + // we're in a known state. *But* it's really hard to observe that from the + // outside in a non-racy fashion. So let's refresh again and wait for it + // to complete before we continue. + let repository = atom.project.getRepositories()[0] + waitsForPromise(() => repository.refreshStatus()) }) asyncIt('emits a status-changed event when a buffer is saved', async () => { @@ -391,10 +391,10 @@ describe('GitRepositoryAsync-js', () => { beforeEach(() => { atom.project.setPaths([copyRepository()]) - let repository = atom.project.getRepositories()[0].async - let statusHandler = jasmine.createSpy('statusHandler') - repository.onDidChangeStatuses(statusHandler) - waitsFor(() => statusHandler.callCount > 0) + // See the comment in the 'buffer events' beforeEach for why we need to do + // this. + let repository = atom.project.getRepositories()[0] + waitsForPromise(() => repository.refreshStatus()) }) afterEach(() => { From 7421568134561b4750666225cd5271fc947f353f Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Wed, 11 Nov 2015 11:08:48 +0100 Subject: [PATCH 083/502] Add simple ::relativize() - remove spec - Remove GitUtils from async repo - Doesn't cover all the cases as the one in git-utils but I think they might have been specific to that library's implementation --- spec/git-repository-async-spec.js | 35 +++++++++++++++-- src/git-repository-async.js | 62 ++++++++++++++++++++++++++----- 2 files changed, 83 insertions(+), 14 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 0e94d360a..42c071e44 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -294,9 +294,9 @@ describe('GitRepositoryAsync-js', () => { fs.writeFileSync(modifiedPath, 'making this path modified') await repo.refreshStatus() - expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined() - expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy() - expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy() + expect(await repo.getCachedPathStatus(cleanPath)).toBeUndefined() + expect(repo.isStatusNew(await repo.getCachedPathStatus(newPath))).toBeTruthy() + expect(repo.isStatusModified(await repo.getCachedPathStatus(modifiedPath))).toBeTruthy() }) }) @@ -425,6 +425,33 @@ describe('GitRepositoryAsync-js', () => { }) }) - xdescribe('GitRepositoryAsync::relativize(filePath)') + describe('GitRepositoryAsync::relativize(filePath, workdir)', () => { + let repository + + beforeEach(() => { + atom.project.setPaths([copyRepository()]) + repository = atom.project.getRepositories()[0].async + }) + + // This is a change in implementation from the git-utils version + it('just returns path if workdir is not provided', () => { + let _path = '/foo/bar/baz.txt' + let relPath = repository.relativize(_path) + expect(_path).toEqual(relPath) + }) + + it('relativizes a repo path', () => { + let workdir = '/tmp/foo/bar/baz/' + let relativizedPath = repository.relativize(`${workdir}a/b.txt`, workdir) + expect(relativizedPath).toBe('a/b.txt') + }) + + it("doesn't require workdir to end in a slash", () => { + let workdir = '/tmp/foo/bar/baz' + let relativizedPath = repository.relativize(`${workdir}/a/b.txt`, workdir) + expect(relativizedPath).toBe('a/b.txt') + }) + + }) }) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 48fedc888..fa5518d85 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -1,5 +1,6 @@ 'use babel' +const fs = require('fs-plus') const Git = require('nodegit') const path = require('path') const {Emitter, CompositeDisposable} = require('event-kit') @@ -9,11 +10,6 @@ const newStatusFlags = Git.Status.STATUS.WT_NEW | Git.Status.STATUS.INDEX_NEW const deletedStatusFlags = Git.Status.STATUS.WT_DELETED | Git.Status.STATUS.INDEX_DELETED const indexStatusFlags = Git.Status.STATUS.INDEX_NEW | Git.Status.STATUS.INDEX_MODIFIED | Git.Status.STATUS.INDEX_DELETED | Git.Status.STATUS.INDEX_RENAMED | Git.Status.STATUS.INDEX_TYPECHANGE -// Temporary requires -// ================== -// GitUtils is temporarily used for ::relativize only, because I don't want -// to port it just yet. TODO: remove -const GitUtils = require('git-utils') // Just using this for _.isEqual and _.object, we should impl our own here const _ = require('underscore-plus') @@ -32,8 +28,8 @@ module.exports = class GitRepositoryAsync { this.emitter = new Emitter() this.subscriptions = new CompositeDisposable() this.pathStatusCache = {} - this._gitUtilsRepo = GitUtils.open(path) // TODO remove after porting ::relativize this.repoPromise = Git.Repository.open(path) + this.isCaseInsensitive = fs.isCaseInsensitive() let {project, refreshOnWindowFocus} = options this.project = project @@ -96,7 +92,7 @@ module.exports = class GitRepositoryAsync { checkoutHead (_path) { return this.repoPromise.then((repo) => { let checkoutOptions = new Git.CheckoutOptions() - checkoutOptions.paths = [this._gitUtilsRepo.relativize(_path)] + checkoutOptions.paths = [this.relativize(_path, repo.workdir())] checkoutOptions.checkoutStrategy = Git.Checkout.STRATEGY.FORCE | Git.Checkout.STRATEGY.DISABLE_PATHSPEC_MATCH return Git.Checkout.head(repo, checkoutOptions) }).then(() => { @@ -123,8 +119,9 @@ module.exports = class GitRepositoryAsync { // Returns a Promise that resolves to the status bit of a given path if it has // one, otherwise 'current'. getPathStatus (_path) { - let relativePath = this._gitUtilsRepo.relativize(_path) + let relativePath return this.repoPromise.then((repo) => { + relativePath = this.relativize(_path, repo.workdir()) return this._filterStatusesByPath(_path) }).then((statuses) => { let cachedStatus = this.pathStatusCache[relativePath] || 0 @@ -145,9 +142,10 @@ module.exports = class GitRepositoryAsync { // {::isStatusModified} or {::isStatusNew} to get more information. getDirectoryStatus (directoryPath) { - let relativePath = this._gitUtilsRepo.relativize(directoryPath) + let relativePath // XXX _filterSBD already gets repoPromise return this.repoPromise.then((repo) => { + relativePath = this.relativize(directoryPath, repo.workdir()) return this._filterStatusesByDirectory(relativePath) }).then((statuses) => { return Promise.all(statuses.map(function (s) { return s.statusBit() })).then(function (bits) { @@ -215,8 +213,52 @@ module.exports = class GitRepositoryAsync { return } + relativize (_path, workingDirectory) { + // Cargo-culted from git-utils. The original implementation also handles + // this.openedWorkingDirectory, which is set by git-utils when the + // repository is opened. Those branches of the if tree aren't included here + // yet, but if we determine we still need that here it should be simple to + // port. + // + // The original implementation also handled null workingDirectory as it + // pulled it from a sync function that could return null. We require it + // to be passed here. + if (!_path || !workingDirectory) { + return _path + } + + if (process.platform === 'win32') { + _path = _path.replace(/\\/g, '/') + } else { + if (_path[0] !== '/') { + return _path + } + } + + if (!/\/$/.test(workingDirectory)) { + workingDirectory = `${workingDirectory}/` + } + + if (this.isCaseInsensitive) { + let lowerCasePath = _path.toLowerCase() + + workingDirectory = workingDirectory.toLowerCase() + if (lowerCasePath.indexOf(workingDirectory) === 0) { + return _path.substring(workingDirectory.length) + } else { + if (lowerCasePath === workingDirectory) { + return '' + } + } + } + + return _path + } + getCachedPathStatus (_path) { - return this.pathStatusCache[this._gitUtilsRepo.relativize(_path)] + return this.repoPromise.then((repo) => { + return this.pathStatusCache[this.relativize(_path, repo.workdir())] + }) } isStatusNew (statusBit) { From 55a1d3b75f530ea3217690314e5195744546d5a3 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Wed, 11 Nov 2015 16:06:18 +0100 Subject: [PATCH 084/502] Use async repo, add missing return --- spec/git-repository-async-spec.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 5b1050555..affbfc435 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -312,6 +312,8 @@ describe('GitRepositoryAsync-js', () => { // to complete before we continue. let repository = atom.project.getRepositories()[0] waitsForPromise(() => repository.refreshStatus()) + let repository = atom.project.getRepositories()[0].async + waitsForPromise(() => { return repository.refreshStatus }) }) asyncIt('emits a status-changed event when a buffer is saved', async () => { From 861d7550b58d4ee06cb407748fc281508566397a Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Wed, 11 Nov 2015 16:09:57 +0100 Subject: [PATCH 085/502] i am bad at using GH desktop --- spec/git-repository-async-spec.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index affbfc435..cb8157fa9 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -310,8 +310,6 @@ describe('GitRepositoryAsync-js', () => { // we're in a known state. *But* it's really hard to observe that from the // outside in a non-racy fashion. So let's refresh again and wait for it // to complete before we continue. - let repository = atom.project.getRepositories()[0] - waitsForPromise(() => repository.refreshStatus()) let repository = atom.project.getRepositories()[0].async waitsForPromise(() => { return repository.refreshStatus }) }) From 2246cca63119f59c5d460f6c269c1a7558b35de6 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Wed, 11 Nov 2015 16:11:17 +0100 Subject: [PATCH 086/502] Fix typo --- spec/git-repository-async-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index cb8157fa9..7ebcd8210 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -311,7 +311,7 @@ describe('GitRepositoryAsync-js', () => { // outside in a non-racy fashion. So let's refresh again and wait for it // to complete before we continue. let repository = atom.project.getRepositories()[0].async - waitsForPromise(() => { return repository.refreshStatus }) + waitsForPromise(() => { return repository.refreshStatus() }) }) asyncIt('emits a status-changed event when a buffer is saved', async () => { From e7fe7f92e085fb1f77d7ba05fd32ad1fbd3c26f0 Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 11 Nov 2015 11:20:25 -0500 Subject: [PATCH 087/502] We don't need node-pre-gyp as a dependency. --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 3f346b691..62139af29 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,6 @@ "key-path-helpers": "^0.4.0", "less-cache": "0.22", "marked": "^0.3.4", - "node-pre-gyp": "^0.6.15", "nodegit": "~0.5.0", "normalize-package-data": "^2.0.0", "nslog": "^3", From 9e2a65a294bb580d30aa103d2782bc2628b265e3 Mon Sep 17 00:00:00 2001 From: Daniel Hengeveld Date: Wed, 11 Nov 2015 18:33:56 +0100 Subject: [PATCH 088/502] Add cache config from @joefitzgerald's branch. If this doesn't speed us up enough we'll cache node_modules as well :godmode: --- .travis.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.travis.yml b/.travis.yml index bf1bd330e..eabdd483d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,6 +32,12 @@ install: script: script/cibuild +cache: + directories: + - $HOME/.atom/.apm + - $HOME/.atom/.node-gyp/.atom + - $HOME/.atom/.npm + notifications: email: on_success: never From a8c297e7f2a73c128c157020c7b180d0af608107 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 17 Nov 2015 15:32:35 -0800 Subject: [PATCH 089/502] Don't need these, we'll use the JS one. --- spec/git-repository-async-spec.coffee | 460 -------------------------- 1 file changed, 460 deletions(-) delete mode 100644 spec/git-repository-async-spec.coffee diff --git a/spec/git-repository-async-spec.coffee b/spec/git-repository-async-spec.coffee deleted file mode 100644 index c6402d1fb..000000000 --- a/spec/git-repository-async-spec.coffee +++ /dev/null @@ -1,460 +0,0 @@ -temp = require 'temp' -GitRepositoryAsync = require '../src/git-repository-async' -Git = require 'nodegit' -fs = require 'fs-plus' -os = require 'os' -path = require 'path' -Task = require '../src/task' -Project = require '../src/project' - -# Clean up when the process exits -temp.track() - -copyRepository = -> - workingDirPath = temp.mkdirSync('atom-working-dir') - fs.copySync(path.join(__dirname, 'fixtures', 'git', 'working-dir'), workingDirPath) - fs.renameSync(path.join(workingDirPath, 'git.git'), path.join(workingDirPath, '.git')) - fs.realpathSync(workingDirPath) - -openFixture = (fixture) -> - GitRepositoryAsync.open(path.join(__dirname, 'fixtures', 'git', fixture)) - -describe "GitRepositoryAsync", -> - repo = null - - # beforeEach -> - # gitPath = path.join(temp.dir, '.git') - # fs.removeSync(gitPath) if fs.isDirectorySync(gitPath) - # - # afterEach -> - # repo.destroy() if repo?.repo? - - describe "@open(path)", -> - - # This just exercises the framework, but I'm trying to match the sync specs to start - it "repo is null when no repository is found", -> - repo = GitRepositoryAsync.open(path.join(temp.dir, 'nogit.txt')) - - waitsForPromise {shouldReject: true}, -> - repo.repoPromise - - runs -> - expect(repo.repo).toBe null - - describe ".getPath()", -> - # XXX HEAD isn't a git directory.. what's this spec supposed to be about? - xit "returns the repository path for a .git directory path", -> - # Rejects as malformed - repo = GitRepositoryAsync.open(path.join(__dirname, 'fixtures', 'git', 'master.git', 'HEAD')) - - onSuccess = jasmine.createSpy('onSuccess') - - waitsForPromise -> - repo.getPath().then(onSuccess) - - runs -> - expectedPath = path.join(__dirname, 'fixtures', 'git', 'master.git') - expect(onSuccess.mostRecentCall.args[0]).toBe(expectedPath) - - it "returns the repository path for a repository path", -> - repo = openFixture('master.git') - - onSuccess = jasmine.createSpy('onSuccess') - - waitsForPromise -> - repo.getPath().then(onSuccess) - - runs -> - expectedPath = path.join(__dirname, 'fixtures', 'git', 'master.git') - expect(onSuccess.mostRecentCall.args[0]).toBe(expectedPath) - - describe ".isPathIgnored(path)", -> - it "resolves true for an ignored path", -> - repo = openFixture('ignore.git') - onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise -> - repo.isPathIgnored('a.txt').then(onSuccess).catch (e) -> console.log e - - runs -> - expect(onSuccess.mostRecentCall.args[0]).toBeTruthy() - - it "resolves false for a non-ignored path", -> - repo = openFixture('ignore.git') - onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise -> - repo.isPathIgnored('b.txt').then(onSuccess) - runs -> - expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() - - - describe ".isPathModified(path)", -> - [repo, filePath, newPath, emptyPath] = [] - - beforeEach -> - workingDirPath = copyRepository() - repo = GitRepositoryAsync.open(workingDirPath) - filePath = path.join(workingDirPath, 'a.txt') - newPath = path.join(workingDirPath, 'new-path.txt') - fs.writeFileSync(newPath, "i'm new here") - emptyPath = path.join(workingDirPath, 'empty-path.txt') - - describe "when the path is unstaged", -> - it "resolves false if the path has not been modified", -> - onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise -> - repo.isPathModified(filePath).then(onSuccess) - runs -> - expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() - - it "resolves true if the path is modified", -> - fs.writeFileSync(filePath, "change") - onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise -> - repo.isPathModified(filePath).then(onSuccess) - runs -> - expect(onSuccess.mostRecentCall.args[0]).toBeTruthy() - - it "resolves false if the path is new", -> - onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise -> - repo.isPathModified(newPath).then(onSuccess) - runs -> - expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() - - it "resolves false if the path is invalid", -> - onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise -> - repo.isPathModified(emptyPath).then(onSuccess) - runs -> - expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() - - describe ".isPathNew(path)", -> - [filePath, newPath] = [] - - beforeEach -> - workingDirPath = copyRepository() - repo = GitRepositoryAsync.open(workingDirPath) - filePath = path.join(workingDirPath, 'a.txt') - newPath = path.join(workingDirPath, 'new-path.txt') - fs.writeFileSync(newPath, "i'm new here") - - describe "when the path is unstaged", -> - it "returns true if the path is new", -> - onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise -> - repo.isPathNew(newPath).then(onSuccess) - runs -> - expect(onSuccess.mostRecentCall.args[0]).toBeTruthy() - - it "returns false if the path isn't new", -> - onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise -> - repo.isPathModified(newPath).then(onSuccess) - runs -> - expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() - - - describe ".checkoutHead(path)", -> - [filePath] = [] - - beforeEach -> - workingDirPath = copyRepository() - repo = GitRepositoryAsync.open(workingDirPath) - filePath = path.join(workingDirPath, 'a.txt') - - it "no longer reports a path as modified after checkout", -> - onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise -> - repo.isPathModified(filePath).then(onSuccess) - runs -> - expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() - fs.writeFileSync(filePath, 'ch ch changes') - - onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise -> - repo.isPathModified(filePath).then(onSuccess) - runs -> - expect(onSuccess.mostRecentCall.args[0]).toBeTruthy() - - # Don't need to assert that this succeded because waitsForPromise should - # fail if it was rejected.. - waitsForPromise -> - repo.checkoutHead(filePath) - runs -> - onSuccess = jasmine.createSpy('onSuccess') - - waitsForPromise -> - repo.isPathModified(filePath).then(onSuccess) - runs -> - expect(onSuccess.mostRecentCall.args[0]).toBeFalsy() - - it "restores the contents of the path to the original text", -> - fs.writeFileSync(filePath, 'ch ch changes') - waitsForPromise -> - repo.checkoutHead(filePath) - runs -> - expect(fs.readFileSync(filePath, 'utf8')).toBe '' - - it "fires a did-change-status event if the checkout completes successfully", -> - fs.writeFileSync(filePath, 'ch ch changes') - statusHandler = jasmine.createSpy('statusHandler') - - waitsForPromise -> - repo.getPathStatus(filePath) - runs -> - repo.onDidChangeStatus statusHandler - - waitsForPromise -> - repo.checkoutHead(filePath) - runs -> - expect(statusHandler.callCount).toBe 1 - expect(statusHandler.argsForCall[0][0]).toEqual {path: filePath, pathStatus: 0} - - waitsForPromise -> - repo.checkoutHead(filePath) - runs -> - expect(statusHandler.callCount).toBe 1 - - xdescribe ".checkoutHeadForEditor(editor)", -> - [filePath, editor] = [] - - beforeEach -> - workingDirPath = copyRepository() - repo = repo = GitRepositoryAsync.open(workingDirPath) - filePath = path.join(workingDirPath, 'a.txt') - fs.writeFileSync(filePath, 'ch ch changes') - - waitsForPromise -> - atom.workspace.open(filePath) - - runs -> - editor = atom.workspace.getActiveTextEditor() - - xit "displays a confirmation dialog by default", -> - spyOn(atom, 'confirm').andCallFake ({buttons}) -> buttons.OK() - atom.config.set('editor.confirmCheckoutHeadRevision', true) - - waitsForPromise -> - repo.checkoutHeadForEditor(editor) - runs -> - expect(fs.readFileSync(filePath, 'utf8')).toBe '' - - xit "does not display a dialog when confirmation is disabled", -> - spyOn(atom, 'confirm') - atom.config.set('editor.confirmCheckoutHeadRevision', false) - - waitsForPromise -> - repo.checkoutHeadForEditor(editor) - runs -> - expect(fs.readFileSync(filePath, 'utf8')).toBe '' - expect(atom.confirm).not.toHaveBeenCalled() - - xdescribe ".destroy()", -> - it "throws an exception when any method is called after it is called", -> - repo = new GitRepository(require.resolve('./fixtures/git/master.git/HEAD')) - repo.destroy() - expect(-> repo.getShortHead()).toThrow() - - describe ".getPathStatus(path)", -> - [filePath] = [] - - beforeEach -> - workingDirectory = copyRepository() - repo = GitRepositoryAsync.open(workingDirectory) - filePath = path.join(workingDirectory, 'file.txt') - - it "trigger a status-changed event when the new status differs from the last cached one", -> - statusHandler = jasmine.createSpy("statusHandler") - repo.onDidChangeStatus statusHandler - fs.writeFileSync(filePath, '') - - waitsForPromise -> - repo.getPathStatus(filePath) - - runs -> - expect(statusHandler.callCount).toBe 1 - status = Git.Status.STATUS.WT_MODIFIED - expect(statusHandler.argsForCall[0][0]).toEqual {path: filePath, pathStatus: status} - fs.writeFileSync(filePath, 'abc') - - waitsForPromise -> - status = repo.getPathStatus(filePath) - - runs -> - expect(statusHandler.callCount).toBe 1 - - describe ".getDirectoryStatus(path)", -> - [directoryPath, filePath] = [] - - beforeEach -> - workingDirectory = copyRepository() - repo = GitRepositoryAsync.open(workingDirectory) - directoryPath = path.join(workingDirectory, 'dir') - filePath = path.join(directoryPath, 'b.txt') - - it "gets the status based on the files inside the directory", -> - onSuccess = jasmine.createSpy('onSuccess') - onSuccess2 = jasmine.createSpy('onSuccess2') - - waitsForPromise -> - repo.getDirectoryStatus(directoryPath).then(onSuccess) - - runs -> - expect(onSuccess.callCount).toBe 1 - expect(repo.isStatusModified(onSuccess.mostRecentCall)).toBe false - fs.writeFileSync(filePath, 'abc') - - waitsForPromise -> - repo.getDirectoryStatus(directoryPath).then(onSuccess2) - runs -> - expect(onSuccess2.callCount).toBe 1 - expect(repo.isStatusModified(onSuccess2.argsForCall[0][0])).toBe true - - - describe ".refreshStatus()", -> - [newPath, modifiedPath, cleanPath, originalModifiedPathText] = [] - - beforeEach -> - workingDirectory = copyRepository() - repo = GitRepositoryAsync.open(workingDirectory) - modifiedPath = path.join(workingDirectory, 'file.txt') - newPath = path.join(workingDirectory, 'untracked.txt') - cleanPath = path.join(workingDirectory, 'other.txt') - fs.writeFileSync(cleanPath, 'Full of text') - fs.writeFileSync(newPath, '') - newPath = fs.absolute newPath # specs could be running under symbol path. - - it "returns status information for all new and modified files", -> - fs.writeFileSync(modifiedPath, 'making this path modified') - statusHandler = jasmine.createSpy('statusHandler') - onSuccess = jasmine.createSpy('onSuccess') - repo.onDidChangeStatuses statusHandler - waitsForPromise -> - repo.refreshStatus().then(onSuccess) - - runs -> - # Callers will use the promise returned by refreshStatus, not the - # cache directly - expect(onSuccess.mostRecentCall.args[0]).toEqual(repo.pathStatusCache) - expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined() - expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy() - expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy() - - # This tests the async implementation's events directly, but ultimately I - # think we want users to just be able to subscribe to events on GitRepository - # and have them bubble up from async-land - - describe "buffer events", -> - [editor] = [] - - beforeEach -> - atom.project.setPaths([copyRepository()]) - - it "emits a status-changed event when a buffer is saved", -> - waitsForPromise -> - atom.workspace.open('other.txt').then (o) -> - editor = o - runs -> - editor.insertNewline() - statusHandler = jasmine.createSpy('statusHandler') - repo = atom.project.getRepositories()[0] - repo.async.onDidChangeStatus statusHandler - editor.save() - - waitsFor -> - statusHandler.callCount >= 1 - runs -> - expect(statusHandler.callCount).toBe 1 - expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} - - it "emits a status-changed event when a buffer is reloaded", -> - waitsForPromise -> - atom.workspace.open('other.txt').then (o) -> - editor = o - runs -> - fs.writeFileSync(editor.getPath(), 'changed') - - statusHandler = jasmine.createSpy('statusHandler') - atom.project.getRepositories()[0].async.onDidChangeStatus statusHandler - editor.getBuffer().reload() - reloadHandler = jasmine.createSpy 'reloadHandler' - - waitsFor -> - statusHandler.callCount >= 1 - runs -> - expect(statusHandler.callCount).toBe 1 - expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} - buffer = editor.getBuffer() - buffer.onDidReload(reloadHandler) - buffer.reload() - - waitsFor -> - reloadHandler.callCount >= 1 - runs -> - expect(statusHandler.callCount).toBe 1 - - it "emits a status-changed event when a buffer's path changes", -> - waitsForPromise -> - atom.workspace.open('other.txt').then (o) -> - editor = o - runs -> - fs.writeFileSync(editor.getPath(), 'changed') - - statusHandler = jasmine.createSpy('statusHandler') - atom.project.getRepositories()[0].async.onDidChangeStatus statusHandler - editor.getBuffer().emitter.emit 'did-change-path' - waitsFor -> - statusHandler.callCount >= 1 - runs -> - expect(statusHandler.callCount).toBe 1 - expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} - - pathHandler = jasmine.createSpy('pathHandler') - buffer = editor.getBuffer() - buffer.onDidChangePath pathHandler - buffer.emitter.emit 'did-change-path' - waitsFor -> - pathHandler.callCount >= 1 - runs -> - expect(statusHandler.callCount).toBe 1 - - it "stops listening to the buffer when the repository is destroyed (regression)", -> - waitsForPromise -> - atom.workspace.open('other.txt').then (o) -> - editor = o - runs -> - atom.project.getRepositories()[0].destroy() - expect(-> editor.save()).not.toThrow() - - describe "when a project is deserialized", -> - [buffer, project2] = [] - - afterEach -> - project2?.destroy() - - it "subscribes to all the serialized buffers in the project", -> - atom.project.setPaths([copyRepository()]) - - waitsForPromise -> - atom.workspace.open('file.txt') - - runs -> - project2 = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) - project2.deserialize(atom.project.serialize(), atom.deserializers) - buffer = project2.getBuffers()[0] - - waitsFor -> - buffer.loaded - - runs -> - originalContent = buffer.getText() - buffer.append('changes') - - statusHandler = jasmine.createSpy('statusHandler') - project2.getRepositories()[0].async.onDidChangeStatus statusHandler - buffer.save() - waitsFor -> - statusHandler.callCount >= 1 - runs -> - expect(statusHandler.callCount).toBe 1 - expect(statusHandler).toHaveBeenCalledWith {path: buffer.getPath(), pathStatus: 256} From d54e3f2d3c0b516be53b8151a6b1441ad12d6538 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 17 Nov 2015 15:37:58 -0800 Subject: [PATCH 090/502] Implement the Windows focus refreshing. --- src/git-repository-async.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index fa5518d85..d0c7c2323 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -3,7 +3,7 @@ const fs = require('fs-plus') const Git = require('nodegit') const path = require('path') -const {Emitter, CompositeDisposable} = require('event-kit') +const {Emitter, CompositeDisposable, Disposable} = require('event-kit') const modifiedStatusFlags = Git.Status.STATUS.WT_MODIFIED | Git.Status.STATUS.INDEX_MODIFIED | Git.Status.STATUS.WT_DELETED | Git.Status.STATUS.INDEX_DELETED | Git.Status.STATUS.WT_TYPECHANGE | Git.Status.STATUS.INDEX_TYPECHANGE const newStatusFlags = Git.Status.STATUS.WT_NEW | Git.Status.STATUS.INDEX_NEW @@ -37,7 +37,11 @@ module.exports = class GitRepositoryAsync { refreshOnWindowFocus = true } if (refreshOnWindowFocus) { - // TODO + const onWindowFocus = () => { + this.refreshStatus() + } + window.addEventListener('focus', onWindowFocus) + this.subscriptions.add(new Disposable(() => window.removeEventListener('focus', onWindowFocus))) } if (this.project) { From 8c47335db6f913a8493a1bdf852b86d2702cdf6b Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 17 Nov 2015 15:38:10 -0800 Subject: [PATCH 091/502] ES6 harder. --- src/git-repository-async.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index d0c7c2323..1969a9c6b 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -343,17 +343,17 @@ module.exports = class GitRepositoryAsync { // Returns a {Promise} that resolves true if the given path is a submodule in // the repository. isSubmodule (_path) { - return this.repoPromise.then(function (repo) { - return repo.openIndex() - }).then(function (index) { - let entry = index.getByPath(_path) - let submoduleMode = 57344 // TODO compose this from libgit2 constants + return this.repoPromise + .then(repo => repo.openIndex()) + .then(index => { + let entry = index.getByPath(_path) + let submoduleMode = 57344 // TODO compose this from libgit2 constants - if (entry.mode === submoduleMode) { - return true - } else { - return false - } - }) + if (entry.mode === submoduleMode) { + return true + } else { + return false + } + }) } } From e392e5dfccbba59e693f4f63bd4ed28de64cc037 Mon Sep 17 00:00:00 2001 From: Dave Rael Date: Tue, 24 Nov 2015 09:34:53 -0700 Subject: [PATCH 092/502] :checkered_flag: Make --wait work on Windows including using Atom as the git commit editor --- resources/win/atom.cmd | 34 +++++++++++++++++++++++++++--- resources/win/atom.sh | 33 +++++++++++++++++++++++++---- src/browser/squirrel-update.coffee | 2 +- 3 files changed, 61 insertions(+), 8 deletions(-) diff --git a/resources/win/atom.cmd b/resources/win/atom.cmd index d3188b3ea..0df49dd64 100644 --- a/resources/win/atom.cmd +++ b/resources/win/atom.cmd @@ -1,6 +1,7 @@ @echo off SET EXPECT_OUTPUT= +SET WAIT= FOR %%a IN (%*) DO ( IF /I "%%a"=="-f" SET EXPECT_OUTPUT=YES @@ -11,13 +12,40 @@ FOR %%a IN (%*) DO ( IF /I "%%a"=="--test" SET EXPECT_OUTPUT=YES IF /I "%%a"=="-v" SET EXPECT_OUTPUT=YES IF /I "%%a"=="--version" SET EXPECT_OUTPUT=YES - IF /I "%%a"=="-w" SET EXPECT_OUTPUT=YES - IF /I "%%a"=="--wait" SET EXPECT_OUTPUT=YES + IF /I "%%a"=="-w" ( + SET EXPECT_OUTPUT=YES + SET WAIT=YES + ) + IF /I "%%a"=="--wait" ( + SET EXPECT_OUTPUT=YES + SET WAIT=YES + ) ) +rem Getting the process ID in cmd of the current cmd process: http://superuser.com/questions/881789/identify-and-kill-batch-script-started-before +set T=%TEMP%\sthUnique.tmp +wmic process where (Name="WMIC.exe" AND CommandLine LIKE "%%%TIME%%%") get ParentProcessId /value | find "ParentProcessId" >%T% +set /P A=<%T% +set PID=%A:~16% + IF "%EXPECT_OUTPUT%"=="YES" ( SET ELECTRON_ENABLE_LOGGING=YES - "%~dp0\..\..\atom.exe" %* + IF "%WAIT%"=="YES" ( + "%~dp0\..\..\atom.exe" --pid=%PID% %* + rem If the wait flag is set, don't exit this process until Atom tells it to. + goto waitLoop + ) + ELSE ( + "%~dp0\..\..\atom.exe" %* + ) ) ELSE ( "%~dp0\..\app\apm\bin\node.exe" "%~dp0\atom.js" %* ) + +goto end + +:waitLoop + sleep 1 + goto waitLoop + +:end diff --git a/resources/win/atom.sh b/resources/win/atom.sh index 9e2c6da65..4fd5a3106 100644 --- a/resources/win/atom.sh +++ b/resources/win/atom.sh @@ -4,12 +4,26 @@ while getopts ":fhtvw-:" opt; do case "$opt" in -) case "${OPTARG}" in - foreground|help|test|version|wait) + wait) + WAIT=1 + ;; + help|version) + REDIRECT_STDERR=1 EXPECT_OUTPUT=1 - ;; + ;; + foreground|test) + EXPECT_OUTPUT=1 + ;; esac ;; - f|h|t|v|w) + w) + WAIT=1 + ;; + h|v) + REDIRECT_STDERR=1 + EXPECT_OUTPUT=1 + ;; + f|t) EXPECT_OUTPUT=1 ;; esac @@ -17,9 +31,20 @@ done directory=$(dirname "$0") +WINPS=`ps | grep -i $$` +PID=`echo $WINPS | cut -d' ' -f 4` +echo $PID + if [ $EXPECT_OUTPUT ]; then export ELECTRON_ENABLE_LOGGING=1 - "$directory/../../atom.exe" "$@" + "$directory/../../atom.exe" --executed-from="$(pwd)" --pid=$PID "$@" else "$directory/../app/apm/bin/node.exe" "$directory/atom.js" "$@" fi + +# If the wait flag is set, don't exit this process until Atom tells it to. +if [ $WAIT ]; then + while true; do + sleep 1 + done +fi diff --git a/src/browser/squirrel-update.coffee b/src/browser/squirrel-update.coffee index 0e4743a21..3660158fc 100644 --- a/src/browser/squirrel-update.coffee +++ b/src/browser/squirrel-update.coffee @@ -139,7 +139,7 @@ addCommandsToPath = (callback) -> atomShCommandPath = path.join(binFolder, 'atom') relativeAtomShPath = path.relative(binFolder, path.join(appFolder, 'resources', 'cli', 'atom.sh')) - atomShCommand = "#!/bin/sh\r\n\"$(dirname \"$0\")/#{relativeAtomShPath.replace(/\\/g, '/')}\" \"$@\"" + atomShCommand = "#!/bin/sh\r\n\"$(dirname \"$0\")/#{relativeAtomShPath.replace(/\\/g, '/')}\" \"$@\"\r\necho" apmCommandPath = path.join(binFolder, 'apm.cmd') relativeApmPath = path.relative(binFolder, path.join(process.resourcesPath, 'app', 'apm', 'bin', 'apm.cmd')) From ca629027ff6cb5d258d8bafbd78e09f1e54a1e12 Mon Sep 17 00:00:00 2001 From: Dave Rael Date: Tue, 24 Nov 2015 15:04:53 -0700 Subject: [PATCH 093/502] :checkered-flag: Add race condition protection for getting process id to make sure temp file isn't in conflict with another process and to make sure to get the right process id --- resources/win/atom.cmd | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/win/atom.cmd b/resources/win/atom.cmd index 0df49dd64..0463928d8 100644 --- a/resources/win/atom.cmd +++ b/resources/win/atom.cmd @@ -23,10 +23,11 @@ FOR %%a IN (%*) DO ( ) rem Getting the process ID in cmd of the current cmd process: http://superuser.com/questions/881789/identify-and-kill-batch-script-started-before -set T=%TEMP%\sthUnique.tmp +set T=%TEMP%\atomCmdProcessId-%time::=%.tmp wmic process where (Name="WMIC.exe" AND CommandLine LIKE "%%%TIME%%%") get ParentProcessId /value | find "ParentProcessId" >%T% set /P A=<%T% set PID=%A:~16% +del %T% IF "%EXPECT_OUTPUT%"=="YES" ( SET ELECTRON_ENABLE_LOGGING=YES From c8254566eff33918d433ba80171dab489ee7ad28 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 26 Nov 2015 15:34:45 +0100 Subject: [PATCH 094/502] Change verticalScrollbar's height according to block decorations --- spec/text-editor-presenter-spec.coffee | 33 ++++++++++++++++++++++++ src/text-editor-presenter.coffee | 35 +++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 8dd34fde8..6f1cf3a70 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -486,6 +486,39 @@ describe "TextEditorPresenter", -> presenter = buildPresenter(scrollTop: 0, lineHeight: 10, explicitHeight: 500) expect(presenter.getState().verticalScrollbar.scrollHeight).toBe 500 + it "updates when new block decorations are measured, changed or destroyed", -> + presenter = buildPresenter(scrollTop: 0, lineHeight: 10) + expect(presenter.getState().verticalScrollbar.scrollHeight).toBe editor.getScreenLineCount() * 10 + + addBlockDecorationAtScreenRow = (screenRow) -> + editor.decorateMarker( + editor.markScreenPosition([screenRow, 0], invalidate: "never"), + type: "block", + item: document.createElement("div") + ) + + blockDecoration1 = addBlockDecorationAtScreenRow(0) + blockDecoration2 = addBlockDecorationAtScreenRow(3) + blockDecoration3 = addBlockDecorationAtScreenRow(7) + + presenter.setBlockDecorationSize(blockDecoration1, 0, 35.8) + presenter.setBlockDecorationSize(blockDecoration2, 0, 50.3) + presenter.setBlockDecorationSize(blockDecoration3, 0, 95.2) + + linesHeight = editor.getScreenLineCount() * 10 + blockDecorationsHeight = Math.round(35.8 + 50.3 + 95.2) + expect(presenter.getState().verticalScrollbar.scrollHeight).toBe(linesHeight + blockDecorationsHeight) + + presenter.setBlockDecorationSize(blockDecoration2, 0, 100.3) + + blockDecorationsHeight = Math.round(35.8 + 100.3 + 95.2) + expect(presenter.getState().verticalScrollbar.scrollHeight).toBe(linesHeight + blockDecorationsHeight) + + waitsForStateToUpdate presenter, -> blockDecoration3.destroy() + runs -> + blockDecorationsHeight = Math.round(35.8 + 100.3) + expect(presenter.getState().verticalScrollbar.scrollHeight).toBe(linesHeight + blockDecorationsHeight) + it "updates when the ::lineHeight changes", -> presenter = buildPresenter(scrollTop: 0, lineHeight: 10) expectStateUpdate presenter, -> presenter.setLineHeight(20) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 405e34548..6296e9331 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -28,6 +28,7 @@ class TextEditorPresenter @lineDecorationsByScreenRow = {} @lineNumberDecorationsByScreenRow = {} @customGutterDecorationsByGutterName = {} + @blockDecorationsDimensions = new Map @screenRowsToMeasure = [] @transferMeasurementsToModel() @transferMeasurementsFromModel() @@ -71,6 +72,7 @@ class TextEditorPresenter getPreMeasurementState: -> @updating = true + @updateBlockDecorations() if @shouldUpdateBlockDecorations @updateVerticalDimensions() @updateScrollbarDimensions() @@ -140,6 +142,7 @@ class TextEditorPresenter @shouldUpdateHiddenInputState = false @shouldUpdateContentState = false @shouldUpdateDecorations = false + @shouldUpdateBlockDecorations = false @shouldUpdateLinesState = false @shouldUpdateTilesState = false @shouldUpdateCursorsState = false @@ -158,6 +161,7 @@ class TextEditorPresenter @shouldUpdateHiddenInputState = true @shouldUpdateContentState = true @shouldUpdateDecorations = true + @shouldUpdateBlockDecorations = true @shouldUpdateLinesState = true @shouldUpdateTilesState = true @shouldUpdateCursorsState = true @@ -188,6 +192,8 @@ class TextEditorPresenter @shouldUpdateLineNumbersState = true @shouldUpdateDecorations = true @shouldUpdateOverlaysState = true + @shouldUpdateBlockDecorations = true + @shouldUpdateVerticalScrollState = true @shouldUpdateCustomGutterDecorationState = true @emitDidUpdateState() @@ -727,10 +733,19 @@ class TextEditorPresenter @scrollHeight = scrollHeight @updateScrollTop(@scrollTop) + getLinesHeight: -> + @lineHeight * @model.getScreenLineCount() + + getBlockDecorationsHeight: -> + sizes = Array.from(@blockDecorationsDimensions.values()) + sum = (a, b) -> a + b + height = sizes.map((size) -> size.height).reduce(sum, 0) + height + updateVerticalDimensions: -> if @lineHeight? oldContentHeight = @contentHeight - @contentHeight = @lineHeight * @model.getScreenLineCount() + @contentHeight = Math.round(@getLinesHeight() + @getBlockDecorationsHeight()) if @contentHeight isnt oldContentHeight @updateHeight() @@ -1364,6 +1379,24 @@ class TextEditorPresenter @emitDidUpdateState() + setBlockDecorationSize: (decoration, width, height) -> + @blockDecorationsDimensions.set(decoration.id, {width, height}) + + @shouldUpdateBlockDecorations = true + @shouldUpdateVerticalScrollState = true + @emitDidUpdateState() + + updateBlockDecorations: -> + blockDecorations = {} + for decoration in @model.getDecorations(type: "block") + blockDecorations[decoration.id] = decoration + + @blockDecorationsDimensions.forEach (value, key) => + unless blockDecorations.hasOwnProperty(key) + @blockDecorationsDimensions.delete(key) + + @shouldUpdateVerticalScrollState = true + observeCursor: (cursor) -> didChangePositionDisposable = cursor.onDidChangePosition => @shouldUpdateHiddenInputState = true if cursor.isLastCursor() From 89d9a2ce832f8cee0f35e5a6fd8bc588163127de Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 26 Nov 2015 15:40:07 +0100 Subject: [PATCH 095/502] :art: Create block decorations in `TextEditor` This is just an experiment, although I like that we can hide some information (irrelevant to the user) behind a clean API. --- spec/text-editor-presenter-spec.coffee | 15 +++++---------- src/text-editor.coffee | 7 +++++++ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 6f1cf3a70..e3dc9feaa 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -490,16 +490,11 @@ describe "TextEditorPresenter", -> presenter = buildPresenter(scrollTop: 0, lineHeight: 10) expect(presenter.getState().verticalScrollbar.scrollHeight).toBe editor.getScreenLineCount() * 10 - addBlockDecorationAtScreenRow = (screenRow) -> - editor.decorateMarker( - editor.markScreenPosition([screenRow, 0], invalidate: "never"), - type: "block", - item: document.createElement("div") - ) - - blockDecoration1 = addBlockDecorationAtScreenRow(0) - blockDecoration2 = addBlockDecorationAtScreenRow(3) - blockDecoration3 = addBlockDecorationAtScreenRow(7) + # Setting `null` as the DOM element, as it doesn't really matter here. + # Maybe a signal that we should separate models from views? + blockDecoration1 = editor.addBlockDecorationForScreenRow(0, null) + blockDecoration2 = editor.addBlockDecorationForScreenRow(3, null) + blockDecoration3 = editor.addBlockDecorationForScreenRow(7, null) presenter.setBlockDecorationSize(blockDecoration1, 0, 35.8) presenter.setBlockDecorationSize(blockDecoration2, 0, 50.3) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index a151c9dba..f88c9149c 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -1398,6 +1398,13 @@ class TextEditor extends Model Section: Decorations ### + addBlockDecorationForScreenRow: (screenRow, element) -> + @decorateMarker( + @markScreenPosition([screenRow, 0], invalidate: "never"), + type: "block", + element: element + ) + # Essential: Add a decoration that tracks a {TextEditorMarker}. When the # marker moves, is invalidated, or is destroyed, the decoration will be # updated to reflect the marker's state. From 30da4bdb0c66f8f9ae8b50a7a75028a1370c9fe2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 26 Nov 2015 15:43:33 +0100 Subject: [PATCH 096/502] Ensure gutter scroll height takes block decorations into account --- spec/text-editor-presenter-spec.coffee | 28 ++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index e3dc9feaa..1e8b3dbb9 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -2874,6 +2874,34 @@ describe "TextEditorPresenter", -> customGutter.destroy() describe ".scrollHeight", -> + it "updates when new block decorations are measured, changed or destroyed", -> + presenter = buildPresenter(scrollTop: 0, lineHeight: 10) + expect(presenter.getState().verticalScrollbar.scrollHeight).toBe editor.getScreenLineCount() * 10 + + # Setting `null` as the DOM element, as it doesn't really matter here. + # Maybe a signal that we should separate models from views? + blockDecoration1 = editor.addBlockDecorationForScreenRow(0, null) + blockDecoration2 = editor.addBlockDecorationForScreenRow(3, null) + blockDecoration3 = editor.addBlockDecorationForScreenRow(7, null) + + presenter.setBlockDecorationSize(blockDecoration1, 0, 35.8) + presenter.setBlockDecorationSize(blockDecoration2, 0, 50.3) + presenter.setBlockDecorationSize(blockDecoration3, 0, 95.2) + + linesHeight = editor.getScreenLineCount() * 10 + blockDecorationsHeight = Math.round(35.8 + 50.3 + 95.2) + expect(getStylesForGutterWithName(presenter, 'line-number').scrollHeight).toBe(linesHeight + blockDecorationsHeight) + + presenter.setBlockDecorationSize(blockDecoration2, 0, 100.3) + + blockDecorationsHeight = Math.round(35.8 + 100.3 + 95.2) + expect(getStylesForGutterWithName(presenter, 'line-number').scrollHeight).toBe(linesHeight + blockDecorationsHeight) + + waitsForStateToUpdate presenter, -> blockDecoration3.destroy() + runs -> + blockDecorationsHeight = Math.round(35.8 + 100.3) + expect(getStylesForGutterWithName(presenter, 'line-number').scrollHeight).toBe(linesHeight + blockDecorationsHeight) + it "is initialized based on ::lineHeight, the number of lines, and ::explicitHeight", -> presenter = buildPresenter() expect(getStylesForGutterWithName(presenter, 'line-number').scrollHeight).toBe editor.getScreenLineCount() * 10 From 96863eef1cde237824bd55f618d64882ddfcd9aa Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 26 Nov 2015 15:44:59 +0100 Subject: [PATCH 097/502] Ensure content scrollHeight takes block decorations into account Seems like all these properties in the presenter's state share a common contract, which we should probably extract. It could also mean there's an underlying design problem, because we are testing two dimensions of the same behavior in a single spec. --- spec/text-editor-presenter-spec.coffee | 28 ++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 1e8b3dbb9..a5d963bd3 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -679,6 +679,34 @@ describe "TextEditorPresenter", -> expect(presenter.getState().content.maxHeight).toBe(50) describe ".scrollHeight", -> + it "updates when new block decorations are measured, changed or destroyed", -> + presenter = buildPresenter(scrollTop: 0, lineHeight: 10) + expect(presenter.getState().verticalScrollbar.scrollHeight).toBe editor.getScreenLineCount() * 10 + + # Setting `null` as the DOM element, as it doesn't really matter here. + # Maybe a signal that we should separate models from views? + blockDecoration1 = editor.addBlockDecorationForScreenRow(0, null) + blockDecoration2 = editor.addBlockDecorationForScreenRow(3, null) + blockDecoration3 = editor.addBlockDecorationForScreenRow(7, null) + + presenter.setBlockDecorationSize(blockDecoration1, 0, 35.8) + presenter.setBlockDecorationSize(blockDecoration2, 0, 50.3) + presenter.setBlockDecorationSize(blockDecoration3, 0, 95.2) + + linesHeight = editor.getScreenLineCount() * 10 + blockDecorationsHeight = Math.round(35.8 + 50.3 + 95.2) + expect(presenter.getState().content.scrollHeight).toBe(linesHeight + blockDecorationsHeight) + + presenter.setBlockDecorationSize(blockDecoration2, 0, 100.3) + + blockDecorationsHeight = Math.round(35.8 + 100.3 + 95.2) + expect(presenter.getState().content.scrollHeight).toBe(linesHeight + blockDecorationsHeight) + + waitsForStateToUpdate presenter, -> blockDecoration3.destroy() + runs -> + blockDecorationsHeight = Math.round(35.8 + 100.3) + expect(presenter.getState().content.scrollHeight).toBe(linesHeight + blockDecorationsHeight) + it "is initialized based on the lineHeight, the number of lines, and the height", -> presenter = buildPresenter(scrollTop: 0, lineHeight: 10) expect(presenter.getState().content.scrollHeight).toBe editor.getScreenLineCount() * 10 From 5ac7ffcf48c0a7c6a86fe83f0feafa018cd20e5b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 26 Nov 2015 16:09:21 +0100 Subject: [PATCH 098/502] :art: Rename to ::blockDecorationsDimensionsById --- src/text-editor-presenter.coffee | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 6296e9331..cc5327f0e 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -28,7 +28,7 @@ class TextEditorPresenter @lineDecorationsByScreenRow = {} @lineNumberDecorationsByScreenRow = {} @customGutterDecorationsByGutterName = {} - @blockDecorationsDimensions = new Map + @blockDecorationsDimensionsById = new Map @screenRowsToMeasure = [] @transferMeasurementsToModel() @transferMeasurementsFromModel() @@ -737,7 +737,7 @@ class TextEditorPresenter @lineHeight * @model.getScreenLineCount() getBlockDecorationsHeight: -> - sizes = Array.from(@blockDecorationsDimensions.values()) + sizes = Array.from(@blockDecorationsDimensionsById.values()) sum = (a, b) -> a + b height = sizes.map((size) -> size.height).reduce(sum, 0) height @@ -1380,7 +1380,7 @@ class TextEditorPresenter @emitDidUpdateState() setBlockDecorationSize: (decoration, width, height) -> - @blockDecorationsDimensions.set(decoration.id, {width, height}) + @blockDecorationsDimensionsById.set(decoration.id, {width, height}) @shouldUpdateBlockDecorations = true @shouldUpdateVerticalScrollState = true @@ -1391,9 +1391,9 @@ class TextEditorPresenter for decoration in @model.getDecorations(type: "block") blockDecorations[decoration.id] = decoration - @blockDecorationsDimensions.forEach (value, key) => + @blockDecorationsDimensionsById.forEach (value, key) => unless blockDecorations.hasOwnProperty(key) - @blockDecorationsDimensions.delete(key) + @blockDecorationsDimensionsById.delete(key) @shouldUpdateVerticalScrollState = true From 02651b7a580ad7f8e102e5523dad103b829572b3 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 26 Nov 2015 16:10:08 +0100 Subject: [PATCH 099/502] :art: Rename to ::setBlockDecorationDimensions --- spec/text-editor-presenter-spec.coffee | 24 ++++++++++++------------ src/text-editor-presenter.coffee | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index a5d963bd3..010a9fc2d 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -496,15 +496,15 @@ describe "TextEditorPresenter", -> blockDecoration2 = editor.addBlockDecorationForScreenRow(3, null) blockDecoration3 = editor.addBlockDecorationForScreenRow(7, null) - presenter.setBlockDecorationSize(blockDecoration1, 0, 35.8) - presenter.setBlockDecorationSize(blockDecoration2, 0, 50.3) - presenter.setBlockDecorationSize(blockDecoration3, 0, 95.2) + presenter.setBlockDecorationDimensions(blockDecoration1, 0, 35.8) + presenter.setBlockDecorationDimensions(blockDecoration2, 0, 50.3) + presenter.setBlockDecorationDimensions(blockDecoration3, 0, 95.2) linesHeight = editor.getScreenLineCount() * 10 blockDecorationsHeight = Math.round(35.8 + 50.3 + 95.2) expect(presenter.getState().verticalScrollbar.scrollHeight).toBe(linesHeight + blockDecorationsHeight) - presenter.setBlockDecorationSize(blockDecoration2, 0, 100.3) + presenter.setBlockDecorationDimensions(blockDecoration2, 0, 100.3) blockDecorationsHeight = Math.round(35.8 + 100.3 + 95.2) expect(presenter.getState().verticalScrollbar.scrollHeight).toBe(linesHeight + blockDecorationsHeight) @@ -689,15 +689,15 @@ describe "TextEditorPresenter", -> blockDecoration2 = editor.addBlockDecorationForScreenRow(3, null) blockDecoration3 = editor.addBlockDecorationForScreenRow(7, null) - presenter.setBlockDecorationSize(blockDecoration1, 0, 35.8) - presenter.setBlockDecorationSize(blockDecoration2, 0, 50.3) - presenter.setBlockDecorationSize(blockDecoration3, 0, 95.2) + presenter.setBlockDecorationDimensions(blockDecoration1, 0, 35.8) + presenter.setBlockDecorationDimensions(blockDecoration2, 0, 50.3) + presenter.setBlockDecorationDimensions(blockDecoration3, 0, 95.2) linesHeight = editor.getScreenLineCount() * 10 blockDecorationsHeight = Math.round(35.8 + 50.3 + 95.2) expect(presenter.getState().content.scrollHeight).toBe(linesHeight + blockDecorationsHeight) - presenter.setBlockDecorationSize(blockDecoration2, 0, 100.3) + presenter.setBlockDecorationDimensions(blockDecoration2, 0, 100.3) blockDecorationsHeight = Math.round(35.8 + 100.3 + 95.2) expect(presenter.getState().content.scrollHeight).toBe(linesHeight + blockDecorationsHeight) @@ -2912,15 +2912,15 @@ describe "TextEditorPresenter", -> blockDecoration2 = editor.addBlockDecorationForScreenRow(3, null) blockDecoration3 = editor.addBlockDecorationForScreenRow(7, null) - presenter.setBlockDecorationSize(blockDecoration1, 0, 35.8) - presenter.setBlockDecorationSize(blockDecoration2, 0, 50.3) - presenter.setBlockDecorationSize(blockDecoration3, 0, 95.2) + presenter.setBlockDecorationDimensions(blockDecoration1, 0, 35.8) + presenter.setBlockDecorationDimensions(blockDecoration2, 0, 50.3) + presenter.setBlockDecorationDimensions(blockDecoration3, 0, 95.2) linesHeight = editor.getScreenLineCount() * 10 blockDecorationsHeight = Math.round(35.8 + 50.3 + 95.2) expect(getStylesForGutterWithName(presenter, 'line-number').scrollHeight).toBe(linesHeight + blockDecorationsHeight) - presenter.setBlockDecorationSize(blockDecoration2, 0, 100.3) + presenter.setBlockDecorationDimensions(blockDecoration2, 0, 100.3) blockDecorationsHeight = Math.round(35.8 + 100.3 + 95.2) expect(getStylesForGutterWithName(presenter, 'line-number').scrollHeight).toBe(linesHeight + blockDecorationsHeight) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index cc5327f0e..d38d3c98f 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -1379,7 +1379,7 @@ class TextEditorPresenter @emitDidUpdateState() - setBlockDecorationSize: (decoration, width, height) -> + setBlockDecorationDimensions: (decoration, width, height) -> @blockDecorationsDimensionsById.set(decoration.id, {width, height}) @shouldUpdateBlockDecorations = true From 1b5fd1863021bdc9f0c790c0fb388f0490a2921e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 27 Nov 2015 09:54:14 +0100 Subject: [PATCH 100/502] Use ::rowForPosition and ::positionForRow instead of simple math --- src/text-editor-presenter.coffee | 59 +++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 17 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index d38d3c98f..ab9def07e 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -28,7 +28,8 @@ class TextEditorPresenter @lineDecorationsByScreenRow = {} @lineNumberDecorationsByScreenRow = {} @customGutterDecorationsByGutterName = {} - @blockDecorationsDimensionsById = new Map + @blockDecorationsDimensionsById = {} + @blockDecorationsDimensionsByScreenRow = {} @screenRowsToMeasure = [] @transferMeasurementsToModel() @transferMeasurementsFromModel() @@ -426,7 +427,7 @@ class TextEditorPresenter continue if rowsWithinTile.length is 0 tile = @state.content.tiles[tileStartRow] ?= {} - tile.top = tileStartRow * @lineHeight - @scrollTop + tile.top = @positionForRow(tileStartRow) - @scrollTop tile.left = -@scrollLeft tile.height = @tileSize * @lineHeight tile.display = "block" @@ -692,19 +693,35 @@ class TextEditorPresenter return + rowForPosition: (position, floor = true) -> + top = 0 + for tileRow in [0..@model.getScreenLineCount()] by @tileSize + for row in [tileRow...Math.min(tileRow + @tileSize, @model.getScreenLineCount())] by 1 + nextTop = top + @lineHeight + if floor + return row if nextTop > position + else + return row if top >= position + top = nextTop + @model.getScreenLineCount() + + positionForRow: (targetRow) -> + top = 0 + for tileRow in [0..@model.getScreenLineCount()] by @tileSize + for row in [tileRow...Math.min(tileRow + @tileSize, @model.getScreenLineCount())] by 1 + return top if row is targetRow + top += @lineHeight + top + updateStartRow: -> return unless @scrollTop? and @lineHeight? - startRow = Math.floor(@scrollTop / @lineHeight) - @startRow = Math.max(0, startRow) + @startRow = Math.max(0, @rowForPosition(@scrollTop)) updateEndRow: -> return unless @scrollTop? and @lineHeight? and @height? - startRow = Math.max(0, Math.floor(@scrollTop / @lineHeight)) - visibleLinesCount = Math.ceil(@height / @lineHeight) + 1 - endRow = startRow + visibleLinesCount - @endRow = Math.min(@model.getScreenLineCount(), endRow) + @endRow = @rowForPosition(@scrollTop + @height + @lineHeight, false) updateRowsPerPage: -> rowsPerPage = Math.floor(@getClientHeight() / @lineHeight) @@ -736,16 +753,17 @@ class TextEditorPresenter getLinesHeight: -> @lineHeight * @model.getScreenLineCount() - getBlockDecorationsHeight: -> - sizes = Array.from(@blockDecorationsDimensionsById.values()) + getBlockDecorationsHeight: (blockDecorations) -> sum = (a, b) -> a + b - height = sizes.map((size) -> size.height).reduce(sum, 0) - height + Array.from(blockDecorations).map((size) -> size.height).reduce(sum, 0) updateVerticalDimensions: -> if @lineHeight? oldContentHeight = @contentHeight - @contentHeight = Math.round(@getLinesHeight() + @getBlockDecorationsHeight()) + allBlockDecorations = _.values(@blockDecorationsDimensionsById) + @contentHeight = Math.round( + @getLinesHeight() + @getBlockDecorationsHeight(allBlockDecorations) + ) if @contentHeight isnt oldContentHeight @updateHeight() @@ -1380,20 +1398,27 @@ class TextEditorPresenter @emitDidUpdateState() setBlockDecorationDimensions: (decoration, width, height) -> - @blockDecorationsDimensionsById.set(decoration.id, {width, height}) + screenRow = decoration.getMarker().getHeadScreenPosition().row + dimensions = {width, height} + + @blockDecorationsDimensionsByScreenRow[screenRow] ?= {} + @blockDecorationsDimensionsByScreenRow[screenRow][decoration.id] = dimensions + @blockDecorationsDimensionsById[decoration.id] = dimensions @shouldUpdateBlockDecorations = true @shouldUpdateVerticalScrollState = true @emitDidUpdateState() updateBlockDecorations: -> + # TODO: Move this inside `DisplayBuffer` blockDecorations = {} for decoration in @model.getDecorations(type: "block") blockDecorations[decoration.id] = decoration - @blockDecorationsDimensionsById.forEach (value, key) => - unless blockDecorations.hasOwnProperty(key) - @blockDecorationsDimensionsById.delete(key) + for screenRow, decorations of @blockDecorationsDimensionsByScreenRow + for decorationId of decorations when not blockDecorations.hasOwnProperty(decorationId) + delete @blockDecorationsDimensionsById[decorationId] + delete @blockDecorationsDimensionsByScreenRow[screenRow][decorationId] @shouldUpdateVerticalScrollState = true From a9cb1bda8d217ee5f4ea614487d8323a0fbd4cf9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 27 Nov 2015 10:16:38 +0100 Subject: [PATCH 101/502] Use `::positionForRow` in `::updateTilesState` The algorithm is still super sloooow :snail:, but we'll fix that in a later commit. --- spec/text-editor-presenter-spec.coffee | 31 ++++++++++++++++++++++++++ src/text-editor-presenter.coffee | 12 +++++----- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 010a9fc2d..9edd8e9f8 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -168,6 +168,37 @@ describe "TextEditorPresenter", -> expect(stateFn(presenter).tiles[12]).toBeUndefined() + it "computes each tile's height and scrollTop based on block decorations' height", -> + presenter = buildPresenter(explicitHeight: 120, scrollTop: 0, lineHeight: 10, tileSize: 2) + + blockDecoration1 = editor.addBlockDecorationForScreenRow(3) + blockDecoration2 = editor.addBlockDecorationForScreenRow(5) + presenter.setBlockDecorationDimensions(blockDecoration1, 0, 30) + presenter.setBlockDecorationDimensions(blockDecoration2, 0, 40) + + expect(stateFn(presenter).tiles[0].height).toBe(20) + expect(stateFn(presenter).tiles[0].top).toBe(0) + expect(stateFn(presenter).tiles[2].height).toBe(20 + 30) + expect(stateFn(presenter).tiles[2].top).toBe(20) + expect(stateFn(presenter).tiles[4].height).toBe(20 + 40) + expect(stateFn(presenter).tiles[4].top).toBe(20 + 30 + 20) + expect(stateFn(presenter).tiles[6].height).toBe(20) + expect(stateFn(presenter).tiles[6].top).toBe((20 + 40) + (20 + 30) + 20) + expect(stateFn(presenter).tiles[8]).toBeUndefined() + + presenter.setScrollTop(20) + + expect(stateFn(presenter).tiles[0]).toBeUndefined() + expect(stateFn(presenter).tiles[2].height).toBe(20 + 30) + expect(stateFn(presenter).tiles[2].top).toBe(0) + expect(stateFn(presenter).tiles[4].height).toBe(20 + 40) + expect(stateFn(presenter).tiles[4].top).toBe(30 + 20) + expect(stateFn(presenter).tiles[6].height).toBe(20) + expect(stateFn(presenter).tiles[6].top).toBe((20 + 40) + (20 + 30)) + expect(stateFn(presenter).tiles[8].height).toBe(20) + expect(stateFn(presenter).tiles[8].top).toBe((20 + 40) + (20 + 30) + 20) + expect(stateFn(presenter).tiles[10]).toBeUndefined() + it "includes state for all tiles if no external ::explicitHeight is assigned", -> presenter = buildPresenter(explicitHeight: null, tileSize: 2) expect(stateFn(presenter).tiles[0]).toBeDefined() diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index ab9def07e..5cec15278 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -429,14 +429,14 @@ class TextEditorPresenter tile = @state.content.tiles[tileStartRow] ?= {} tile.top = @positionForRow(tileStartRow) - @scrollTop tile.left = -@scrollLeft - tile.height = @tileSize * @lineHeight + tile.height = @positionForRow(tileStartRow + @tileSize) - @positionForRow(tileStartRow) tile.display = "block" tile.zIndex = zIndex tile.highlights ?= {} gutterTile = @lineNumberGutter.tiles[tileStartRow] ?= {} - gutterTile.top = tileStartRow * @lineHeight - @scrollTop - gutterTile.height = @tileSize * @lineHeight + gutterTile.top = @positionForRow(tileStartRow) - @scrollTop + gutterTile.height = @positionForRow(tileStartRow + @tileSize) - @positionForRow(tileStartRow) gutterTile.display = "block" gutterTile.zIndex = zIndex @@ -697,7 +697,8 @@ class TextEditorPresenter top = 0 for tileRow in [0..@model.getScreenLineCount()] by @tileSize for row in [tileRow...Math.min(tileRow + @tileSize, @model.getScreenLineCount())] by 1 - nextTop = top + @lineHeight + blockDecorationsForCurrentRow = _.values(@blockDecorationsDimensionsByScreenRow[row]) + nextTop = top + @lineHeight + @getBlockDecorationsHeight(blockDecorationsForCurrentRow) if floor return row if nextTop > position else @@ -710,7 +711,8 @@ class TextEditorPresenter for tileRow in [0..@model.getScreenLineCount()] by @tileSize for row in [tileRow...Math.min(tileRow + @tileSize, @model.getScreenLineCount())] by 1 return top if row is targetRow - top += @lineHeight + blockDecorationsForNextRow = _.values(@blockDecorationsDimensionsByScreenRow[row + 1]) + top += @lineHeight + @getBlockDecorationsHeight(blockDecorationsForNextRow) top updateStartRow: -> From 6e2587bc8cfb0b96bd83d210e8bfd81a52a1fa3a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 27 Nov 2015 10:30:43 +0100 Subject: [PATCH 102/502] :racehorse: Cache screen row height I am trying to defer the usage of fancy algorithms as much as possible. The current one is linear and should probably be changed, but it performs quite decently for the time being. Maybe with some more caching we could even avoid to implement a tree data structure? --- src/text-editor-presenter.coffee | 33 ++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 5cec15278..aea789416 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -30,6 +30,7 @@ class TextEditorPresenter @customGutterDecorationsByGutterName = {} @blockDecorationsDimensionsById = {} @blockDecorationsDimensionsByScreenRow = {} + @heightsByScreenRow = {} @screenRowsToMeasure = [] @transferMeasurementsToModel() @transferMeasurementsFromModel() @@ -426,17 +427,20 @@ class TextEditorPresenter continue if rowsWithinTile.length is 0 + top = @positionForRow(tileStartRow) + height = @positionForRow(tileStartRow + @tileSize) - top + tile = @state.content.tiles[tileStartRow] ?= {} - tile.top = @positionForRow(tileStartRow) - @scrollTop + tile.top = top - @scrollTop tile.left = -@scrollLeft - tile.height = @positionForRow(tileStartRow + @tileSize) - @positionForRow(tileStartRow) + tile.height = height tile.display = "block" tile.zIndex = zIndex tile.highlights ?= {} gutterTile = @lineNumberGutter.tiles[tileStartRow] ?= {} - gutterTile.top = @positionForRow(tileStartRow) - @scrollTop - gutterTile.height = @positionForRow(tileStartRow + @tileSize) - @positionForRow(tileStartRow) + gutterTile.top = top - @scrollTop + gutterTile.height = height gutterTile.display = "block" gutterTile.zIndex = zIndex @@ -693,12 +697,17 @@ class TextEditorPresenter return + getScreenRowHeight: (screenRow) -> + @heightsByScreenRow[screenRow] or @lineHeight + + setScreenRowHeight: (screenRow, height) -> + @heightsByScreenRow[screenRow] = height + rowForPosition: (position, floor = true) -> top = 0 for tileRow in [0..@model.getScreenLineCount()] by @tileSize for row in [tileRow...Math.min(tileRow + @tileSize, @model.getScreenLineCount())] by 1 - blockDecorationsForCurrentRow = _.values(@blockDecorationsDimensionsByScreenRow[row]) - nextTop = top + @lineHeight + @getBlockDecorationsHeight(blockDecorationsForCurrentRow) + nextTop = top + @getScreenRowHeight(row) if floor return row if nextTop > position else @@ -711,8 +720,7 @@ class TextEditorPresenter for tileRow in [0..@model.getScreenLineCount()] by @tileSize for row in [tileRow...Math.min(tileRow + @tileSize, @model.getScreenLineCount())] by 1 return top if row is targetRow - blockDecorationsForNextRow = _.values(@blockDecorationsDimensionsByScreenRow[row + 1]) - top += @lineHeight + @getBlockDecorationsHeight(blockDecorationsForNextRow) + top += @getScreenRowHeight(row + 1) top updateStartRow: -> @@ -1403,10 +1411,15 @@ class TextEditorPresenter screenRow = decoration.getMarker().getHeadScreenPosition().row dimensions = {width, height} - @blockDecorationsDimensionsByScreenRow[screenRow] ?= {} - @blockDecorationsDimensionsByScreenRow[screenRow][decoration.id] = dimensions + screenRowDecorations = @blockDecorationsDimensionsByScreenRow[screenRow] ?= {} + screenRowDecorations[decoration.id] = dimensions @blockDecorationsDimensionsById[decoration.id] = dimensions + @setScreenRowHeight( + screenRow, + @lineHeight + @getBlockDecorationsHeight(_.values(screenRowDecorations)) + ) + @shouldUpdateBlockDecorations = true @shouldUpdateVerticalScrollState = true @emitDidUpdateState() From 0b5638f7495814e9f4315f96d2d9470a287e39df Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 27 Nov 2015 10:47:25 +0100 Subject: [PATCH 103/502] Make sure tile positions are computed correctly --- spec/text-editor-presenter-spec.coffee | 20 +++++++++++--------- src/text-editor-presenter.coffee | 2 +- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 9edd8e9f8..ef8f8b2bd 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -171,22 +171,24 @@ describe "TextEditorPresenter", -> it "computes each tile's height and scrollTop based on block decorations' height", -> presenter = buildPresenter(explicitHeight: 120, scrollTop: 0, lineHeight: 10, tileSize: 2) - blockDecoration1 = editor.addBlockDecorationForScreenRow(3) - blockDecoration2 = editor.addBlockDecorationForScreenRow(5) - presenter.setBlockDecorationDimensions(blockDecoration1, 0, 30) - presenter.setBlockDecorationDimensions(blockDecoration2, 0, 40) + blockDecoration1 = editor.addBlockDecorationForScreenRow(0) + blockDecoration2 = editor.addBlockDecorationForScreenRow(3) + blockDecoration3 = editor.addBlockDecorationForScreenRow(5) + presenter.setBlockDecorationDimensions(blockDecoration1, 0, 1) + presenter.setBlockDecorationDimensions(blockDecoration2, 0, 30) + presenter.setBlockDecorationDimensions(blockDecoration3, 0, 40) - expect(stateFn(presenter).tiles[0].height).toBe(20) + expect(stateFn(presenter).tiles[0].height).toBe(20 + 1) expect(stateFn(presenter).tiles[0].top).toBe(0) expect(stateFn(presenter).tiles[2].height).toBe(20 + 30) - expect(stateFn(presenter).tiles[2].top).toBe(20) + expect(stateFn(presenter).tiles[2].top).toBe(20 + 1) expect(stateFn(presenter).tiles[4].height).toBe(20 + 40) - expect(stateFn(presenter).tiles[4].top).toBe(20 + 30 + 20) + expect(stateFn(presenter).tiles[4].top).toBe(20 + 30 + 20 + 1) expect(stateFn(presenter).tiles[6].height).toBe(20) - expect(stateFn(presenter).tiles[6].top).toBe((20 + 40) + (20 + 30) + 20) + expect(stateFn(presenter).tiles[6].top).toBe((20 + 40) + (20 + 30) + 20 + 1) expect(stateFn(presenter).tiles[8]).toBeUndefined() - presenter.setScrollTop(20) + presenter.setScrollTop(21) expect(stateFn(presenter).tiles[0]).toBeUndefined() expect(stateFn(presenter).tiles[2].height).toBe(20 + 30) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index aea789416..8738357b4 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -720,7 +720,7 @@ class TextEditorPresenter for tileRow in [0..@model.getScreenLineCount()] by @tileSize for row in [tileRow...Math.min(tileRow + @tileSize, @model.getScreenLineCount())] by 1 return top if row is targetRow - top += @getScreenRowHeight(row + 1) + top += @getScreenRowHeight(row) top updateStartRow: -> From 1a8d7b486d644b0d9ea0914721077a8b9fc0f5a7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 27 Nov 2015 12:10:48 +0100 Subject: [PATCH 104/502] Remove caching by screen rows Because we cannot use them as a cache key, because markers' position can change at any time. Performance-wise this is slow with many markers, as we need to do a lot of buffer-to-screen conversions. --- src/text-editor-presenter.coffee | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 8738357b4..4fd612ec5 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -74,7 +74,7 @@ class TextEditorPresenter getPreMeasurementState: -> @updating = true - @updateBlockDecorations() if @shouldUpdateBlockDecorations + @updateBlockDecorations() @updateVerticalDimensions() @updateScrollbarDimensions() @@ -144,7 +144,6 @@ class TextEditorPresenter @shouldUpdateHiddenInputState = false @shouldUpdateContentState = false @shouldUpdateDecorations = false - @shouldUpdateBlockDecorations = false @shouldUpdateLinesState = false @shouldUpdateTilesState = false @shouldUpdateCursorsState = false @@ -163,7 +162,6 @@ class TextEditorPresenter @shouldUpdateHiddenInputState = true @shouldUpdateContentState = true @shouldUpdateDecorations = true - @shouldUpdateBlockDecorations = true @shouldUpdateLinesState = true @shouldUpdateTilesState = true @shouldUpdateCursorsState = true @@ -194,7 +192,6 @@ class TextEditorPresenter @shouldUpdateLineNumbersState = true @shouldUpdateDecorations = true @shouldUpdateOverlaysState = true - @shouldUpdateBlockDecorations = true @shouldUpdateVerticalScrollState = true @shouldUpdateCustomGutterDecorationState = true @emitDidUpdateState() @@ -1410,32 +1407,27 @@ class TextEditorPresenter setBlockDecorationDimensions: (decoration, width, height) -> screenRow = decoration.getMarker().getHeadScreenPosition().row dimensions = {width, height} - - screenRowDecorations = @blockDecorationsDimensionsByScreenRow[screenRow] ?= {} - screenRowDecorations[decoration.id] = dimensions @blockDecorationsDimensionsById[decoration.id] = dimensions - @setScreenRowHeight( - screenRow, - @lineHeight + @getBlockDecorationsHeight(_.values(screenRowDecorations)) - ) - - @shouldUpdateBlockDecorations = true @shouldUpdateVerticalScrollState = true @emitDidUpdateState() updateBlockDecorations: -> - # TODO: Move this inside `DisplayBuffer` + @heightsByScreenRow = {} blockDecorations = {} + + # TODO: move into DisplayBuffer for decoration in @model.getDecorations(type: "block") blockDecorations[decoration.id] = decoration - for screenRow, decorations of @blockDecorationsDimensionsByScreenRow - for decorationId of decorations when not blockDecorations.hasOwnProperty(decorationId) + for decorationId of @blockDecorationsDimensionsById + unless blockDecorations.hasOwnProperty(decorationId) delete @blockDecorationsDimensionsById[decorationId] - delete @blockDecorationsDimensionsByScreenRow[screenRow][decorationId] - @shouldUpdateVerticalScrollState = true + for decorationId, decoration of blockDecorations + screenRow = decoration.getMarker().getHeadScreenPosition().row + @heightsByScreenRow[screenRow] ?= @lineHeight + @heightsByScreenRow[screenRow] += @blockDecorationsDimensionsById[decorationId].height observeCursor: (cursor) -> didChangePositionDisposable = cursor.onDidChangePosition => From e1e06580c1b3f12118ce4eff8fc331ace3e1604e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 27 Nov 2015 12:47:47 +0100 Subject: [PATCH 105/502] Move position conversion in LinesYardstick --- spec/fake-lines-yardstick.coffee | 30 +++++++++++++++++++++----- src/lines-yardstick.coffee | 36 ++++++++++++++++++++++++-------- src/text-editor-presenter.coffee | 34 +++++++----------------------- 3 files changed, 59 insertions(+), 41 deletions(-) diff --git a/spec/fake-lines-yardstick.coffee b/spec/fake-lines-yardstick.coffee index 1872b8c65..781318f8c 100644 --- a/spec/fake-lines-yardstick.coffee +++ b/spec/fake-lines-yardstick.coffee @@ -31,7 +31,7 @@ class FakeLinesYardstick targetColumn = screenPosition.column baseCharacterWidth = @model.getDefaultCharWidth() - top = targetRow * @model.getLineHeightInPixels() + top = @topPixelPositionForRow(targetRow) left = 0 column = 0 @@ -60,17 +60,37 @@ class FakeLinesYardstick {top, left} - pixelRectForScreenRange: (screenRange) -> - lineHeight = @model.getLineHeightInPixels() + rowForTopPixelPosition: (position, floor = true) -> + top = 0 + for tileStartRow in [0..@model.getScreenLineCount()] by @presenter.getTileSize() + tileEndRow = Math.min(tileStartRow + @presenter.getTileSize(), @model.getScreenLineCount()) + for row in [tileStartRow...tileEndRow] by 1 + nextTop = top + @presenter.getScreenRowHeight(row) + if floor + return row if nextTop > position + else + return row if top >= position + top = nextTop + @model.getScreenLineCount() + topPixelPositionForRow: (targetRow) -> + top = 0 + for tileStartRow in [0..@model.getScreenLineCount()] by @presenter.getTileSize() + tileEndRow = Math.min(tileStartRow + @presenter.getTileSize(), @model.getScreenLineCount()) + for row in [tileStartRow...tileEndRow] by 1 + return top if row is targetRow + top += @presenter.getScreenRowHeight(row) + top + + pixelRectForScreenRange: (screenRange) -> if screenRange.end.row > screenRange.start.row top = @pixelPositionForScreenPosition(screenRange.start).top left = 0 - height = (screenRange.end.row - screenRange.start.row + 1) * lineHeight + height = @topPixelPositionForRow(screenRange.end.row + 1) - top width = @presenter.getScrollWidth() else {top, left} = @pixelPositionForScreenPosition(screenRange.start, false) - height = lineHeight + height = @topPixelPositionForRow(screenRange.end.row + 1) - top width = @pixelPositionForScreenPosition(screenRange.end, false).left - left {top, left, width, height} diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index 54ba6cf57..2febf8add 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -22,11 +22,9 @@ class LinesYardstick targetTop = pixelPosition.top targetLeft = pixelPosition.left defaultCharWidth = @model.getDefaultCharWidth() - row = Math.floor(targetTop / @model.getLineHeightInPixels()) - targetLeft = 0 if row < 0 + row = @rowForTopPixelPosition(targetTop) + targetLeft = 0 if targetTop < 0 targetLeft = Infinity if row > @model.getLastScreenRow() - row = Math.min(row, @model.getLastScreenRow()) - row = Math.max(0, row) @prepareScreenRowsForMeasurement([row]) unless measureVisibleLinesOnly @@ -92,7 +90,7 @@ class LinesYardstick @prepareScreenRowsForMeasurement([targetRow]) unless measureVisibleLinesOnly - top = targetRow * @model.getLineHeightInPixels() + top = @topPixelPositionForRow(targetRow) left = @leftPixelPositionForScreenPosition(targetRow, targetColumn) @clearScreenRowsForMeasurement() unless measureVisibleLinesOnly @@ -174,17 +172,37 @@ class LinesYardstick left + width - offset - pixelRectForScreenRange: (screenRange, measureVisibleLinesOnly) -> - lineHeight = @model.getLineHeightInPixels() + rowForTopPixelPosition: (position, floor = true) -> + top = 0 + for tileStartRow in [0..@model.getScreenLineCount()] by @presenter.getTileSize() + tileEndRow = Math.min(tileStartRow + @presenter.getTileSize(), @model.getScreenLineCount()) + for row in [tileStartRow...tileEndRow] by 1 + nextTop = top + @presenter.getScreenRowHeight(row) + if floor + return row if nextTop > position + else + return row if top >= position + top = nextTop + @model.getScreenLineCount() + topPixelPositionForRow: (targetRow) -> + top = 0 + for tileStartRow in [0..@model.getScreenLineCount()] by @presenter.getTileSize() + tileEndRow = Math.min(tileStartRow + @presenter.getTileSize(), @model.getScreenLineCount()) + for row in [tileStartRow...tileEndRow] by 1 + return top if row is targetRow + top += @presenter.getScreenRowHeight(row) + top + + pixelRectForScreenRange: (screenRange, measureVisibleLinesOnly) -> if screenRange.end.row > screenRange.start.row top = @pixelPositionForScreenPosition(screenRange.start, true, measureVisibleLinesOnly).top left = 0 - height = (screenRange.end.row - screenRange.start.row + 1) * lineHeight + height = @topPixelPositionForRow(screenRange.end.row + 1) - top width = @presenter.getScrollWidth() else {top, left} = @pixelPositionForScreenPosition(screenRange.start, false, measureVisibleLinesOnly) - height = lineHeight + height = @topPixelPositionForRow(screenRange.end.row + 1) - top width = @pixelPositionForScreenPosition(screenRange.end, false, measureVisibleLinesOnly).left - left {top, left, width, height} diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 4fd612ec5..041a117e1 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -374,6 +374,9 @@ class TextEditorPresenter getEndTileRow: -> @constrainRow(@tileForRow(@endRow)) + getTileSize: -> + @tileSize + isValidScreenRow: (screenRow) -> screenRow >= 0 and screenRow < @model.getScreenLineCount() @@ -424,8 +427,8 @@ class TextEditorPresenter continue if rowsWithinTile.length is 0 - top = @positionForRow(tileStartRow) - height = @positionForRow(tileStartRow + @tileSize) - top + top = @linesYardstick.topPixelPositionForRow(tileStartRow) + height = @linesYardstick.topPixelPositionForRow(tileStartRow + @tileSize) - top tile = @state.content.tiles[tileStartRow] ?= {} tile.top = top - @scrollTop @@ -697,38 +700,15 @@ class TextEditorPresenter getScreenRowHeight: (screenRow) -> @heightsByScreenRow[screenRow] or @lineHeight - setScreenRowHeight: (screenRow, height) -> - @heightsByScreenRow[screenRow] = height - - rowForPosition: (position, floor = true) -> - top = 0 - for tileRow in [0..@model.getScreenLineCount()] by @tileSize - for row in [tileRow...Math.min(tileRow + @tileSize, @model.getScreenLineCount())] by 1 - nextTop = top + @getScreenRowHeight(row) - if floor - return row if nextTop > position - else - return row if top >= position - top = nextTop - @model.getScreenLineCount() - - positionForRow: (targetRow) -> - top = 0 - for tileRow in [0..@model.getScreenLineCount()] by @tileSize - for row in [tileRow...Math.min(tileRow + @tileSize, @model.getScreenLineCount())] by 1 - return top if row is targetRow - top += @getScreenRowHeight(row) - top - updateStartRow: -> return unless @scrollTop? and @lineHeight? - @startRow = Math.max(0, @rowForPosition(@scrollTop)) + @startRow = Math.max(0, @linesYardstick.rowForTopPixelPosition(@scrollTop)) updateEndRow: -> return unless @scrollTop? and @lineHeight? and @height? - @endRow = @rowForPosition(@scrollTop + @height + @lineHeight, false) + @endRow = @linesYardstick.rowForTopPixelPosition(@scrollTop + @height + @lineHeight, false) updateRowsPerPage: -> rowsPerPage = Math.floor(@getClientHeight() / @lineHeight) From 587f86261277b6c0428d041b5bd272c44ec9ee42 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 27 Nov 2015 12:55:09 +0100 Subject: [PATCH 106/502] Use LinesYardstick consistently --- src/text-editor-presenter.coffee | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 041a117e1..ece1fe790 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -638,9 +638,11 @@ class TextEditorPresenter continue unless @gutterIsVisible(gutter) for decorationId, {properties, screenRange} of @customGutterDecorationsByGutterName[gutterName] + top = @linesYardstick.topPixelPositionForRow(screenRange.start.row) + bottom = @linesYardstick.topPixelPositionForRow(screenRange.end.row + 1) @customGutterDecorations[gutterName][decorationId] = - top: @lineHeight * screenRange.start.row - height: @lineHeight * screenRange.getRowCount() + top: top + height: bottom - top item: properties.item class: properties.class @@ -1313,7 +1315,7 @@ class TextEditorPresenter screenRange.end.column = 0 repositionRegionWithinTile: (region, tileStartRow) -> - region.top += @scrollTop - tileStartRow * @lineHeight + region.top += @scrollTop - @linesYardstick.topPixelPositionForRow(tileStartRow) region.left += @scrollLeft buildHighlightRegions: (screenRange) -> @@ -1487,7 +1489,7 @@ class TextEditorPresenter @emitDidUpdateState() didChangeFirstVisibleScreenRow: (screenRow) -> - @updateScrollTop(screenRow * @lineHeight) + @updateScrollTop(@linesYardstick.topPixelPositionForRow(screenRow)) getVerticalScrollMarginInPixels: -> Math.round(@model.getVerticalScrollMargin() * @lineHeight) @@ -1508,8 +1510,8 @@ class TextEditorPresenter verticalScrollMarginInPixels = @getVerticalScrollMarginInPixels() - top = screenRange.start.row * @lineHeight - bottom = (screenRange.end.row + 1) * @lineHeight + top = @linesYardstick.topPixelPositionForRow(screenRange.start.row) + bottom = @linesYardstick.topPixelPositionForRow(screenRange.end.row + 1) if options?.center desiredScrollCenter = (top + bottom) / 2 @@ -1581,7 +1583,7 @@ class TextEditorPresenter restoreScrollTopIfNeeded: -> unless @scrollTop? - @updateScrollTop(@model.getFirstVisibleScreenRow() * @lineHeight) + @updateScrollTop(@linesYardstick.topPixelPositionForRow(@model.getFirstVisibleScreenRow())) restoreScrollLeftIfNeeded: -> unless @scrollLeft? From 6611cf8353463968199a9c3db3e2a6c75d2065d1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 27 Nov 2015 14:05:05 +0100 Subject: [PATCH 107/502] Force cursor to be tall as ::lineHeight --- src/text-editor-presenter.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index ece1fe790..fd23d337a 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -509,6 +509,7 @@ class TextEditorPresenter return unless cursor.isVisible() and @startRow <= screenRange.start.row < @endRow pixelRect = @pixelRectForScreenRange(screenRange) + pixelRect.height = @lineHeight pixelRect.width = Math.round(@baseCharacterWidth) if pixelRect.width is 0 @state.content.cursors[cursor.id] = pixelRect From 6365c869927b949c14f6e7e66de2f7d9da741b69 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 27 Nov 2015 14:32:01 +0100 Subject: [PATCH 108/502] Include blockDecorations inside each line's state --- spec/text-editor-presenter-spec.coffee | 62 ++++++++++++++++++++++++++ src/text-editor-presenter.coffee | 7 +++ 2 files changed, 69 insertions(+) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index ef8f8b2bd..615019ee4 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -1270,6 +1270,68 @@ describe "TextEditorPresenter", -> expect(lineStateForScreenRow(presenter, 0).endOfLineInvisibles).toEqual [atom.config.get('editor.invisibles.eol')] expect(lineStateForScreenRow(presenter, 1).endOfLineInvisibles).toEqual [atom.config.get('editor.invisibles.cr'), atom.config.get('editor.invisibles.eol')] + describe ".blockDecorations", -> + it "adds block decorations to the relevant line state objects, both initially and when decorations change", -> + blockDecoration1 = editor.addBlockDecorationForScreenRow(0) + presenter = buildPresenter() + blockDecoration2 = editor.addBlockDecorationForScreenRow(3) + blockDecoration3 = editor.addBlockDecorationForScreenRow(7) + + waitsForStateToUpdate presenter + runs -> + expect(lineStateForScreenRow(presenter, 0).blockDecorations).toEqual [blockDecoration1] + expect(lineStateForScreenRow(presenter, 1).blockDecorations).toBeUndefined() + expect(lineStateForScreenRow(presenter, 2).blockDecorations).toBeUndefined() + expect(lineStateForScreenRow(presenter, 3).blockDecorations).toEqual [blockDecoration2] + expect(lineStateForScreenRow(presenter, 4).blockDecorations).toBeUndefined() + expect(lineStateForScreenRow(presenter, 5).blockDecorations).toBeUndefined() + expect(lineStateForScreenRow(presenter, 6).blockDecorations).toBeUndefined() + expect(lineStateForScreenRow(presenter, 7).blockDecorations).toEqual [blockDecoration3] + expect(lineStateForScreenRow(presenter, 8).blockDecorations).toBeUndefined() + expect(lineStateForScreenRow(presenter, 9).blockDecorations).toBeUndefined() + expect(lineStateForScreenRow(presenter, 10).blockDecorations).toBeUndefined() + expect(lineStateForScreenRow(presenter, 11).blockDecorations).toBeUndefined() + expect(lineStateForScreenRow(presenter, 12).blockDecorations).toBeUndefined() + + waitsForStateToUpdate presenter, -> + blockDecoration1.getMarker().setHeadBufferPosition([1, 0]) + blockDecoration2.getMarker().setHeadBufferPosition([5, 0]) + blockDecoration3.getMarker().setHeadBufferPosition([9, 0]) + + runs -> + expect(lineStateForScreenRow(presenter, 0).blockDecorations).toBeUndefined() + expect(lineStateForScreenRow(presenter, 1).blockDecorations).toEqual [blockDecoration1] + expect(lineStateForScreenRow(presenter, 2).blockDecorations).toBeUndefined() + expect(lineStateForScreenRow(presenter, 3).blockDecorations).toBeUndefined() + expect(lineStateForScreenRow(presenter, 4).blockDecorations).toBeUndefined() + expect(lineStateForScreenRow(presenter, 5).blockDecorations).toEqual [blockDecoration2] + expect(lineStateForScreenRow(presenter, 6).blockDecorations).toBeUndefined() + expect(lineStateForScreenRow(presenter, 7).blockDecorations).toBeUndefined() + expect(lineStateForScreenRow(presenter, 8).blockDecorations).toBeUndefined() + expect(lineStateForScreenRow(presenter, 9).blockDecorations).toEqual [blockDecoration3] + expect(lineStateForScreenRow(presenter, 10).blockDecorations).toBeUndefined() + expect(lineStateForScreenRow(presenter, 11).blockDecorations).toBeUndefined() + expect(lineStateForScreenRow(presenter, 12).blockDecorations).toBeUndefined() + + waitsForStateToUpdate presenter, -> + blockDecoration1.destroy() + blockDecoration3.destroy() + + runs -> + expect(lineStateForScreenRow(presenter, 0).blockDecorations).toBeUndefined() + expect(lineStateForScreenRow(presenter, 1).blockDecorations).toBeUndefined() + expect(lineStateForScreenRow(presenter, 2).blockDecorations).toBeUndefined() + expect(lineStateForScreenRow(presenter, 3).blockDecorations).toBeUndefined() + expect(lineStateForScreenRow(presenter, 4).blockDecorations).toBeUndefined() + expect(lineStateForScreenRow(presenter, 5).blockDecorations).toEqual [blockDecoration2] + expect(lineStateForScreenRow(presenter, 6).blockDecorations).toBeUndefined() + expect(lineStateForScreenRow(presenter, 7).blockDecorations).toBeUndefined() + expect(lineStateForScreenRow(presenter, 8).blockDecorations).toBeUndefined() + expect(lineStateForScreenRow(presenter, 9).blockDecorations).toBeUndefined() + expect(lineStateForScreenRow(presenter, 10).blockDecorations).toBeUndefined() + expect(lineStateForScreenRow(presenter, 11).blockDecorations).toBeUndefined() + expect(lineStateForScreenRow(presenter, 12).blockDecorations).toBeUndefined() + describe ".decorationClasses", -> it "adds decoration classes to the relevant line state objects, both initially and when decorations change", -> marker1 = editor.addMarkerLayer(maintainHistory: true).markBufferRange([[4, 0], [6, 2]], invalidate: 'touch') diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index fd23d337a..2cf5b0d2a 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -477,6 +477,7 @@ class TextEditorPresenter lineState = tileState.lines[line.id] lineState.screenRow = screenRow lineState.decorationClasses = @lineDecorationClassesForRow(screenRow) + lineState.blockDecorations = @blockDecorationsByScreenRow[screenRow] else tileState.lines[line.id] = screenRow: screenRow @@ -493,6 +494,7 @@ class TextEditorPresenter tabLength: line.tabLength fold: line.fold decorationClasses: @lineDecorationClassesForRow(screenRow) + blockDecorations: @blockDecorationsByScreenRow[screenRow] for id, line of tileState.lines delete tileState.lines[id] unless visibleLineIds.hasOwnProperty(id) @@ -1397,6 +1399,7 @@ class TextEditorPresenter updateBlockDecorations: -> @heightsByScreenRow = {} + @blockDecorationsByScreenRow = {} blockDecorations = {} # TODO: move into DisplayBuffer @@ -1409,6 +1412,10 @@ class TextEditorPresenter for decorationId, decoration of blockDecorations screenRow = decoration.getMarker().getHeadScreenPosition().row + @blockDecorationsByScreenRow[screenRow] ?= [] + @blockDecorationsByScreenRow[screenRow].push(decoration) + + continue unless @blockDecorationsDimensionsById.hasOwnProperty(decorationId) @heightsByScreenRow[screenRow] ?= @lineHeight @heightsByScreenRow[screenRow] += @blockDecorationsDimensionsById[decorationId].height From 6ad21307cce6ca86d9b1705a0e985f971f5801cc Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 27 Nov 2015 16:27:01 +0100 Subject: [PATCH 109/502] Provide blockDecorationsHeight for each line number --- spec/text-editor-presenter-spec.coffee | 65 ++++++++++++++++++++++++++ src/text-editor-presenter.coffee | 3 +- 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 615019ee4..8f7622f15 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -2563,6 +2563,71 @@ describe "TextEditorPresenter", -> expectValues lineNumberStateForScreenRow(presenter, 6), {screenRow: 6, bufferRow: 5, softWrapped: false} expectValues lineNumberStateForScreenRow(presenter, 7), {screenRow: 7, bufferRow: 6, softWrapped: false} + describe ".blockDecorations", -> + it "adds block decorations' height to the relevant line number state objects, both initially and when decorations change", -> + blockDecoration1 = editor.addBlockDecorationForScreenRow(0) + presenter = buildPresenter() + blockDecoration2 = editor.addBlockDecorationForScreenRow(3) + blockDecoration3 = editor.addBlockDecorationForScreenRow(3) + blockDecoration4 = editor.addBlockDecorationForScreenRow(7) + + presenter.setBlockDecorationDimensions(blockDecoration1, 0, 10) + presenter.setBlockDecorationDimensions(blockDecoration2, 0, 20) + presenter.setBlockDecorationDimensions(blockDecoration3, 0, 30) + presenter.setBlockDecorationDimensions(blockDecoration4, 0, 40) + + waitsForStateToUpdate presenter + runs -> + expect(lineNumberStateForScreenRow(presenter, 0).blockDecorationsHeight).toBe(10) + expect(lineNumberStateForScreenRow(presenter, 1).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 2).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 3).blockDecorationsHeight).toBe(20 + 30) + expect(lineNumberStateForScreenRow(presenter, 4).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 5).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 6).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 7).blockDecorationsHeight).toBe(40) + expect(lineNumberStateForScreenRow(presenter, 8).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 9).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 10).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 11).blockDecorationsHeight).toBe(0) + + waitsForStateToUpdate presenter, -> + blockDecoration1.getMarker().setHeadBufferPosition([1, 0]) + blockDecoration2.getMarker().setHeadBufferPosition([5, 0]) + blockDecoration3.getMarker().setHeadBufferPosition([9, 0]) + + runs -> + expect(lineNumberStateForScreenRow(presenter, 0).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 1).blockDecorationsHeight).toBe(10) + expect(lineNumberStateForScreenRow(presenter, 2).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 3).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 4).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 5).blockDecorationsHeight).toBe(20) + expect(lineNumberStateForScreenRow(presenter, 6).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 7).blockDecorationsHeight).toBe(40) + expect(lineNumberStateForScreenRow(presenter, 8).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 9).blockDecorationsHeight).toBe(30) + expect(lineNumberStateForScreenRow(presenter, 10).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 11).blockDecorationsHeight).toBe(0) + + waitsForStateToUpdate presenter, -> + blockDecoration1.destroy() + blockDecoration3.destroy() + + runs -> + expect(lineNumberStateForScreenRow(presenter, 0).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 1).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 2).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 3).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 4).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 5).blockDecorationsHeight).toBe(20) + expect(lineNumberStateForScreenRow(presenter, 6).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 7).blockDecorationsHeight).toBe(40) + expect(lineNumberStateForScreenRow(presenter, 8).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 9).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 10).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 11).blockDecorationsHeight).toBe(0) + describe ".decorationClasses", -> it "adds decoration classes to the relevant line number state objects, both initially and when decorations change", -> marker1 = editor.addMarkerLayer(maintainHistory: true).markBufferRange([[4, 0], [6, 2]], invalidate: 'touch') diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 2cf5b0d2a..d078594a3 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -693,8 +693,9 @@ class TextEditorPresenter line = @model.tokenizedLineForScreenRow(screenRow) decorationClasses = @lineNumberDecorationClassesForRow(screenRow) foldable = @model.isFoldableAtScreenRow(screenRow) + blockDecorationsHeight = @getScreenRowHeight(screenRow) - @lineHeight - tileState.lineNumbers[line.id] = {screenRow, bufferRow, softWrapped, decorationClasses, foldable} + tileState.lineNumbers[line.id] = {screenRow, bufferRow, softWrapped, decorationClasses, foldable, blockDecorationsHeight} visibleLineNumberIds[line.id] = true for id of tileState.lineNumbers From 0159d5c31eb184483a85b8a96026662f6a88f1f9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 27 Nov 2015 16:58:08 +0100 Subject: [PATCH 110/502] :art: --- spec/fake-lines-yardstick.coffee | 8 +------- src/lines-yardstick.coffee | 8 +------- src/text-editor-presenter.coffee | 14 +++++++------- 3 files changed, 9 insertions(+), 21 deletions(-) diff --git a/spec/fake-lines-yardstick.coffee b/spec/fake-lines-yardstick.coffee index 781318f8c..952a9ddfc 100644 --- a/spec/fake-lines-yardstick.coffee +++ b/spec/fake-lines-yardstick.coffee @@ -74,13 +74,7 @@ class FakeLinesYardstick @model.getScreenLineCount() topPixelPositionForRow: (targetRow) -> - top = 0 - for tileStartRow in [0..@model.getScreenLineCount()] by @presenter.getTileSize() - tileEndRow = Math.min(tileStartRow + @presenter.getTileSize(), @model.getScreenLineCount()) - for row in [tileStartRow...tileEndRow] by 1 - return top if row is targetRow - top += @presenter.getScreenRowHeight(row) - top + @presenter.getScreenRowsHeight(0, targetRow) pixelRectForScreenRange: (screenRange) -> if screenRange.end.row > screenRange.start.row diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index 2febf8add..442097537 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -186,13 +186,7 @@ class LinesYardstick @model.getScreenLineCount() topPixelPositionForRow: (targetRow) -> - top = 0 - for tileStartRow in [0..@model.getScreenLineCount()] by @presenter.getTileSize() - tileEndRow = Math.min(tileStartRow + @presenter.getTileSize(), @model.getScreenLineCount()) - for row in [tileStartRow...tileEndRow] by 1 - return top if row is targetRow - top += @presenter.getScreenRowHeight(row) - top + @presenter.getScreenRowsHeight(0, targetRow) pixelRectForScreenRange: (screenRange, measureVisibleLinesOnly) -> if screenRange.end.row > screenRange.start.row diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index d078594a3..d9f65f3cb 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -706,6 +706,12 @@ class TextEditorPresenter getScreenRowHeight: (screenRow) -> @heightsByScreenRow[screenRow] or @lineHeight + getScreenRowsHeight: (startRow, endRow) -> + height = 0 + for screenRow in [startRow...endRow] by 1 + height += @getScreenRowHeight(screenRow) + Math.round(height) + updateStartRow: -> return unless @scrollTop? and @lineHeight? @@ -746,17 +752,11 @@ class TextEditorPresenter getLinesHeight: -> @lineHeight * @model.getScreenLineCount() - getBlockDecorationsHeight: (blockDecorations) -> - sum = (a, b) -> a + b - Array.from(blockDecorations).map((size) -> size.height).reduce(sum, 0) - updateVerticalDimensions: -> if @lineHeight? oldContentHeight = @contentHeight allBlockDecorations = _.values(@blockDecorationsDimensionsById) - @contentHeight = Math.round( - @getLinesHeight() + @getBlockDecorationsHeight(allBlockDecorations) - ) + @contentHeight = @getScreenRowsHeight(0, @model.getScreenLineCount()) if @contentHeight isnt oldContentHeight @updateHeight() From ef851a822c4fbc51573a20698c930825ceaa96e9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 30 Nov 2015 10:12:44 +0100 Subject: [PATCH 111/502] :art: Move block decoration related stuff into its own presenter --- src/block-decorations-presenter.js | 75 ++++++++++++++++++++++++++++++ src/text-editor-presenter.coffee | 75 +++++++++++++++--------------- 2 files changed, 113 insertions(+), 37 deletions(-) create mode 100644 src/block-decorations-presenter.js diff --git a/src/block-decorations-presenter.js b/src/block-decorations-presenter.js new file mode 100644 index 000000000..373444c45 --- /dev/null +++ b/src/block-decorations-presenter.js @@ -0,0 +1,75 @@ +/** @babel */ + +const {Emitter} = require('event-kit') + +module.exports = +class BlockDecorationsPresenter { + constructor (model) { + this.model = model + this.emitter = new Emitter() + this.blockDecorationsDimensionsById = new Map + this.blockDecorationsByScreenRow = new Map + this.heightsByScreenRow = new Map + } + + onDidUpdateState (callback) { + return this.emitter.on('did-update-state', callback) + } + + update () { + this.heightsByScreenRow.clear() + this.blockDecorationsByScreenRow.clear() + let blockDecorations = new Map + + // TODO: move into DisplayBuffer + for (let decoration of this.model.getDecorations({type: "block"})) { + blockDecorations.set(decoration.id, decoration) + } + + for (let [decorationId] of this.blockDecorationsDimensionsById) { + if (!blockDecorations.has(decorationId)) { + this.blockDecorationsDimensionsById.delete(decorationId) + } + } + + for (let [decorationId, decoration] of blockDecorations) { + let screenRow = decoration.getMarker().getHeadScreenPosition().row + this.addBlockDecorationToScreenRow(screenRow, decoration) + if (this.hasMeasuredBlockDecoration(decoration)) { + this.addHeightToScreenRow( + screenRow, + this.blockDecorationsDimensionsById.get(decorationId).height + ) + } + } + } + + setBlockDecorationDimensions (decoration, width, height) { + this.blockDecorationsDimensionsById.set(decoration.id, {width, height}) + this.emitter.emit('did-update-state') + } + + blockDecorationsHeightForScreenRow (screenRow) { + return Number(this.heightsByScreenRow.get(screenRow)) || 0 + } + + addHeightToScreenRow (screenRow, height) { + let previousHeight = this.blockDecorationsHeightForScreenRow(screenRow) + let newHeight = previousHeight + height + this.heightsByScreenRow.set(screenRow, newHeight) + } + + addBlockDecorationToScreenRow (screenRow, decoration) { + let decorations = this.blockDecorationsForScreenRow(screenRow) || [] + decorations.push(decoration) + this.blockDecorationsByScreenRow.set(screenRow, decorations) + } + + blockDecorationsForScreenRow (screenRow) { + return this.blockDecorationsByScreenRow.get(screenRow) + } + + hasMeasuredBlockDecoration (decoration) { + return this.blockDecorationsDimensionsById.has(decoration.id) + } +} diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index d9f65f3cb..0e7a2c56d 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -2,6 +2,7 @@ {Point, Range} = require 'text-buffer' _ = require 'underscore-plus' Decoration = require './decoration' +BlockDecorationsPresenter = require './block-decorations-presenter' module.exports = class TextEditorPresenter @@ -28,9 +29,7 @@ class TextEditorPresenter @lineDecorationsByScreenRow = {} @lineNumberDecorationsByScreenRow = {} @customGutterDecorationsByGutterName = {} - @blockDecorationsDimensionsById = {} - @blockDecorationsDimensionsByScreenRow = {} - @heightsByScreenRow = {} + @blockDecorationsPresenter = new BlockDecorationsPresenter(@model) @screenRowsToMeasure = [] @transferMeasurementsToModel() @transferMeasurementsFromModel() @@ -74,7 +73,8 @@ class TextEditorPresenter getPreMeasurementState: -> @updating = true - @updateBlockDecorations() + @blockDecorationsPresenter.update() + @updateVerticalDimensions() @updateScrollbarDimensions() @@ -196,6 +196,35 @@ class TextEditorPresenter @shouldUpdateCustomGutterDecorationState = true @emitDidUpdateState() + @disposables.add @blockDecorationsPresenter.onDidUpdateState => + @shouldUpdateVerticalScrollState = true + @emitDidUpdateState() + + # @disposables.add @ + + # @disposables.add @model.onDidAddDecoration (decoration) => + # return unless decoration.isType("block") + # + # didMoveDisposable = decoration.getMarker().onDidChange ({oldHeadScreenPosition, newHeadScreenPosition}) => + # # @blockDecorationsMoveOperations.add() + # # @moveBlockDecoration(decoration, oldHeadScreenPosition, newHeadScreenPosition) + # @emitDidUpdateState() + # didChangeDisposable = decoration.onDidChangeProperties (properties) => + # # @changePropertiesOperations.add() + # # @updateBlockDecoration(decoration) + # @emitDidUpdateState() + # didDestroyDisposable = decoration.onDidDestroy => + # didMoveDisposable.dispose() + # didChangeDisposable.dispose() + # didDestroyDisposable.dispose() + # + # # @destroyOperations.add() + # # @destroyBlockDecoration(decoration) + # @emitDidUpdateState() + # + # # @addBlockDecoration(decoration) + # @emitDidUpdateState() + @disposables.add @model.onDidChangeGrammar(@didChangeGrammar.bind(this)) @disposables.add @model.onDidChangePlaceholderText => @shouldUpdateContentState = true @@ -477,7 +506,7 @@ class TextEditorPresenter lineState = tileState.lines[line.id] lineState.screenRow = screenRow lineState.decorationClasses = @lineDecorationClassesForRow(screenRow) - lineState.blockDecorations = @blockDecorationsByScreenRow[screenRow] + lineState.blockDecorations = this.blockDecorationsPresenter.blockDecorationsForScreenRow(screenRow) else tileState.lines[line.id] = screenRow: screenRow @@ -494,7 +523,7 @@ class TextEditorPresenter tabLength: line.tabLength fold: line.fold decorationClasses: @lineDecorationClassesForRow(screenRow) - blockDecorations: @blockDecorationsByScreenRow[screenRow] + blockDecorations: this.blockDecorationsPresenter.blockDecorationsForScreenRow(screenRow) for id, line of tileState.lines delete tileState.lines[id] unless visibleLineIds.hasOwnProperty(id) @@ -693,7 +722,7 @@ class TextEditorPresenter line = @model.tokenizedLineForScreenRow(screenRow) decorationClasses = @lineNumberDecorationClassesForRow(screenRow) foldable = @model.isFoldableAtScreenRow(screenRow) - blockDecorationsHeight = @getScreenRowHeight(screenRow) - @lineHeight + blockDecorationsHeight = @blockDecorationsPresenter.blockDecorationsHeightForScreenRow(screenRow) tileState.lineNumbers[line.id] = {screenRow, bufferRow, softWrapped, decorationClasses, foldable, blockDecorationsHeight} visibleLineNumberIds[line.id] = true @@ -704,7 +733,7 @@ class TextEditorPresenter return getScreenRowHeight: (screenRow) -> - @heightsByScreenRow[screenRow] or @lineHeight + @lineHeight + @blockDecorationsPresenter.blockDecorationsHeightForScreenRow(screenRow) getScreenRowsHeight: (startRow, endRow) -> height = 0 @@ -755,7 +784,6 @@ class TextEditorPresenter updateVerticalDimensions: -> if @lineHeight? oldContentHeight = @contentHeight - allBlockDecorations = _.values(@blockDecorationsDimensionsById) @contentHeight = @getScreenRowsHeight(0, @model.getScreenLineCount()) if @contentHeight isnt oldContentHeight @@ -1391,34 +1419,7 @@ class TextEditorPresenter @emitDidUpdateState() setBlockDecorationDimensions: (decoration, width, height) -> - screenRow = decoration.getMarker().getHeadScreenPosition().row - dimensions = {width, height} - @blockDecorationsDimensionsById[decoration.id] = dimensions - - @shouldUpdateVerticalScrollState = true - @emitDidUpdateState() - - updateBlockDecorations: -> - @heightsByScreenRow = {} - @blockDecorationsByScreenRow = {} - blockDecorations = {} - - # TODO: move into DisplayBuffer - for decoration in @model.getDecorations(type: "block") - blockDecorations[decoration.id] = decoration - - for decorationId of @blockDecorationsDimensionsById - unless blockDecorations.hasOwnProperty(decorationId) - delete @blockDecorationsDimensionsById[decorationId] - - for decorationId, decoration of blockDecorations - screenRow = decoration.getMarker().getHeadScreenPosition().row - @blockDecorationsByScreenRow[screenRow] ?= [] - @blockDecorationsByScreenRow[screenRow].push(decoration) - - continue unless @blockDecorationsDimensionsById.hasOwnProperty(decorationId) - @heightsByScreenRow[screenRow] ?= @lineHeight - @heightsByScreenRow[screenRow] += @blockDecorationsDimensionsById[decorationId].height + @blockDecorationsPresenter.setBlockDecorationDimensions(arguments...) observeCursor: (cursor) -> didChangePositionDisposable = cursor.onDidChangePosition => From 0419fb16a0524732d42c9446c4ce4160d2d65f64 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 30 Nov 2015 10:19:53 +0100 Subject: [PATCH 112/502] :art: Improve names a bit --- src/block-decorations-presenter.js | 46 +++++++++++++++--------------- src/text-editor-presenter.coffee | 10 +++---- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/block-decorations-presenter.js b/src/block-decorations-presenter.js index 373444c45..c5697e7d4 100644 --- a/src/block-decorations-presenter.js +++ b/src/block-decorations-presenter.js @@ -7,8 +7,8 @@ class BlockDecorationsPresenter { constructor (model) { this.model = model this.emitter = new Emitter() - this.blockDecorationsDimensionsById = new Map - this.blockDecorationsByScreenRow = new Map + this.dimensionsByDecorationId = new Map + this.decorationsByScreenRow = new Map this.heightsByScreenRow = new Map } @@ -18,58 +18,58 @@ class BlockDecorationsPresenter { update () { this.heightsByScreenRow.clear() - this.blockDecorationsByScreenRow.clear() - let blockDecorations = new Map + this.decorationsByScreenRow.clear() + let decorations = new Map // TODO: move into DisplayBuffer for (let decoration of this.model.getDecorations({type: "block"})) { - blockDecorations.set(decoration.id, decoration) + decorations.set(decoration.id, decoration) } - for (let [decorationId] of this.blockDecorationsDimensionsById) { - if (!blockDecorations.has(decorationId)) { - this.blockDecorationsDimensionsById.delete(decorationId) + for (let [decorationId] of this.dimensionsByDecorationId) { + if (!decorations.has(decorationId)) { + this.dimensionsByDecorationId.delete(decorationId) } } - for (let [decorationId, decoration] of blockDecorations) { + for (let [decorationId, decoration] of decorations) { let screenRow = decoration.getMarker().getHeadScreenPosition().row - this.addBlockDecorationToScreenRow(screenRow, decoration) - if (this.hasMeasuredBlockDecoration(decoration)) { + this.addDecorationToScreenRow(screenRow, decoration) + if (this.hasMeasurementsForDecoration(decoration)) { this.addHeightToScreenRow( screenRow, - this.blockDecorationsDimensionsById.get(decorationId).height + this.dimensionsByDecorationId.get(decorationId).height ) } } } - setBlockDecorationDimensions (decoration, width, height) { - this.blockDecorationsDimensionsById.set(decoration.id, {width, height}) + setDimensionsForDecoration (decoration, width, height) { + this.dimensionsByDecorationId.set(decoration.id, {width, height}) this.emitter.emit('did-update-state') } - blockDecorationsHeightForScreenRow (screenRow) { + heightForScreenRow (screenRow) { return Number(this.heightsByScreenRow.get(screenRow)) || 0 } addHeightToScreenRow (screenRow, height) { - let previousHeight = this.blockDecorationsHeightForScreenRow(screenRow) + let previousHeight = this.heightForScreenRow(screenRow) let newHeight = previousHeight + height this.heightsByScreenRow.set(screenRow, newHeight) } - addBlockDecorationToScreenRow (screenRow, decoration) { - let decorations = this.blockDecorationsForScreenRow(screenRow) || [] + addDecorationToScreenRow (screenRow, decoration) { + let decorations = this.getDecorationsByScreenRow(screenRow) || [] decorations.push(decoration) - this.blockDecorationsByScreenRow.set(screenRow, decorations) + this.decorationsByScreenRow.set(screenRow, decorations) } - blockDecorationsForScreenRow (screenRow) { - return this.blockDecorationsByScreenRow.get(screenRow) + getDecorationsByScreenRow (screenRow) { + return this.decorationsByScreenRow.get(screenRow) } - hasMeasuredBlockDecoration (decoration) { - return this.blockDecorationsDimensionsById.has(decoration.id) + hasMeasurementsForDecoration (decoration) { + return this.dimensionsByDecorationId.has(decoration.id) } } diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 0e7a2c56d..153a78dbf 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -506,7 +506,7 @@ class TextEditorPresenter lineState = tileState.lines[line.id] lineState.screenRow = screenRow lineState.decorationClasses = @lineDecorationClassesForRow(screenRow) - lineState.blockDecorations = this.blockDecorationsPresenter.blockDecorationsForScreenRow(screenRow) + lineState.blockDecorations = this.blockDecorationsPresenter.getDecorationsByScreenRow(screenRow) else tileState.lines[line.id] = screenRow: screenRow @@ -523,7 +523,7 @@ class TextEditorPresenter tabLength: line.tabLength fold: line.fold decorationClasses: @lineDecorationClassesForRow(screenRow) - blockDecorations: this.blockDecorationsPresenter.blockDecorationsForScreenRow(screenRow) + blockDecorations: this.blockDecorationsPresenter.getDecorationsByScreenRow(screenRow) for id, line of tileState.lines delete tileState.lines[id] unless visibleLineIds.hasOwnProperty(id) @@ -722,7 +722,7 @@ class TextEditorPresenter line = @model.tokenizedLineForScreenRow(screenRow) decorationClasses = @lineNumberDecorationClassesForRow(screenRow) foldable = @model.isFoldableAtScreenRow(screenRow) - blockDecorationsHeight = @blockDecorationsPresenter.blockDecorationsHeightForScreenRow(screenRow) + blockDecorationsHeight = @blockDecorationsPresenter.heightForScreenRow(screenRow) tileState.lineNumbers[line.id] = {screenRow, bufferRow, softWrapped, decorationClasses, foldable, blockDecorationsHeight} visibleLineNumberIds[line.id] = true @@ -733,7 +733,7 @@ class TextEditorPresenter return getScreenRowHeight: (screenRow) -> - @lineHeight + @blockDecorationsPresenter.blockDecorationsHeightForScreenRow(screenRow) + @lineHeight + @blockDecorationsPresenter.heightForScreenRow(screenRow) getScreenRowsHeight: (startRow, endRow) -> height = 0 @@ -1419,7 +1419,7 @@ class TextEditorPresenter @emitDidUpdateState() setBlockDecorationDimensions: (decoration, width, height) -> - @blockDecorationsPresenter.setBlockDecorationDimensions(arguments...) + @blockDecorationsPresenter.setDimensionsForDecoration(arguments...) observeCursor: (cursor) -> didChangePositionDisposable = cursor.onDidChangePosition => From b998e7f2d99a48bfeaaf294d6a63346b7b7ac4c5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 30 Nov 2015 11:43:46 +0100 Subject: [PATCH 113/502] :racehorse: Incremental updates for block decorations --- spec/text-editor-presenter-spec.coffee | 64 ++++----- src/block-decorations-presenter.js | 171 +++++++++++++++++++------ src/decoration.coffee | 8 +- src/text-editor-presenter.coffee | 30 +---- 4 files changed, 176 insertions(+), 97 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 8f7622f15..58f832109 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -1280,18 +1280,18 @@ describe "TextEditorPresenter", -> waitsForStateToUpdate presenter runs -> expect(lineStateForScreenRow(presenter, 0).blockDecorations).toEqual [blockDecoration1] - expect(lineStateForScreenRow(presenter, 1).blockDecorations).toBeUndefined() - expect(lineStateForScreenRow(presenter, 2).blockDecorations).toBeUndefined() + expect(lineStateForScreenRow(presenter, 1).blockDecorations).toEqual [] + expect(lineStateForScreenRow(presenter, 2).blockDecorations).toEqual [] expect(lineStateForScreenRow(presenter, 3).blockDecorations).toEqual [blockDecoration2] - expect(lineStateForScreenRow(presenter, 4).blockDecorations).toBeUndefined() - expect(lineStateForScreenRow(presenter, 5).blockDecorations).toBeUndefined() - expect(lineStateForScreenRow(presenter, 6).blockDecorations).toBeUndefined() + expect(lineStateForScreenRow(presenter, 4).blockDecorations).toEqual [] + expect(lineStateForScreenRow(presenter, 5).blockDecorations).toEqual [] + expect(lineStateForScreenRow(presenter, 6).blockDecorations).toEqual [] expect(lineStateForScreenRow(presenter, 7).blockDecorations).toEqual [blockDecoration3] - expect(lineStateForScreenRow(presenter, 8).blockDecorations).toBeUndefined() - expect(lineStateForScreenRow(presenter, 9).blockDecorations).toBeUndefined() - expect(lineStateForScreenRow(presenter, 10).blockDecorations).toBeUndefined() - expect(lineStateForScreenRow(presenter, 11).blockDecorations).toBeUndefined() - expect(lineStateForScreenRow(presenter, 12).blockDecorations).toBeUndefined() + expect(lineStateForScreenRow(presenter, 8).blockDecorations).toEqual [] + expect(lineStateForScreenRow(presenter, 9).blockDecorations).toEqual [] + expect(lineStateForScreenRow(presenter, 10).blockDecorations).toEqual [] + expect(lineStateForScreenRow(presenter, 11).blockDecorations).toEqual [] + expect(lineStateForScreenRow(presenter, 12).blockDecorations).toEqual [] waitsForStateToUpdate presenter, -> blockDecoration1.getMarker().setHeadBufferPosition([1, 0]) @@ -1299,38 +1299,38 @@ describe "TextEditorPresenter", -> blockDecoration3.getMarker().setHeadBufferPosition([9, 0]) runs -> - expect(lineStateForScreenRow(presenter, 0).blockDecorations).toBeUndefined() + expect(lineStateForScreenRow(presenter, 0).blockDecorations).toEqual [] expect(lineStateForScreenRow(presenter, 1).blockDecorations).toEqual [blockDecoration1] - expect(lineStateForScreenRow(presenter, 2).blockDecorations).toBeUndefined() - expect(lineStateForScreenRow(presenter, 3).blockDecorations).toBeUndefined() - expect(lineStateForScreenRow(presenter, 4).blockDecorations).toBeUndefined() + expect(lineStateForScreenRow(presenter, 2).blockDecorations).toEqual [] + expect(lineStateForScreenRow(presenter, 3).blockDecorations).toEqual [] + expect(lineStateForScreenRow(presenter, 4).blockDecorations).toEqual [] expect(lineStateForScreenRow(presenter, 5).blockDecorations).toEqual [blockDecoration2] - expect(lineStateForScreenRow(presenter, 6).blockDecorations).toBeUndefined() - expect(lineStateForScreenRow(presenter, 7).blockDecorations).toBeUndefined() - expect(lineStateForScreenRow(presenter, 8).blockDecorations).toBeUndefined() + expect(lineStateForScreenRow(presenter, 6).blockDecorations).toEqual [] + expect(lineStateForScreenRow(presenter, 7).blockDecorations).toEqual [] + expect(lineStateForScreenRow(presenter, 8).blockDecorations).toEqual [] expect(lineStateForScreenRow(presenter, 9).blockDecorations).toEqual [blockDecoration3] - expect(lineStateForScreenRow(presenter, 10).blockDecorations).toBeUndefined() - expect(lineStateForScreenRow(presenter, 11).blockDecorations).toBeUndefined() - expect(lineStateForScreenRow(presenter, 12).blockDecorations).toBeUndefined() + expect(lineStateForScreenRow(presenter, 10).blockDecorations).toEqual [] + expect(lineStateForScreenRow(presenter, 11).blockDecorations).toEqual [] + expect(lineStateForScreenRow(presenter, 12).blockDecorations).toEqual [] waitsForStateToUpdate presenter, -> blockDecoration1.destroy() blockDecoration3.destroy() runs -> - expect(lineStateForScreenRow(presenter, 0).blockDecorations).toBeUndefined() - expect(lineStateForScreenRow(presenter, 1).blockDecorations).toBeUndefined() - expect(lineStateForScreenRow(presenter, 2).blockDecorations).toBeUndefined() - expect(lineStateForScreenRow(presenter, 3).blockDecorations).toBeUndefined() - expect(lineStateForScreenRow(presenter, 4).blockDecorations).toBeUndefined() + expect(lineStateForScreenRow(presenter, 0).blockDecorations).toEqual [] + expect(lineStateForScreenRow(presenter, 1).blockDecorations).toEqual [] + expect(lineStateForScreenRow(presenter, 2).blockDecorations).toEqual [] + expect(lineStateForScreenRow(presenter, 3).blockDecorations).toEqual [] + expect(lineStateForScreenRow(presenter, 4).blockDecorations).toEqual [] expect(lineStateForScreenRow(presenter, 5).blockDecorations).toEqual [blockDecoration2] - expect(lineStateForScreenRow(presenter, 6).blockDecorations).toBeUndefined() - expect(lineStateForScreenRow(presenter, 7).blockDecorations).toBeUndefined() - expect(lineStateForScreenRow(presenter, 8).blockDecorations).toBeUndefined() - expect(lineStateForScreenRow(presenter, 9).blockDecorations).toBeUndefined() - expect(lineStateForScreenRow(presenter, 10).blockDecorations).toBeUndefined() - expect(lineStateForScreenRow(presenter, 11).blockDecorations).toBeUndefined() - expect(lineStateForScreenRow(presenter, 12).blockDecorations).toBeUndefined() + expect(lineStateForScreenRow(presenter, 6).blockDecorations).toEqual [] + expect(lineStateForScreenRow(presenter, 7).blockDecorations).toEqual [] + expect(lineStateForScreenRow(presenter, 8).blockDecorations).toEqual [] + expect(lineStateForScreenRow(presenter, 9).blockDecorations).toEqual [] + expect(lineStateForScreenRow(presenter, 10).blockDecorations).toEqual [] + expect(lineStateForScreenRow(presenter, 11).blockDecorations).toEqual [] + expect(lineStateForScreenRow(presenter, 12).blockDecorations).toEqual [] describe ".decorationClasses", -> it "adds decoration classes to the relevant line state objects, both initially and when decorations change", -> diff --git a/src/block-decorations-presenter.js b/src/block-decorations-presenter.js index c5697e7d4..3a669e2f9 100644 --- a/src/block-decorations-presenter.js +++ b/src/block-decorations-presenter.js @@ -1,75 +1,172 @@ /** @babel */ -const {Emitter} = require('event-kit') +const {CompositeDisposable, Emitter} = require('event-kit') module.exports = class BlockDecorationsPresenter { constructor (model) { this.model = model + this.disposables = new CompositeDisposable() this.emitter = new Emitter() - this.dimensionsByDecorationId = new Map this.decorationsByScreenRow = new Map - this.heightsByScreenRow = new Map + this.heightByScreenRow = new Map + this.screenRowByDecoration = new Map + this.dimensionsByDecoration = new Map + this.moveOperationsByDecoration = new Map + this.addOperationsByDecoration = new Map + this.changeOperationsByDecoration = new Map + this.firstUpdate = true + + this.observeModel() + } + + destroy () { + this.disposables.dispose() } onDidUpdateState (callback) { - return this.emitter.on('did-update-state', callback) + return this.emitter.on("did-update-state", callback) + } + + observeModel () { + this.disposables.add( + this.model.onDidAddDecoration((decoration) => this.observeDecoration(decoration)) + ) } update () { - this.heightsByScreenRow.clear() + if (this.firstUpdate) { + this.fullUpdate() + this.firstUpdate = false + } else { + this.incrementalUpdate() + } + } + + fullUpdate () { this.decorationsByScreenRow.clear() - let decorations = new Map + this.screenRowByDecoration.clear() + this.moveOperationsByDecoration.clear() + this.addOperationsByDecoration.clear() - // TODO: move into DisplayBuffer for (let decoration of this.model.getDecorations({type: "block"})) { - decorations.set(decoration.id, decoration) - } - - for (let [decorationId] of this.dimensionsByDecorationId) { - if (!decorations.has(decorationId)) { - this.dimensionsByDecorationId.delete(decorationId) - } - } - - for (let [decorationId, decoration] of decorations) { let screenRow = decoration.getMarker().getHeadScreenPosition().row this.addDecorationToScreenRow(screenRow, decoration) - if (this.hasMeasurementsForDecoration(decoration)) { - this.addHeightToScreenRow( - screenRow, - this.dimensionsByDecorationId.get(decorationId).height - ) - } + this.observeDecoration(decoration) } } + incrementalUpdate () { + for (let [changedDecoration] of this.changeOperationsByDecoration) { + let screenRow = changedDecoration.getMarker().getHeadScreenPosition().row + this.recalculateScreenRowHeight(screenRow) + } + + for (let [addedDecoration] of this.addOperationsByDecoration) { + let screenRow = addedDecoration.getMarker().getHeadScreenPosition().row + this.addDecorationToScreenRow(screenRow, addedDecoration) + } + + for (let [movedDecoration, moveOperations] of this.moveOperationsByDecoration) { + let {oldHeadScreenPosition} = moveOperations[0] + let {newHeadScreenPosition} = moveOperations[moveOperations.length - 1] + this.removeDecorationFromScreenRow(oldHeadScreenPosition.row, movedDecoration) + this.addDecorationToScreenRow(newHeadScreenPosition.row, movedDecoration) + } + + this.addOperationsByDecoration.clear() + this.moveOperationsByDecoration.clear() + this.changeOperationsByDecoration.clear() + } + setDimensionsForDecoration (decoration, width, height) { - this.dimensionsByDecorationId.set(decoration.id, {width, height}) - this.emitter.emit('did-update-state') + this.changeOperationsByDecoration.set(decoration, true) + this.dimensionsByDecoration.set(decoration, {width, height}) + this.emitter.emit("did-update-state") } heightForScreenRow (screenRow) { - return Number(this.heightsByScreenRow.get(screenRow)) || 0 - } - - addHeightToScreenRow (screenRow, height) { - let previousHeight = this.heightForScreenRow(screenRow) - let newHeight = previousHeight + height - this.heightsByScreenRow.set(screenRow, newHeight) + return this.heightByScreenRow.get(screenRow) || 0 } addDecorationToScreenRow (screenRow, decoration) { - let decorations = this.getDecorationsByScreenRow(screenRow) || [] - decorations.push(decoration) - this.decorationsByScreenRow.set(screenRow, decorations) + let decorations = this.getDecorationsByScreenRow(screenRow) + if (!decorations.has(decoration)) { + decorations.add(decoration) + this.screenRowByDecoration.set(decoration, screenRow) + this.recalculateScreenRowHeight(screenRow) + } + } + + removeDecorationFromScreenRow (screenRow, decoration) { + if (!Number.isInteger(screenRow) || !decoration) { + return + } + + let decorations = this.getDecorationsByScreenRow(screenRow) + if (decorations.has(decoration)) { + decorations.delete(decoration) + this.recalculateScreenRowHeight(screenRow) + } } getDecorationsByScreenRow (screenRow) { + if (!this.decorationsByScreenRow.has(screenRow)) { + this.decorationsByScreenRow.set(screenRow, new Set()) + } + return this.decorationsByScreenRow.get(screenRow) } - hasMeasurementsForDecoration (decoration) { - return this.dimensionsByDecorationId.has(decoration.id) + getDecorationDimensions (decoration) { + return this.dimensionsByDecoration.get(decoration) || {width: 0, height: 0} + } + + recalculateScreenRowHeight (screenRow) { + let height = 0 + for (let decoration of this.getDecorationsByScreenRow(screenRow)) { + height += this.getDecorationDimensions(decoration).height + } + this.heightByScreenRow.set(screenRow, height) + } + + observeDecoration (decoration) { + if (!decoration.isType("block")) { + return + } + + let didMoveDisposable = decoration.getMarker().onDidChange((markerEvent) => { + this.didMoveDecoration(decoration, markerEvent) + }) + + let didDestroyDisposable = decoration.onDidDestroy(() => { + didMoveDisposable.dispose() + didDestroyDisposable.dispose() + this.didDestroyDecoration(decoration) + }) + + this.didAddDecoration(decoration) + } + + didAddDecoration (decoration) { + this.addOperationsByDecoration.set(decoration, true) + this.emitter.emit("did-update-state") + } + + didMoveDecoration (decoration, markerEvent) { + let moveOperations = this.moveOperationsByDecoration.get(decoration) || [] + moveOperations.push(markerEvent) + this.moveOperationsByDecoration.set(decoration, moveOperations) + this.emitter.emit("did-update-state") + } + + didDestroyDecoration (decoration) { + this.moveOperationsByDecoration.delete(decoration) + this.addOperationsByDecoration.delete(decoration) + + this.removeDecorationFromScreenRow( + this.screenRowByDecoration.get(decoration), decoration + ) + this.emitter.emit("did-update-state") } } diff --git a/src/decoration.coffee b/src/decoration.coffee index f57d234d1..11e32236d 100644 --- a/src/decoration.coffee +++ b/src/decoration.coffee @@ -35,7 +35,6 @@ translateDecorationParamsOldToNew = (decorationParams) -> # the marker. module.exports = class Decoration - # Private: Check if the `decorationProperties.type` matches `type` # # * `decorationProperties` {Object} eg. `{type: 'line-number', class: 'my-new-class'}` @@ -154,6 +153,13 @@ class Decoration @displayBuffer.scheduleUpdateDecorationsEvent() @emitter.emit 'did-change-properties', {oldProperties, newProperties} + ### + Section: Utility + ### + + inspect: -> + "" + ### Section: Private methods ### diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 153a78dbf..f71858fac 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -46,6 +46,7 @@ class TextEditorPresenter getLinesYardstick: -> @linesYardstick destroy: -> + @blockDecorationsPresenter.destroy() @disposables.dispose() clearTimeout(@stoppedScrollingTimeoutId) if @stoppedScrollingTimeoutId? clearInterval(@reflowingInterval) if @reflowingInterval? @@ -200,31 +201,6 @@ class TextEditorPresenter @shouldUpdateVerticalScrollState = true @emitDidUpdateState() - # @disposables.add @ - - # @disposables.add @model.onDidAddDecoration (decoration) => - # return unless decoration.isType("block") - # - # didMoveDisposable = decoration.getMarker().onDidChange ({oldHeadScreenPosition, newHeadScreenPosition}) => - # # @blockDecorationsMoveOperations.add() - # # @moveBlockDecoration(decoration, oldHeadScreenPosition, newHeadScreenPosition) - # @emitDidUpdateState() - # didChangeDisposable = decoration.onDidChangeProperties (properties) => - # # @changePropertiesOperations.add() - # # @updateBlockDecoration(decoration) - # @emitDidUpdateState() - # didDestroyDisposable = decoration.onDidDestroy => - # didMoveDisposable.dispose() - # didChangeDisposable.dispose() - # didDestroyDisposable.dispose() - # - # # @destroyOperations.add() - # # @destroyBlockDecoration(decoration) - # @emitDidUpdateState() - # - # # @addBlockDecoration(decoration) - # @emitDidUpdateState() - @disposables.add @model.onDidChangeGrammar(@didChangeGrammar.bind(this)) @disposables.add @model.onDidChangePlaceholderText => @shouldUpdateContentState = true @@ -506,7 +482,7 @@ class TextEditorPresenter lineState = tileState.lines[line.id] lineState.screenRow = screenRow lineState.decorationClasses = @lineDecorationClassesForRow(screenRow) - lineState.blockDecorations = this.blockDecorationsPresenter.getDecorationsByScreenRow(screenRow) + lineState.blockDecorations = Array.from(this.blockDecorationsPresenter.getDecorationsByScreenRow(screenRow)) else tileState.lines[line.id] = screenRow: screenRow @@ -523,7 +499,7 @@ class TextEditorPresenter tabLength: line.tabLength fold: line.fold decorationClasses: @lineDecorationClassesForRow(screenRow) - blockDecorations: this.blockDecorationsPresenter.getDecorationsByScreenRow(screenRow) + blockDecorations: Array.from(this.blockDecorationsPresenter.getDecorationsByScreenRow(screenRow)) for id, line of tileState.lines delete tileState.lines[id] unless visibleLineIds.hasOwnProperty(id) From 539a5b0ae7e2779b84e47e1a0e681f2fa86c45b9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 30 Nov 2015 14:26:52 +0100 Subject: [PATCH 114/502] :racehorse: Do as little pixel conversion as possible We desperately need a tree-based data structure. :cry: --- spec/fake-lines-yardstick.coffee | 16 +++++++++++++++- src/lines-yardstick.coffee | 16 +++++++++++++++- src/text-editor-presenter.coffee | 23 +++++++++++++---------- 3 files changed, 43 insertions(+), 12 deletions(-) diff --git a/spec/fake-lines-yardstick.coffee b/spec/fake-lines-yardstick.coffee index 952a9ddfc..9dfeb7da4 100644 --- a/spec/fake-lines-yardstick.coffee +++ b/spec/fake-lines-yardstick.coffee @@ -74,7 +74,21 @@ class FakeLinesYardstick @model.getScreenLineCount() topPixelPositionForRow: (targetRow) -> - @presenter.getScreenRowsHeight(0, targetRow) + top = 0 + for row in [0..targetRow] + return top if targetRow is row + top += @presenter.getScreenRowHeight(row) + top + + topPixelPositionForRows: (startRow, endRow, step) -> + results = {} + top = 0 + for tileStartRow in [0..endRow] by step + tileEndRow = Math.min(tileStartRow + step, @model.getScreenLineCount()) + results[tileStartRow] = top + for row in [tileStartRow...tileEndRow] by 1 + top += @presenter.getScreenRowHeight(row) + results pixelRectForScreenRange: (screenRange) -> if screenRange.end.row > screenRange.start.row diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index 442097537..d827d7bce 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -186,7 +186,21 @@ class LinesYardstick @model.getScreenLineCount() topPixelPositionForRow: (targetRow) -> - @presenter.getScreenRowsHeight(0, targetRow) + top = 0 + for row in [0..targetRow] + return top if targetRow is row + top += @presenter.getScreenRowHeight(row) + top + + topPixelPositionForRows: (startRow, endRow, step) -> + results = {} + top = 0 + for tileStartRow in [0..endRow] by step + tileEndRow = Math.min(tileStartRow + step, @model.getScreenLineCount()) + results[tileStartRow] = top + for row in [tileStartRow...tileEndRow] by 1 + top += @presenter.getScreenRowHeight(row) + results pixelRectForScreenRange: (screenRange, measureVisibleLinesOnly) -> if screenRange.end.row > screenRange.start.row diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index f71858fac..869b7914b 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -421,7 +421,14 @@ class TextEditorPresenter screenRowIndex = screenRows.length - 1 zIndex = 0 + tilesPositions = @linesYardstick.topPixelPositionForRows( + @tileForRow(startRow), + @tileForRow(endRow) + @tileSize, + @tileSize + ) + for tileStartRow in [@tileForRow(endRow)..@tileForRow(startRow)] by -@tileSize + tileEndRow = @constrainRow(tileStartRow + @tileSize) rowsWithinTile = [] while screenRowIndex >= 0 @@ -432,8 +439,8 @@ class TextEditorPresenter continue if rowsWithinTile.length is 0 - top = @linesYardstick.topPixelPositionForRow(tileStartRow) - height = @linesYardstick.topPixelPositionForRow(tileStartRow + @tileSize) - top + top = Math.round(tilesPositions[tileStartRow]) + height = Math.round(tilesPositions[tileEndRow] - top) tile = @state.content.tiles[tileStartRow] ?= {} tile.top = top - @scrollTop @@ -709,13 +716,7 @@ class TextEditorPresenter return getScreenRowHeight: (screenRow) -> - @lineHeight + @blockDecorationsPresenter.heightForScreenRow(screenRow) - - getScreenRowsHeight: (startRow, endRow) -> - height = 0 - for screenRow in [startRow...endRow] by 1 - height += @getScreenRowHeight(screenRow) - Math.round(height) + @lineHeight + @blockDecorationsPresenter.heightForScreenRow(screenRow) updateStartRow: -> return unless @scrollTop? and @lineHeight? @@ -760,7 +761,9 @@ class TextEditorPresenter updateVerticalDimensions: -> if @lineHeight? oldContentHeight = @contentHeight - @contentHeight = @getScreenRowsHeight(0, @model.getScreenLineCount()) + @contentHeight = Math.round( + @linesYardstick.topPixelPositionForRow(@model.getScreenLineCount()) + ) if @contentHeight isnt oldContentHeight @updateHeight() From 8a54a2c15bbb893675e1aa2bdbc86e55da14f098 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 30 Nov 2015 14:44:58 +0100 Subject: [PATCH 115/502] Use a boolean in each line state object --- spec/text-editor-presenter-spec.coffee | 86 +++++++++++++------------- src/text-editor-presenter.coffee | 5 +- 2 files changed, 46 insertions(+), 45 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 58f832109..b940fd06d 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -1270,8 +1270,8 @@ describe "TextEditorPresenter", -> expect(lineStateForScreenRow(presenter, 0).endOfLineInvisibles).toEqual [atom.config.get('editor.invisibles.eol')] expect(lineStateForScreenRow(presenter, 1).endOfLineInvisibles).toEqual [atom.config.get('editor.invisibles.cr'), atom.config.get('editor.invisibles.eol')] - describe ".blockDecorations", -> - it "adds block decorations to the relevant line state objects, both initially and when decorations change", -> + describe ".hasBlockDecorations", -> + it "is true when block decorations are present before a line, both initially and when decorations change", -> blockDecoration1 = editor.addBlockDecorationForScreenRow(0) presenter = buildPresenter() blockDecoration2 = editor.addBlockDecorationForScreenRow(3) @@ -1279,19 +1279,19 @@ describe "TextEditorPresenter", -> waitsForStateToUpdate presenter runs -> - expect(lineStateForScreenRow(presenter, 0).blockDecorations).toEqual [blockDecoration1] - expect(lineStateForScreenRow(presenter, 1).blockDecorations).toEqual [] - expect(lineStateForScreenRow(presenter, 2).blockDecorations).toEqual [] - expect(lineStateForScreenRow(presenter, 3).blockDecorations).toEqual [blockDecoration2] - expect(lineStateForScreenRow(presenter, 4).blockDecorations).toEqual [] - expect(lineStateForScreenRow(presenter, 5).blockDecorations).toEqual [] - expect(lineStateForScreenRow(presenter, 6).blockDecorations).toEqual [] - expect(lineStateForScreenRow(presenter, 7).blockDecorations).toEqual [blockDecoration3] - expect(lineStateForScreenRow(presenter, 8).blockDecorations).toEqual [] - expect(lineStateForScreenRow(presenter, 9).blockDecorations).toEqual [] - expect(lineStateForScreenRow(presenter, 10).blockDecorations).toEqual [] - expect(lineStateForScreenRow(presenter, 11).blockDecorations).toEqual [] - expect(lineStateForScreenRow(presenter, 12).blockDecorations).toEqual [] + expect(lineStateForScreenRow(presenter, 0).hasBlockDecorations).toBe(true) + expect(lineStateForScreenRow(presenter, 1).hasBlockDecorations).toBe(false) + expect(lineStateForScreenRow(presenter, 2).hasBlockDecorations).toBe(false) + expect(lineStateForScreenRow(presenter, 3).hasBlockDecorations).toBe(true) + expect(lineStateForScreenRow(presenter, 4).hasBlockDecorations).toBe(false) + expect(lineStateForScreenRow(presenter, 5).hasBlockDecorations).toBe(false) + expect(lineStateForScreenRow(presenter, 6).hasBlockDecorations).toBe(false) + expect(lineStateForScreenRow(presenter, 7).hasBlockDecorations).toBe(true) + expect(lineStateForScreenRow(presenter, 8).hasBlockDecorations).toBe(false) + expect(lineStateForScreenRow(presenter, 9).hasBlockDecorations).toBe(false) + expect(lineStateForScreenRow(presenter, 10).hasBlockDecorations).toBe(false) + expect(lineStateForScreenRow(presenter, 11).hasBlockDecorations).toBe(false) + expect(lineStateForScreenRow(presenter, 12).hasBlockDecorations).toBe(false) waitsForStateToUpdate presenter, -> blockDecoration1.getMarker().setHeadBufferPosition([1, 0]) @@ -1299,38 +1299,38 @@ describe "TextEditorPresenter", -> blockDecoration3.getMarker().setHeadBufferPosition([9, 0]) runs -> - expect(lineStateForScreenRow(presenter, 0).blockDecorations).toEqual [] - expect(lineStateForScreenRow(presenter, 1).blockDecorations).toEqual [blockDecoration1] - expect(lineStateForScreenRow(presenter, 2).blockDecorations).toEqual [] - expect(lineStateForScreenRow(presenter, 3).blockDecorations).toEqual [] - expect(lineStateForScreenRow(presenter, 4).blockDecorations).toEqual [] - expect(lineStateForScreenRow(presenter, 5).blockDecorations).toEqual [blockDecoration2] - expect(lineStateForScreenRow(presenter, 6).blockDecorations).toEqual [] - expect(lineStateForScreenRow(presenter, 7).blockDecorations).toEqual [] - expect(lineStateForScreenRow(presenter, 8).blockDecorations).toEqual [] - expect(lineStateForScreenRow(presenter, 9).blockDecorations).toEqual [blockDecoration3] - expect(lineStateForScreenRow(presenter, 10).blockDecorations).toEqual [] - expect(lineStateForScreenRow(presenter, 11).blockDecorations).toEqual [] - expect(lineStateForScreenRow(presenter, 12).blockDecorations).toEqual [] + expect(lineStateForScreenRow(presenter, 0).hasBlockDecorations).toBe(false) + expect(lineStateForScreenRow(presenter, 1).hasBlockDecorations).toBe(true) + expect(lineStateForScreenRow(presenter, 2).hasBlockDecorations).toBe(false) + expect(lineStateForScreenRow(presenter, 3).hasBlockDecorations).toBe(false) + expect(lineStateForScreenRow(presenter, 4).hasBlockDecorations).toBe(false) + expect(lineStateForScreenRow(presenter, 5).hasBlockDecorations).toBe(true) + expect(lineStateForScreenRow(presenter, 6).hasBlockDecorations).toBe(false) + expect(lineStateForScreenRow(presenter, 7).hasBlockDecorations).toBe(false) + expect(lineStateForScreenRow(presenter, 8).hasBlockDecorations).toBe(false) + expect(lineStateForScreenRow(presenter, 9).hasBlockDecorations).toBe(true) + expect(lineStateForScreenRow(presenter, 10).hasBlockDecorations).toBe(false) + expect(lineStateForScreenRow(presenter, 11).hasBlockDecorations).toBe(false) + expect(lineStateForScreenRow(presenter, 12).hasBlockDecorations).toBe(false) waitsForStateToUpdate presenter, -> blockDecoration1.destroy() blockDecoration3.destroy() runs -> - expect(lineStateForScreenRow(presenter, 0).blockDecorations).toEqual [] - expect(lineStateForScreenRow(presenter, 1).blockDecorations).toEqual [] - expect(lineStateForScreenRow(presenter, 2).blockDecorations).toEqual [] - expect(lineStateForScreenRow(presenter, 3).blockDecorations).toEqual [] - expect(lineStateForScreenRow(presenter, 4).blockDecorations).toEqual [] - expect(lineStateForScreenRow(presenter, 5).blockDecorations).toEqual [blockDecoration2] - expect(lineStateForScreenRow(presenter, 6).blockDecorations).toEqual [] - expect(lineStateForScreenRow(presenter, 7).blockDecorations).toEqual [] - expect(lineStateForScreenRow(presenter, 8).blockDecorations).toEqual [] - expect(lineStateForScreenRow(presenter, 9).blockDecorations).toEqual [] - expect(lineStateForScreenRow(presenter, 10).blockDecorations).toEqual [] - expect(lineStateForScreenRow(presenter, 11).blockDecorations).toEqual [] - expect(lineStateForScreenRow(presenter, 12).blockDecorations).toEqual [] + expect(lineStateForScreenRow(presenter, 0).hasBlockDecorations).toBe(false) + expect(lineStateForScreenRow(presenter, 1).hasBlockDecorations).toBe(false) + expect(lineStateForScreenRow(presenter, 2).hasBlockDecorations).toBe(false) + expect(lineStateForScreenRow(presenter, 3).hasBlockDecorations).toBe(false) + expect(lineStateForScreenRow(presenter, 4).hasBlockDecorations).toBe(false) + expect(lineStateForScreenRow(presenter, 5).hasBlockDecorations).toBe(true) + expect(lineStateForScreenRow(presenter, 6).hasBlockDecorations).toBe(false) + expect(lineStateForScreenRow(presenter, 7).hasBlockDecorations).toBe(false) + expect(lineStateForScreenRow(presenter, 8).hasBlockDecorations).toBe(false) + expect(lineStateForScreenRow(presenter, 9).hasBlockDecorations).toBe(false) + expect(lineStateForScreenRow(presenter, 10).hasBlockDecorations).toBe(false) + expect(lineStateForScreenRow(presenter, 11).hasBlockDecorations).toBe(false) + expect(lineStateForScreenRow(presenter, 12).hasBlockDecorations).toBe(false) describe ".decorationClasses", -> it "adds decoration classes to the relevant line state objects, both initially and when decorations change", -> @@ -2563,8 +2563,8 @@ describe "TextEditorPresenter", -> expectValues lineNumberStateForScreenRow(presenter, 6), {screenRow: 6, bufferRow: 5, softWrapped: false} expectValues lineNumberStateForScreenRow(presenter, 7), {screenRow: 7, bufferRow: 6, softWrapped: false} - describe ".blockDecorations", -> - it "adds block decorations' height to the relevant line number state objects, both initially and when decorations change", -> + describe ".blockDecorationsHeight", -> + it "adds the sum of all block decorations' heights to the relevant line number state objects, both initially and when decorations change", -> blockDecoration1 = editor.addBlockDecorationForScreenRow(0) presenter = buildPresenter() blockDecoration2 = editor.addBlockDecorationForScreenRow(3) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 869b7914b..0e802a0f7 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -485,11 +485,12 @@ class TextEditorPresenter throw new Error("No line exists for row #{screenRow}. Last screen row: #{@model.getLastScreenRow()}") visibleLineIds[line.id] = true + blockDecorations = this.blockDecorationsPresenter.getDecorationsByScreenRow(screenRow) if tileState.lines.hasOwnProperty(line.id) lineState = tileState.lines[line.id] lineState.screenRow = screenRow lineState.decorationClasses = @lineDecorationClassesForRow(screenRow) - lineState.blockDecorations = Array.from(this.blockDecorationsPresenter.getDecorationsByScreenRow(screenRow)) + lineState.hasBlockDecorations = blockDecorations.size isnt 0 else tileState.lines[line.id] = screenRow: screenRow @@ -506,7 +507,7 @@ class TextEditorPresenter tabLength: line.tabLength fold: line.fold decorationClasses: @lineDecorationClassesForRow(screenRow) - blockDecorations: Array.from(this.blockDecorationsPresenter.getDecorationsByScreenRow(screenRow)) + hasBlockDecorations: blockDecorations.size isnt 0 for id, line of tileState.lines delete tileState.lines[id] unless visibleLineIds.hasOwnProperty(id) From 526a97562ec3c597f9fe58d69af24d48f6d81afc Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 30 Nov 2015 15:05:15 +0100 Subject: [PATCH 116/502] Include block decorations as a separate object on presenter's state --- spec/text-editor-presenter-spec.coffee | 66 ++++++++++++++++++++++++++ src/block-decorations-presenter.js | 12 +++-- src/text-editor-presenter.coffee | 10 +++- 3 files changed, 83 insertions(+), 5 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index b940fd06d..903c6ebf7 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -2064,6 +2064,72 @@ describe "TextEditorPresenter", -> flashCount: 2 } + describe ".blockDecorations", -> + stateForBlockDecoration = (presenter, decoration) -> + presenter.getState().content.blockDecorations[decoration.id] + + it "contains state for block decorations, indicating the screen row they belong to both initially and when their markers move", -> + item = {} + blockDecoration1 = editor.addBlockDecorationForScreenRow(0, item) + blockDecoration2 = editor.addBlockDecorationForScreenRow(4, item) + blockDecoration3 = editor.addBlockDecorationForScreenRow(4, item) + blockDecoration4 = editor.addBlockDecorationForScreenRow(10, item) + presenter = buildPresenter(explicitHeight: 30, scrollTop: 0) + + expectValues stateForBlockDecoration(presenter, blockDecoration1), { + decoration: blockDecoration1 + screenRow: 0 + } + expectValues stateForBlockDecoration(presenter, blockDecoration2), { + decoration: blockDecoration2 + screenRow: 4 + } + expectValues stateForBlockDecoration(presenter, blockDecoration3), { + decoration: blockDecoration3 + screenRow: 4 + } + expectValues stateForBlockDecoration(presenter, blockDecoration4), { + decoration: blockDecoration4 + screenRow: 10 + } + + waitsForStateToUpdate presenter, -> + editor.getBuffer().insert([0, 0], 'Hello world \n\n\n\n') + + runs -> + expectValues stateForBlockDecoration(presenter, blockDecoration1), { + decoration: blockDecoration1 + screenRow: 4 + } + expectValues stateForBlockDecoration(presenter, blockDecoration2), { + decoration: blockDecoration2 + screenRow: 8 + } + expectValues stateForBlockDecoration(presenter, blockDecoration3), { + decoration: blockDecoration3 + screenRow: 8 + } + expectValues stateForBlockDecoration(presenter, blockDecoration4), { + decoration: blockDecoration4 + screenRow: 14 + } + + waitsForStateToUpdate presenter, -> + blockDecoration2.destroy() + blockDecoration4.destroy() + + runs -> + expectValues stateForBlockDecoration(presenter, blockDecoration1), { + decoration: blockDecoration1 + screenRow: 4 + } + expect(stateForBlockDecoration(presenter, blockDecoration2)).toBeUndefined() + expectValues stateForBlockDecoration(presenter, blockDecoration3), { + decoration: blockDecoration3 + screenRow: 8 + } + expect(stateForBlockDecoration(presenter, blockDecoration4)).toBeUndefined() + describe ".overlays", -> [item] = [] stateForOverlay = (presenter, decoration) -> diff --git a/src/block-decorations-presenter.js b/src/block-decorations-presenter.js index 3a669e2f9..762372b15 100644 --- a/src/block-decorations-presenter.js +++ b/src/block-decorations-presenter.js @@ -90,7 +90,7 @@ class BlockDecorationsPresenter { } addDecorationToScreenRow (screenRow, decoration) { - let decorations = this.getDecorationsByScreenRow(screenRow) + let decorations = this.decorationsForScreenRow(screenRow) if (!decorations.has(decoration)) { decorations.add(decoration) this.screenRowByDecoration.set(decoration, screenRow) @@ -103,14 +103,14 @@ class BlockDecorationsPresenter { return } - let decorations = this.getDecorationsByScreenRow(screenRow) + let decorations = this.decorationsForScreenRow(screenRow) if (decorations.has(decoration)) { decorations.delete(decoration) this.recalculateScreenRowHeight(screenRow) } } - getDecorationsByScreenRow (screenRow) { + decorationsForScreenRow (screenRow) { if (!this.decorationsByScreenRow.has(screenRow)) { this.decorationsByScreenRow.set(screenRow, new Set()) } @@ -118,13 +118,17 @@ class BlockDecorationsPresenter { return this.decorationsByScreenRow.get(screenRow) } + getAllDecorationsByScreenRow () { + return this.decorationsByScreenRow + } + getDecorationDimensions (decoration) { return this.dimensionsByDecoration.get(decoration) || {width: 0, height: 0} } recalculateScreenRowHeight (screenRow) { let height = 0 - for (let decoration of this.getDecorationsByScreenRow(screenRow)) { + for (let decoration of this.decorationsForScreenRow(screenRow)) { height += this.getDecorationDimensions(decoration).height } this.heightByScreenRow.set(screenRow, height) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 0e802a0f7..8fd0a790a 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -76,6 +76,7 @@ class TextEditorPresenter @blockDecorationsPresenter.update() + @updateBlockDecorationsState() @updateVerticalDimensions() @updateScrollbarDimensions() @@ -485,7 +486,7 @@ class TextEditorPresenter throw new Error("No line exists for row #{screenRow}. Last screen row: #{@model.getLastScreenRow()}") visibleLineIds[line.id] = true - blockDecorations = this.blockDecorationsPresenter.getDecorationsByScreenRow(screenRow) + blockDecorations = this.blockDecorationsPresenter.decorationsForScreenRow(screenRow) if tileState.lines.hasOwnProperty(line.id) lineState = tileState.lines[line.id] lineState.screenRow = screenRow @@ -1205,6 +1206,13 @@ class TextEditorPresenter return unless 0 <= @startRow <= @endRow <= Infinity @decorations = @model.decorationsStateForScreenRowRange(@startRow, @endRow - 1) + updateBlockDecorationsState: -> + @state.content.blockDecorations = {} + + @blockDecorationsPresenter.getAllDecorationsByScreenRow().forEach (decorations, screenRow) => + decorations.forEach (decoration) => + @state.content.blockDecorations[decoration.id] = {decoration, screenRow} + updateLineDecorations: -> @lineDecorationsByScreenRow = {} @lineNumberDecorationsByScreenRow = {} From da1fd69a1f2c4a60e961693ebfe6e49463950c0e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 30 Nov 2015 15:43:39 +0100 Subject: [PATCH 117/502] Start implementing BlockDecorationsComponent --- src/block-decorations-component.coffee | 45 ++++++++++++++++++++++++++ src/text-editor-component.coffee | 5 +++ src/text-editor.coffee | 4 +-- 3 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 src/block-decorations-component.coffee diff --git a/src/block-decorations-component.coffee b/src/block-decorations-component.coffee new file mode 100644 index 000000000..4f3bbae67 --- /dev/null +++ b/src/block-decorations-component.coffee @@ -0,0 +1,45 @@ +cloneObject = (object) -> + clone = {} + clone[key] = value for key, value of object + clone + +module.exports = +class BlockDecorationsComponent + constructor: (@views, @domElementPool) -> + @domNode = @domElementPool.buildElement("div") + @newState = null + @oldState = null + @blockDecorationNodesById = {} + + getDomNode: -> + @domNode + + updateSync: (state) -> + @newState = state.content + @oldState ?= {blockDecorations: {}} + + for id, blockDecorationState of @newState.blockDecorations + if @oldState.blockDecorations.hasOwnProperty(id) + @updateBlockDecorationNode(id) + else + @createAndAppendBlockDecorationNode(id) + + @oldState.blockDecorations[id] = cloneObject(blockDecorationState) + + createAndAppendBlockDecorationNode: (id) -> + blockDecorationState = @newState.blockDecorations[id] + blockDecorationNode = @views.getView(blockDecorationState.decoration.getProperties().item) + blockDecorationNode.classList.add("block-decoration-row-#{blockDecorationState.screenRow}") + + @domNode.appendChild(blockDecorationNode) + + @blockDecorationNodesById[id] = blockDecorationNode + + updateBlockDecorationNode: (id) -> + newBlockDecorationState = @newState.blockDecorations[id] + oldBlockDecorationState = @oldState.blockDecorations[id] + blockDecorationNode = @blockDecorationNodesById[id] + + if newBlockDecorationState.screenRow isnt oldBlockDecorationState.screenRow + blockDecorationNode.classList.remove("block-decoration-row-#{oldBlockDecorationState.screenRow}") + blockDecorationNode.classList.add("block-decoration-row-#{newBlockDecorationState.screenRow}") diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 430b0c0fd..b20e00751 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -13,6 +13,7 @@ ScrollbarCornerComponent = require './scrollbar-corner-component' OverlayManager = require './overlay-manager' DOMElementPool = require './dom-element-pool' LinesYardstick = require './lines-yardstick' +BlockDecorationsComponent = require './block-decorations-component' module.exports = class TextEditorComponent @@ -59,6 +60,7 @@ class TextEditorComponent @presenter.onDidUpdateState(@requestUpdate) @domElementPool = new DOMElementPool + @blockDecorationsComponent = new BlockDecorationsComponent(@views, @domElementPool) @domNode = document.createElement('div') if @useShadowDOM @@ -68,9 +70,11 @@ class TextEditorComponent insertionPoint.setAttribute('select', 'atom-overlay') @domNode.appendChild(insertionPoint) @overlayManager = new OverlayManager(@presenter, @hostElement, @views) + @hostElement.appendChild(@blockDecorationsComponent.getDomNode()) else @domNode.classList.add('editor-contents') @overlayManager = new OverlayManager(@presenter, @domNode, @views) + @domNode.appendChild(@blockDecorationsComponent.getDomNode()) @scrollViewNode = document.createElement('div') @scrollViewNode.classList.add('scroll-view') @@ -156,6 +160,7 @@ class TextEditorComponent @hiddenInputComponent.updateSync(@newState) @linesComponent.updateSync(@newState) + @blockDecorationsComponent.updateSync(@newState) @horizontalScrollbarComponent.updateSync(@newState) @verticalScrollbarComponent.updateSync(@newState) @scrollbarCornerComponent.updateSync(@newState) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index f88c9149c..136756538 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -1398,11 +1398,11 @@ class TextEditor extends Model Section: Decorations ### - addBlockDecorationForScreenRow: (screenRow, element) -> + addBlockDecorationForScreenRow: (screenRow, item) -> @decorateMarker( @markScreenPosition([screenRow, 0], invalidate: "never"), type: "block", - element: element + item: item ) # Essential: Add a decoration that tracks a {TextEditorMarker}. When the From e1aa23b92d1f46bd525643abcdc5595f4f31ad10 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 30 Nov 2015 11:47:06 -0500 Subject: [PATCH 118/502] Const and comparison fixes. h/t @YuriSolovyov --- src/git-repository-async.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 1969a9c6b..4051f5df6 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -346,14 +346,9 @@ module.exports = class GitRepositoryAsync { return this.repoPromise .then(repo => repo.openIndex()) .then(index => { - let entry = index.getByPath(_path) - let submoduleMode = 57344 // TODO compose this from libgit2 constants - - if (entry.mode === submoduleMode) { - return true - } else { - return false - } + const entry = index.getByPath(_path) + const submoduleMode = 57344 // TODO compose this from libgit2 constants + return entry.mode === submoduleMode }) } } From f1cb69ccdeb09735fff8d9cabecbb8239bdd5c95 Mon Sep 17 00:00:00 2001 From: Dave Rael Date: Mon, 30 Nov 2015 18:20:40 -0600 Subject: [PATCH 119/502] :art: Cleanup tabs vs spaces and debugging line --- resources/win/atom.cmd | 6 +++--- resources/win/atom.sh | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/resources/win/atom.cmd b/resources/win/atom.cmd index 0463928d8..c9bfdd5ba 100644 --- a/resources/win/atom.cmd +++ b/resources/win/atom.cmd @@ -34,7 +34,7 @@ IF "%EXPECT_OUTPUT%"=="YES" ( IF "%WAIT%"=="YES" ( "%~dp0\..\..\atom.exe" --pid=%PID% %* rem If the wait flag is set, don't exit this process until Atom tells it to. - goto waitLoop + goto waitLoop ) ELSE ( "%~dp0\..\..\atom.exe" %* @@ -46,7 +46,7 @@ IF "%EXPECT_OUTPUT%"=="YES" ( goto end :waitLoop - sleep 1 - goto waitLoop + sleep 1 + goto waitLoop :end diff --git a/resources/win/atom.sh b/resources/win/atom.sh index 4fd5a3106..0eaf193c0 100644 --- a/resources/win/atom.sh +++ b/resources/win/atom.sh @@ -33,7 +33,6 @@ directory=$(dirname "$0") WINPS=`ps | grep -i $$` PID=`echo $WINPS | cut -d' ' -f 4` -echo $PID if [ $EXPECT_OUTPUT ]; then export ELECTRON_ENABLE_LOGGING=1 From 47b16c513c9fdace8c82726276482ef93f5e0f3b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 1 Dec 2015 09:30:38 +0100 Subject: [PATCH 120/502] Make sure cursors are updated with respect to block decorations --- spec/fake-lines-yardstick.coffee | 5 ++++- spec/text-editor-presenter-spec.coffee | 29 ++++++++++++++++++++++++++ src/lines-yardstick.coffee | 5 ++++- src/text-editor-presenter.coffee | 14 ++++++++++++- 4 files changed, 50 insertions(+), 3 deletions(-) diff --git a/spec/fake-lines-yardstick.coffee b/spec/fake-lines-yardstick.coffee index 9dfeb7da4..e6d1f4f53 100644 --- a/spec/fake-lines-yardstick.coffee +++ b/spec/fake-lines-yardstick.coffee @@ -31,7 +31,7 @@ class FakeLinesYardstick targetColumn = screenPosition.column baseCharacterWidth = @model.getDefaultCharWidth() - top = @topPixelPositionForRow(targetRow) + top = @bottomPixelPositionForRow(targetRow) left = 0 column = 0 @@ -80,6 +80,9 @@ class FakeLinesYardstick top += @presenter.getScreenRowHeight(row) top + bottomPixelPositionForRow: (targetRow) -> + @topPixelPositionForRow(targetRow + 1) - @model.getLineHeightInPixels() + topPixelPositionForRows: (startRow, endRow, step) -> results = {} top = 0 diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 903c6ebf7..83f0d8236 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -1527,6 +1527,35 @@ describe "TextEditorPresenter", -> presenter.setHorizontalScrollbarHeight(10) expect(presenter.getState().content.cursors).not.toEqual({}) + it "updates when block decorations change", -> + editor.setSelectedBufferRanges([ + [[1, 2], [1, 2]], + [[2, 4], [2, 4]], + [[3, 4], [3, 5]] + [[5, 12], [5, 12]], + [[8, 4], [8, 4]] + ]) + presenter = buildPresenter(explicitHeight: 80, scrollTop: 0) + + expect(stateForCursor(presenter, 0)).toEqual {top: 10, left: 2 * 10, width: 10, height: 10} + expect(stateForCursor(presenter, 1)).toEqual {top: 20, left: 4 * 10, width: 10, height: 10} + expect(stateForCursor(presenter, 2)).toBeUndefined() + expect(stateForCursor(presenter, 3)).toEqual {top: 5 * 10, left: 12 * 10, width: 10, height: 10} + expect(stateForCursor(presenter, 4)).toEqual {top: 8 * 10, left: 4 * 10, width: 10, height: 10} + + blockDecoration1 = editor.addBlockDecorationForScreenRow(0) + blockDecoration2 = editor.addBlockDecorationForScreenRow(1) + presenter.setBlockDecorationDimensions(blockDecoration1, 0, 30) + presenter.setBlockDecorationDimensions(blockDecoration2, 0, 10) + + waitsForStateToUpdate presenter + runs -> + expect(stateForCursor(presenter, 0)).toEqual {top: 50, left: 2 * 10, width: 10, height: 10} + expect(stateForCursor(presenter, 1)).toEqual {top: 60, left: 4 * 10, width: 10, height: 10} + expect(stateForCursor(presenter, 2)).toBeUndefined() + expect(stateForCursor(presenter, 3)).toBeUndefined() + expect(stateForCursor(presenter, 4)).toBeUndefined() + it "updates when ::scrollTop changes", -> editor.setSelectedBufferRanges([ [[1, 2], [1, 2]], diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index d827d7bce..bb2c7faba 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -90,7 +90,7 @@ class LinesYardstick @prepareScreenRowsForMeasurement([targetRow]) unless measureVisibleLinesOnly - top = @topPixelPositionForRow(targetRow) + top = @bottomPixelPositionForRow(targetRow) left = @leftPixelPositionForScreenPosition(targetRow, targetColumn) @clearScreenRowsForMeasurement() unless measureVisibleLinesOnly @@ -192,6 +192,9 @@ class LinesYardstick top += @presenter.getScreenRowHeight(row) top + bottomPixelPositionForRow: (targetRow) -> + @topPixelPositionForRow(targetRow + 1) - @model.getLineHeightInPixels() + topPixelPositionForRows: (startRow, endRow, step) -> results = {} top = 0 diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 8fd0a790a..91ff18012 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -199,7 +199,19 @@ class TextEditorPresenter @emitDidUpdateState() @disposables.add @blockDecorationsPresenter.onDidUpdateState => + @shouldUpdateHeightState = true @shouldUpdateVerticalScrollState = true + @shouldUpdateHorizontalScrollState = true + @shouldUpdateScrollbarsState = true + @shouldUpdateContentState = true + @shouldUpdateDecorations = true + @shouldUpdateCursorsState = true + @shouldUpdateLinesState = true + @shouldUpdateLineNumberGutterState = true + @shouldUpdateLineNumbersState = true + @shouldUpdateGutterOrderState = true + @shouldUpdateCustomGutterDecorationState = true + @emitDidUpdateState() @disposables.add @model.onDidChangeGrammar(@didChangeGrammar.bind(this)) @@ -1406,7 +1418,7 @@ class TextEditorPresenter @emitDidUpdateState() - setBlockDecorationDimensions: (decoration, width, height) -> + setBlockDecorationDimensions: -> @blockDecorationsPresenter.setDimensionsForDecoration(arguments...) observeCursor: (cursor) -> From d24290357a647c75e002d1626f17f39e5e6bfa9e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 1 Dec 2015 13:36:23 +0100 Subject: [PATCH 121/502] Implement block decorations in the components land --- spec/text-editor-component-spec.js | 104 +++++++++++++++++++++++++ src/block-decorations-component.coffee | 23 ++++-- src/line-numbers-tile-component.coffee | 7 +- src/lines-tile-component.coffee | 37 +++++++++ src/text-editor-component.coffee | 6 +- 5 files changed, 167 insertions(+), 10 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 609d20291..18ad030c0 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -1651,6 +1651,110 @@ describe('TextEditorComponent', function () { }) }) + describe('block decorations rendering', function () { + function createBlockDecorationForScreenRowWith(screenRow, {className}) { + let item = document.createElement("div") + item.className = className || "" + let blockDecoration = editor.addBlockDecorationForScreenRow(screenRow, item) + return [item, blockDecoration] + } + + afterEach(function () { + atom.themes.removeStylesheet('test') + }) + + it("renders all the editor's block decorations, inserting them in the appropriate spots between lines", async function () { + wrapperNode.style.height = 9 * lineHeightInPixels + 'px' + component.measureDimensions() + await nextViewUpdatePromise() + + let [item1, blockDecoration1] = createBlockDecorationForScreenRowWith(0, {className: "decoration-1"}) + let [item2, blockDecoration2] = createBlockDecorationForScreenRowWith(2, {className: "decoration-2"}) + let [item3, blockDecoration3] = createBlockDecorationForScreenRowWith(4, {className: "decoration-3"}) + let [item4, blockDecoration4] = createBlockDecorationForScreenRowWith(7, {className: "decoration-4"}) + + atom.styles.addStyleSheet ` + atom-text-editor .decoration-1 { width: 30px; height: 80px; } + atom-text-editor .decoration-2 { width: 30px; height: 40px; } + atom-text-editor .decoration-3 { width: 30px; height: 100px; } + atom-text-editor .decoration-4 { width: 30px; height: 120px; } + ` + + await nextViewUpdatePromise() + + expect(component.getDomNode().querySelectorAll(".line").length).toBe(7) + + expect(component.tileNodesForLines()[0].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 80 + 40 + "px") + expect(component.tileNodesForLines()[0].style.webkitTransform).toBe("translate3d(0px, 0px, 0px)") + expect(component.tileNodesForLines()[1].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 100 + "px") + expect(component.tileNodesForLines()[1].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight}px, 0px)`) + expect(component.tileNodesForLines()[2].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 120 + "px") + expect(component.tileNodesForLines()[2].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight + component.tileNodesForLines()[1].offsetHeight}px, 0px)`) + + expect(component.getTopmostDOMNode().querySelector(".decoration-1")).toBe(item1) + expect(component.getTopmostDOMNode().querySelector(".decoration-2")).toBe(item2) + expect(component.getTopmostDOMNode().querySelector(".decoration-3")).toBe(item3) + expect(component.getTopmostDOMNode().querySelector(".decoration-4")).toBe(item4) + + expect(item1.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 0) + expect(item2.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 2 + 80) + expect(item3.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 4 + 80 + 40) + expect(item4.getBoundingClientRect().height).toBe(0) // hidden + + editor.setCursorScreenPosition([0, 0]) + editor.insertNewline() + blockDecoration1.destroy() + + await nextViewUpdatePromise() + + expect(component.getDomNode().querySelectorAll(".line").length).toBe(9) + + expect(component.tileNodesForLines()[0].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + "px") + expect(component.tileNodesForLines()[0].style.webkitTransform).toBe("translate3d(0px, 0px, 0px)") + expect(component.tileNodesForLines()[1].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 100 + 40 + "px") + expect(component.tileNodesForLines()[1].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight}px, 0px)`) + expect(component.tileNodesForLines()[2].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 120 + "px") + expect(component.tileNodesForLines()[2].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight + component.tileNodesForLines()[1].offsetHeight}px, 0px)`) + + expect(component.getTopmostDOMNode().querySelector(".decoration-1")).toBeNull() + expect(component.getTopmostDOMNode().querySelector(".decoration-2")).toBe(item2) + expect(component.getTopmostDOMNode().querySelector(".decoration-3")).toBe(item3) + expect(component.getTopmostDOMNode().querySelector(".decoration-4")).toBe(item4) + + expect(item1.getBoundingClientRect().height).toBe(0) // hidden + expect(item2.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 3) + expect(item3.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 5 + 40) + expect(item4.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 8 + 40 + 100) + + await nextViewUpdatePromise() + + atom.styles.addStyleSheet ` + atom-text-editor .decoration-2 { height: 60px !important; } + ` + + await nextViewUpdatePromise() + + expect(component.getDomNode().querySelectorAll(".line").length).toBe(9) + + expect(component.tileNodesForLines()[0].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + "px") + expect(component.tileNodesForLines()[0].style.webkitTransform).toBe("translate3d(0px, 0px, 0px)") + expect(component.tileNodesForLines()[1].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 100 + 60 + "px") + expect(component.tileNodesForLines()[1].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight}px, 0px)`) + expect(component.tileNodesForLines()[2].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 120 + "px") + expect(component.tileNodesForLines()[2].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight + component.tileNodesForLines()[1].offsetHeight}px, 0px)`) + + expect(component.getTopmostDOMNode().querySelector(".decoration-1")).toBeNull() + expect(component.getTopmostDOMNode().querySelector(".decoration-2")).toBe(item2) + expect(component.getTopmostDOMNode().querySelector(".decoration-3")).toBe(item3) + expect(component.getTopmostDOMNode().querySelector(".decoration-4")).toBe(item4) + + expect(item1.getBoundingClientRect().height).toBe(0) // hidden + expect(item2.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 3) + expect(item3.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 5 + 60) + expect(item4.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 8 + 60 + 100) + }) + }) + describe('highlight decoration rendering', function () { let decoration, marker, scrollViewClientLeft diff --git a/src/block-decorations-component.coffee b/src/block-decorations-component.coffee index 4f3bbae67..0f9270660 100644 --- a/src/block-decorations-component.coffee +++ b/src/block-decorations-component.coffee @@ -5,19 +5,21 @@ cloneObject = (object) -> module.exports = class BlockDecorationsComponent - constructor: (@views, @domElementPool) -> - @domNode = @domElementPool.buildElement("div") + constructor: (@container, @views, @presenter, @domElementPool) -> @newState = null @oldState = null @blockDecorationNodesById = {} - getDomNode: -> - @domNode - updateSync: (state) -> @newState = state.content @oldState ?= {blockDecorations: {}} + for id, blockDecorationState of @oldState.blockDecorations + unless @newState.blockDecorations.hasOwnProperty(id) + @blockDecorationNodesById[id].remove() + delete @blockDecorationNodesById[id] + delete @oldState.blockDecorations[id] + for id, blockDecorationState of @newState.blockDecorations if @oldState.blockDecorations.hasOwnProperty(id) @updateBlockDecorationNode(id) @@ -26,12 +28,21 @@ class BlockDecorationsComponent @oldState.blockDecorations[id] = cloneObject(blockDecorationState) + measureBlockDecorations: -> + for decorationId, blockDecorationNode of @blockDecorationNodesById + decoration = @newState.blockDecorations[decorationId].decoration + @presenter.setBlockDecorationDimensions( + decoration, + blockDecorationNode.offsetWidth, + blockDecorationNode.offsetHeight + ) + createAndAppendBlockDecorationNode: (id) -> blockDecorationState = @newState.blockDecorations[id] blockDecorationNode = @views.getView(blockDecorationState.decoration.getProperties().item) blockDecorationNode.classList.add("block-decoration-row-#{blockDecorationState.screenRow}") - @domNode.appendChild(blockDecorationNode) + @container.appendChild(blockDecorationNode) @blockDecorationNodesById[id] = blockDecorationNode diff --git a/src/line-numbers-tile-component.coffee b/src/line-numbers-tile-component.coffee index 32dbca0a2..30f13fff2 100644 --- a/src/line-numbers-tile-component.coffee +++ b/src/line-numbers-tile-component.coffee @@ -96,12 +96,13 @@ class LineNumbersTileComponent screenRowForNode: (node) -> parseInt(node.dataset.screenRow) buildLineNumberNode: (lineNumberState) -> - {screenRow, bufferRow, softWrapped, top, decorationClasses, zIndex} = lineNumberState + {screenRow, bufferRow, softWrapped, top, decorationClasses, zIndex, blockDecorationsHeight} = lineNumberState className = @buildLineNumberClassName(lineNumberState) lineNumberNode = @domElementPool.buildElement("div", className) lineNumberNode.dataset.screenRow = screenRow lineNumberNode.dataset.bufferRow = bufferRow + lineNumberNode.style.marginTop = blockDecorationsHeight + "px" @setLineNumberInnerNodes(bufferRow, softWrapped, lineNumberNode) lineNumberNode @@ -139,6 +140,10 @@ class LineNumbersTileComponent oldLineNumberState.screenRow = newLineNumberState.screenRow oldLineNumberState.bufferRow = newLineNumberState.bufferRow + unless oldLineNumberState.blockDecorationsHeight is newLineNumberState.blockDecorationsHeight + node.style.marginTop = newLineNumberState.blockDecorationsHeight + "px" + oldLineNumberState.blockDecorationsHeight = newLineNumberState.blockDecorationsHeight + buildLineNumberClassName: ({bufferRow, foldable, decorationClasses, softWrapped}) -> className = "line-number" className += " " + decorationClasses.join(' ') if decorationClasses? diff --git a/src/lines-tile-component.coffee b/src/lines-tile-component.coffee index 6b4ac80ba..f4b0dbab5 100644 --- a/src/lines-tile-component.coffee +++ b/src/lines-tile-component.coffee @@ -20,6 +20,7 @@ class LinesTileComponent @screenRowsByLineId = {} @lineIdsByScreenRow = {} @textNodesByLineId = {} + @insertionPointsByLineId = {} @domNode = @domElementPool.buildElement("div") @domNode.style.position = "absolute" @domNode.style.display = "block" @@ -80,6 +81,8 @@ class LinesTileComponent removeLineNode: (id) -> @domElementPool.freeElementAndDescendants(@lineNodesByLineId[id]) + @removeBlockDecorationInsertionPoint(id) + delete @lineNodesByLineId[id] delete @textNodesByLineId[id] delete @lineIdsByScreenRow[@screenRowsByLineId[id]] @@ -116,6 +119,31 @@ class LinesTileComponent else @domNode.appendChild(lineNode) + @insertBlockDecorationInsertionPoint(id) + + removeBlockDecorationInsertionPoint: (id) -> + if insertionPoint = @insertionPointsByLineId[id] + @domElementPool.freeElementAndDescendants(insertionPoint) + delete @insertionPointsByLineId[id] + + insertBlockDecorationInsertionPoint: (id) -> + {hasBlockDecorations} = @newTileState.lines[id] + + if hasBlockDecorations + lineNode = @lineNodesByLineId[id] + insertionPoint = @domElementPool.buildElement("content") + @domNode.insertBefore(insertionPoint, lineNode) + @insertionPointsByLineId[id] = insertionPoint + + @updateBlockDecorationInsertionPoint(id) + + updateBlockDecorationInsertionPoint: (id) -> + {screenRow} = @newTileState.lines[id] + + if insertionPoint = @insertionPointsByLineId[id] + insertionPoint.dataset.screenRow = screenRow + insertionPoint.setAttribute("select", ".block-decoration-row-#{screenRow}") + findNodeNextTo: (node) -> for nextNode, index in @domNode.children continue if index is 0 # skips highlights node @@ -336,7 +364,16 @@ class LinesTileComponent oldLineState.decorationClasses = newLineState.decorationClasses + if not oldLineState.hasBlockDecorations and newLineState.hasBlockDecorations + @insertBlockDecorationInsertionPoint(id) + else if oldLineState.hasBlockDecorations and not newLineState.hasBlockDecorations + @removeBlockDecorationInsertionPoint(id) + + oldLineState.hasBlockDecorations = newLineState.hasBlockDecorations + if newLineState.screenRow isnt oldLineState.screenRow + @updateBlockDecorationInsertionPoint(id) + lineNode.dataset.screenRow = newLineState.screenRow oldLineState.screenRow = newLineState.screenRow @lineIdsByScreenRow[newLineState.screenRow] = id diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index b20e00751..faf6a6715 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -60,7 +60,6 @@ class TextEditorComponent @presenter.onDidUpdateState(@requestUpdate) @domElementPool = new DOMElementPool - @blockDecorationsComponent = new BlockDecorationsComponent(@views, @domElementPool) @domNode = document.createElement('div') if @useShadowDOM @@ -70,11 +69,11 @@ class TextEditorComponent insertionPoint.setAttribute('select', 'atom-overlay') @domNode.appendChild(insertionPoint) @overlayManager = new OverlayManager(@presenter, @hostElement, @views) - @hostElement.appendChild(@blockDecorationsComponent.getDomNode()) + @blockDecorationsComponent = new BlockDecorationsComponent(@hostElement, @views, @presenter, @domElementPool) else @domNode.classList.add('editor-contents') @overlayManager = new OverlayManager(@presenter, @domNode, @views) - @domNode.appendChild(@blockDecorationsComponent.getDomNode()) + @blockDecorationsComponent = new BlockDecorationsComponent(@domNode, @views, @presenter, @domElementPool) @scrollViewNode = document.createElement('div') @scrollViewNode.classList.add('scroll-view') @@ -177,6 +176,7 @@ class TextEditorComponent readAfterUpdateSync: => @overlayManager?.measureOverlays() + @blockDecorationsComponent.measureBlockDecorations() mountGutterContainerComponent: -> @gutterContainerComponent = new GutterContainerComponent({@editor, @onLineNumberGutterMouseDown, @domElementPool, @views}) From ee3b65506725e9940efe62f4c5e307d4c63eb4dc Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 1 Dec 2015 12:19:51 -0500 Subject: [PATCH 122/502] git-repository will refresh us on window focus. --- src/git-repository-async.js | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 4051f5df6..eea06011c 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -3,7 +3,7 @@ const fs = require('fs-plus') const Git = require('nodegit') const path = require('path') -const {Emitter, CompositeDisposable, Disposable} = require('event-kit') +const {Emitter, CompositeDisposable} = require('event-kit') const modifiedStatusFlags = Git.Status.STATUS.WT_MODIFIED | Git.Status.STATUS.INDEX_MODIFIED | Git.Status.STATUS.WT_DELETED | Git.Status.STATUS.INDEX_DELETED | Git.Status.STATUS.WT_TYPECHANGE | Git.Status.STATUS.INDEX_TYPECHANGE const newStatusFlags = Git.Status.STATUS.WT_NEW | Git.Status.STATUS.INDEX_NEW @@ -31,18 +31,8 @@ module.exports = class GitRepositoryAsync { this.repoPromise = Git.Repository.open(path) this.isCaseInsensitive = fs.isCaseInsensitive() - let {project, refreshOnWindowFocus} = options + let {project} = options this.project = project - if (refreshOnWindowFocus === undefined) { - refreshOnWindowFocus = true - } - if (refreshOnWindowFocus) { - const onWindowFocus = () => { - this.refreshStatus() - } - window.addEventListener('focus', onWindowFocus) - this.subscriptions.add(new Disposable(() => window.removeEventListener('focus', onWindowFocus))) - } if (this.project) { this.subscriptions.add(this.project.onDidAddBuffer((buffer) => { From c76d0f04382865f922096dae0272ffa2da8b8c18 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 1 Dec 2015 12:22:25 -0500 Subject: [PATCH 123/502] Update the current branch as well. --- src/git-repository-async.js | 45 ++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index eea06011c..3a2982efa 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -120,7 +120,7 @@ module.exports = class GitRepositoryAsync { }).then((statuses) => { let cachedStatus = this.pathStatusCache[relativePath] || 0 let status = statuses[0] ? statuses[0].statusBit() : Git.Status.STATUS.CURRENT - if (status !== cachedStatus) { + if (status !== cachedStatus && this.emitter != null) { this.emitter.emit('did-change-status', {path: _path, pathStatus: status}) } this.pathStatusCache[relativePath] = status @@ -156,26 +156,39 @@ module.exports = class GitRepositoryAsync { }) } + // Get the current branch and update this.branch. + // + // Returns :: Promise + // The branch name. + _refreshBranch () { + return this.repoPromise + .then(repo => repo.getCurrentBranch()) + .then(ref => ref.name()) + .then(branchRef => this.branch = branchRef) + } + // Refreshes the git status. Note: the sync GitRepository class does this with // a separate process, let's see if we can avoid that. refreshStatus () { // TODO add upstream, branch, and submodule tracking - return this.repoPromise.then((repo) => { - return repo.getStatus() - }).then((statuses) => { - // update the status cache - return Promise.all(statuses.map((status) => { - return [status.path(), status.statusBit()] - })).then((statusesByPath) => { - return _.object(statusesByPath) + const status = this.repoPromise + .then(repo => repo.getStatus()) + .then(statuses => { + // update the status cache + return Promise.all(statuses.map(status => [status.path(), status.statusBit()])) + .then(statusesByPath => _.object(statusesByPath)) }) - }).then((newPathStatusCache) => { - if (!_.isEqual(this.pathStatusCache, newPathStatusCache)) { - this.emitter.emit('did-change-statuses') - } - this.pathStatusCache = newPathStatusCache - return newPathStatusCache - }) + .then(newPathStatusCache => { + if (!_.isEqual(this.pathStatusCache, newPathStatusCache) && this.emitter != null) { + this.emitter.emit('did-change-statuses') + } + this.pathStatusCache = newPathStatusCache + return newPathStatusCache + }) + + const branch = this._refreshBranch() + + return Promise.all([status, branch]) } // Section: Private From 64d471600337222ac0a0ff07891140807c1b2471 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 1 Dec 2015 12:47:11 -0500 Subject: [PATCH 124/502] Use modern imports. --- spec/git-repository-async-spec.js | 74 ++++++++++++------------------- src/git-repository-async.js | 10 ++--- 2 files changed, 34 insertions(+), 50 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 7ebcd8210..2e6c1188e 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -1,12 +1,14 @@ 'use babel' -const fs = require('fs-plus') -const path = require('path') -const temp = require('temp') -const Git = require('nodegit') +import fs from 'fs-plus' +import path from 'path' +import temp from 'temp' +import Git from 'nodegit' -const GitRepositoryAsync = require('../src/git-repository-async') -const Project = require('../src/project') +import {it, ffit, fffit, beforeEach, afterEach} from './async-spec-helpers' + +import GitRepositoryAsync from '../src/git-repository-async' +import Project from '../src/project' temp.track() @@ -21,24 +23,6 @@ function copyRepository() { return fs.realpathSync(workingDirPath) } -function asyncIt(name, fn) { - it(name, () => { - waitsForPromise(fn) - }) -} - -function fasyncIt(name, fn) { - fit(name, () => { - waitsForPromise(fn) - }) -} - -function xasyncIt(name, fn) { - xit(name, () => { - waitsForPromise(fn) - }) -} - describe('GitRepositoryAsync-js', () => { let repo @@ -47,7 +31,7 @@ describe('GitRepositoryAsync-js', () => { }) describe('@open(path)', () => { - asyncIt('repo is null when no repository is found', async () => { + it('repo is null when no repository is found', async () => { repo = GitRepositoryAsync.open(path.join(temp.dir, 'nogit.txt')) let threw = false @@ -65,7 +49,7 @@ describe('GitRepositoryAsync-js', () => { describe('.getPath()', () => { xit('returns the repository path for a .git directory path') - asyncIt('returns the repository path for a repository path', async () => { + it('returns the repository path for a repository path', async () => { repo = openFixture('master.git') let repoPath = await repo.getPath() expect(repoPath).toBe(path.join(__dirname, 'fixtures', 'git', 'master.git')) @@ -73,13 +57,13 @@ describe('GitRepositoryAsync-js', () => { }) describe('.isPathIgnored(path)', () => { - asyncIt('resolves true for an ignored path', async () => { + it('resolves true for an ignored path', async () => { repo = openFixture('ignore.git') let ignored = await repo.isPathIgnored('a.txt') expect(ignored).toBeTruthy() }) - asyncIt('resolves false for a non-ignored path', async () => { + it('resolves false for a non-ignored path', async () => { repo = openFixture('ignore.git') let ignored = await repo.isPathIgnored('b.txt') expect(ignored).toBeFalsy() @@ -99,23 +83,23 @@ describe('GitRepositoryAsync-js', () => { }) describe('when the path is unstaged', () => { - asyncIt('resolves false if the path has not been modified', async () => { + it('resolves false if the path has not been modified', async () => { let modified = await repo.isPathModified(filePath) expect(modified).toBeFalsy() }) - asyncIt('resolves true if the path is modified', async () => { + it('resolves true if the path is modified', async () => { fs.writeFileSync(filePath, "change") let modified = await repo.isPathModified(filePath) expect(modified).toBeTruthy() }) - asyncIt('resolves false if the path is new', async () => { + it('resolves false if the path is new', async () => { let modified = await repo.isPathModified(newPath) expect(modified).toBeFalsy() }) - asyncIt('resolves false if the path is invalid', async () => { + it('resolves false if the path is invalid', async () => { let modified = await repo.isPathModified(emptyPath) expect(modified).toBeFalsy() }) @@ -134,12 +118,12 @@ describe('GitRepositoryAsync-js', () => { }) describe('when the path is unstaged', () => { - asyncIt('returns true if the path is new', async () => { + it('returns true if the path is new', async () => { let isNew = await repo.isPathNew(newPath) expect(isNew).toBeTruthy() }) - asyncIt("returns false if the path isn't new", async () => { + it("returns false if the path isn't new", async () => { let modified = await repo.isPathModified(newPath) expect(modified).toBeFalsy() }) @@ -155,7 +139,7 @@ describe('GitRepositoryAsync-js', () => { filePath = path.join(workingDirPath, 'a.txt') }) - asyncIt('no longer reports a path as modified after checkout', async () => { + it('no longer reports a path as modified after checkout', async () => { let modified = await repo.isPathModified(filePath) expect(modified).toBeFalsy() @@ -170,13 +154,13 @@ describe('GitRepositoryAsync-js', () => { expect(modified).toBeFalsy() }) - asyncIt('restores the contents of the path to the original text', async () => { + it('restores the contents of the path to the original text', async () => { fs.writeFileSync(filePath, 'ch ch changes') await repo.checkoutHead(filePath) expect(fs.readFileSync(filePath, 'utf8')).toBe('') }) - asyncIt('fires a did-change-status event if the checkout completes successfully', async () => { + it('fires a did-change-status event if the checkout completes successfully', async () => { fs.writeFileSync(filePath, 'ch ch changes') await repo.getPathStatus(filePath) @@ -236,7 +220,7 @@ describe('GitRepositoryAsync-js', () => { filePath = path.join(workingDirectory, 'file.txt') }) - asyncIt('trigger a status-changed event when the new status differs from the last cached one', async () => { + it('trigger a status-changed event when the new status differs from the last cached one', async () => { let statusHandler = jasmine.createSpy("statusHandler") repo.onDidChangeStatus(statusHandler) fs.writeFileSync(filePath, '') @@ -263,7 +247,7 @@ describe('GitRepositoryAsync-js', () => { filePath = path.join(directoryPath, 'b.txt') }) - asyncIt('gets the status based on the files inside the directory', async () => { + it('gets the status based on the files inside the directory', async () => { await repo.checkoutHead(filePath) let result = await repo.getDirectoryStatus(directoryPath) @@ -290,7 +274,7 @@ describe('GitRepositoryAsync-js', () => { newPath = fs.absolute(newPath) // specs could be running under symbol path. }) - asyncIt('returns status information for all new and modified files', async () => { + it('returns status information for all new and modified files', async () => { fs.writeFileSync(modifiedPath, 'making this path modified') await repo.refreshStatus() @@ -314,7 +298,7 @@ describe('GitRepositoryAsync-js', () => { waitsForPromise(() => { return repository.refreshStatus() }) }) - asyncIt('emits a status-changed event when a buffer is saved', async () => { + it('emits a status-changed event when a buffer is saved', async () => { let editor = await atom.workspace.open('other.txt') editor.insertNewline() @@ -328,7 +312,7 @@ describe('GitRepositoryAsync-js', () => { runs(() => expect(called).toEqual({path: editor.getPath(), pathStatus: 256})) }) - asyncIt('emits a status-changed event when a buffer is reloaded', async () => { + it('emits a status-changed event when a buffer is reloaded', async () => { let statusHandler = jasmine.createSpy('statusHandler') let reloadHandler = jasmine.createSpy('reloadHandler') @@ -354,7 +338,7 @@ describe('GitRepositoryAsync-js', () => { }) }) - asyncIt("emits a status-changed event when a buffer's path changes", async () => { + it("emits a status-changed event when a buffer's path changes", async () => { let editor = await atom.workspace.open('other.txt') fs.writeFileSync(editor.getPath(), 'changed') @@ -378,7 +362,7 @@ describe('GitRepositoryAsync-js', () => { }) }) - asyncIt('stops listening to the buffer when the repository is destroyed (regression)', async () => { + it('stops listening to the buffer when the repository is destroyed (regression)', async () => { let editor = await atom.workspace.open('other.txt') atom.project.getRepositories()[0].destroy() expect(() => editor.save()).not.toThrow() @@ -401,7 +385,7 @@ describe('GitRepositoryAsync-js', () => { if (project2) project2.destroy() }) - asyncIt('subscribes to all the serialized buffers in the project', async () => { + it('subscribes to all the serialized buffers in the project', async () => { await atom.workspace.open('file.txt') project2 = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 3a2982efa..1022f8cc2 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -1,9 +1,9 @@ 'use babel' -const fs = require('fs-plus') -const Git = require('nodegit') -const path = require('path') -const {Emitter, CompositeDisposable} = require('event-kit') +import fs from 'fs-plus' +import Git from 'nodegit' +import path from 'path' +import {Emitter, CompositeDisposable} from 'event-kit' const modifiedStatusFlags = Git.Status.STATUS.WT_MODIFIED | Git.Status.STATUS.INDEX_MODIFIED | Git.Status.STATUS.WT_DELETED | Git.Status.STATUS.INDEX_DELETED | Git.Status.STATUS.WT_TYPECHANGE | Git.Status.STATUS.INDEX_TYPECHANGE const newStatusFlags = Git.Status.STATUS.WT_NEW | Git.Status.STATUS.INDEX_NEW @@ -11,7 +11,7 @@ const deletedStatusFlags = Git.Status.STATUS.WT_DELETED | Git.Status.STATUS.INDE const indexStatusFlags = Git.Status.STATUS.INDEX_NEW | Git.Status.STATUS.INDEX_MODIFIED | Git.Status.STATUS.INDEX_DELETED | Git.Status.STATUS.INDEX_RENAMED | Git.Status.STATUS.INDEX_TYPECHANGE // Just using this for _.isEqual and _.object, we should impl our own here -const _ = require('underscore-plus') +import _ from 'underscore-plus' module.exports = class GitRepositoryAsync { static open (path, options = {}) { From e0b1cabb2118c719b823ec56c0a006419d28b8d2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 1 Dec 2015 19:33:35 +0100 Subject: [PATCH 125/502] Implement a linear structure for block decoration coordinates We still cannot handle `::rowForTopPixelPosition` when the passed top position is in the middle of two rows and there's also a block decoration. We'll get there eventually. Also, the specs in this commit should serve as a good test suite for the future logarithmic data structure. --- spec/line-top-index-spec.js | 184 +++++++++++++++++++++++++++++++++++ src/linear-line-top-index.js | 90 +++++++++++++++++ 2 files changed, 274 insertions(+) create mode 100644 spec/line-top-index-spec.js create mode 100644 src/linear-line-top-index.js diff --git a/spec/line-top-index-spec.js b/spec/line-top-index-spec.js new file mode 100644 index 000000000..9a9e33e3d --- /dev/null +++ b/spec/line-top-index-spec.js @@ -0,0 +1,184 @@ +/** @babel */ + +const LineTopIndex = require('../src/linear-line-top-index') + +describe("LineTopIndex", function () { + let lineTopIndex + + beforeEach(function () { + lineTopIndex = new LineTopIndex() + lineTopIndex.setDefaultLineHeight(10) + lineTopIndex.setMaxRow(12) + }) + + describe("::topPixelPositionForRow(row)", function () { + it("performs the simple math when there are no block decorations", function () { + expect(lineTopIndex.topPixelPositionForRow(0)).toBe(0) + expect(lineTopIndex.topPixelPositionForRow(4)).toBe(40) + expect(lineTopIndex.topPixelPositionForRow(5)).toBe(50) + expect(lineTopIndex.topPixelPositionForRow(12)).toBe(120) + expect(lineTopIndex.topPixelPositionForRow(13)).toBe(120) + expect(lineTopIndex.topPixelPositionForRow(14)).toBe(120) + + lineTopIndex.splice(0, 2, 3) + + expect(lineTopIndex.topPixelPositionForRow(0)).toBe(0) + expect(lineTopIndex.topPixelPositionForRow(4)).toBe(40) + expect(lineTopIndex.topPixelPositionForRow(5)).toBe(50) + expect(lineTopIndex.topPixelPositionForRow(12)).toBe(120) + expect(lineTopIndex.topPixelPositionForRow(13)).toBe(130) + expect(lineTopIndex.topPixelPositionForRow(14)).toBe(130) + }) + + it("takes into account inserted and removed blocks", function () { + let block1 = lineTopIndex.insertBlock(0, 10) + let block2 = lineTopIndex.insertBlock(3, 20) + let block3 = lineTopIndex.insertBlock(5, 20) + + expect(lineTopIndex.topPixelPositionForRow(0)).toBe(0) + expect(lineTopIndex.topPixelPositionForRow(1)).toBe(20) + expect(lineTopIndex.topPixelPositionForRow(2)).toBe(30) + expect(lineTopIndex.topPixelPositionForRow(3)).toBe(40) + expect(lineTopIndex.topPixelPositionForRow(4)).toBe(70) + expect(lineTopIndex.topPixelPositionForRow(5)).toBe(80) + expect(lineTopIndex.topPixelPositionForRow(6)).toBe(110) + + lineTopIndex.removeBlock(block1) + lineTopIndex.removeBlock(block3) + + expect(lineTopIndex.topPixelPositionForRow(0)).toBe(0) + expect(lineTopIndex.topPixelPositionForRow(1)).toBe(10) + expect(lineTopIndex.topPixelPositionForRow(2)).toBe(20) + expect(lineTopIndex.topPixelPositionForRow(3)).toBe(30) + expect(lineTopIndex.topPixelPositionForRow(4)).toBe(60) + expect(lineTopIndex.topPixelPositionForRow(5)).toBe(70) + expect(lineTopIndex.topPixelPositionForRow(6)).toBe(80) + }) + + it("moves blocks down/up when splicing regions", function () { + let block1 = lineTopIndex.insertBlock(3, 20) + let block2 = lineTopIndex.insertBlock(5, 30) + + lineTopIndex.splice(0, 0, 4) + + expect(lineTopIndex.topPixelPositionForRow(0)).toBe(0) + expect(lineTopIndex.topPixelPositionForRow(6)).toBe(60) + expect(lineTopIndex.topPixelPositionForRow(7)).toBe(70) + expect(lineTopIndex.topPixelPositionForRow(8)).toBe(100) + expect(lineTopIndex.topPixelPositionForRow(9)).toBe(110) + expect(lineTopIndex.topPixelPositionForRow(10)).toBe(150) + expect(lineTopIndex.topPixelPositionForRow(11)).toBe(160) + + lineTopIndex.splice(0, 6, 2) + + expect(lineTopIndex.topPixelPositionForRow(0)).toBe(0) + expect(lineTopIndex.topPixelPositionForRow(3)).toBe(30) + expect(lineTopIndex.topPixelPositionForRow(4)).toBe(60) + expect(lineTopIndex.topPixelPositionForRow(5)).toBe(70) + expect(lineTopIndex.topPixelPositionForRow(6)).toBe(110) + + lineTopIndex.splice(2, 4, 0) + + expect(lineTopIndex.topPixelPositionForRow(0)).toBe(0) + expect(lineTopIndex.topPixelPositionForRow(1)).toBe(10) + expect(lineTopIndex.topPixelPositionForRow(2)).toBe(20) + expect(lineTopIndex.topPixelPositionForRow(3)).toBe(80) + expect(lineTopIndex.topPixelPositionForRow(4)).toBe(90) + expect(lineTopIndex.topPixelPositionForRow(5)).toBe(100) + expect(lineTopIndex.topPixelPositionForRow(6)).toBe(110) + expect(lineTopIndex.topPixelPositionForRow(7)).toBe(120) + expect(lineTopIndex.topPixelPositionForRow(8)).toBe(130) + expect(lineTopIndex.topPixelPositionForRow(9)).toBe(130) + }) + }) + + describe("::rowForTopPixelPosition(top)", function () { + it("performs the simple math when there are no block decorations", function () { + expect(lineTopIndex.rowForTopPixelPosition(0)).toBe(0) + expect(lineTopIndex.rowForTopPixelPosition(40)).toBe(4) + expect(lineTopIndex.rowForTopPixelPosition(44)).toBe(4) + expect(lineTopIndex.rowForTopPixelPosition(46)).toBe(5) + expect(lineTopIndex.rowForTopPixelPosition(50)).toBe(5) + expect(lineTopIndex.rowForTopPixelPosition(120)).toBe(12) + expect(lineTopIndex.rowForTopPixelPosition(130)).toBe(12) + expect(lineTopIndex.rowForTopPixelPosition(140)).toBe(12) + expect(lineTopIndex.rowForTopPixelPosition(145)).toBe(12) + + lineTopIndex.splice(0, 2, 3) + + expect(lineTopIndex.rowForTopPixelPosition(0)).toBe(0) + expect(lineTopIndex.rowForTopPixelPosition(40)).toBe(4) + expect(lineTopIndex.rowForTopPixelPosition(50)).toBe(5) + expect(lineTopIndex.rowForTopPixelPosition(120)).toBe(12) + expect(lineTopIndex.rowForTopPixelPosition(130)).toBe(13) + expect(lineTopIndex.rowForTopPixelPosition(140)).toBe(13) + }) + + it("takes into account inserted and removed blocks", function () { + let block1 = lineTopIndex.insertBlock(0, 10) + let block2 = lineTopIndex.insertBlock(3, 20) + let block3 = lineTopIndex.insertBlock(5, 20) + + expect(lineTopIndex.rowForTopPixelPosition(0)).toBe(0) + expect(lineTopIndex.rowForTopPixelPosition(6)).toBe(0) + expect(lineTopIndex.rowForTopPixelPosition(10)).toBe(0) + expect(lineTopIndex.rowForTopPixelPosition(12)).toBe(0) + expect(lineTopIndex.rowForTopPixelPosition(17)).toBe(1) + expect(lineTopIndex.rowForTopPixelPosition(20)).toBe(1) + expect(lineTopIndex.rowForTopPixelPosition(30)).toBe(2) + expect(lineTopIndex.rowForTopPixelPosition(40)).toBe(3) + expect(lineTopIndex.rowForTopPixelPosition(70)).toBe(4) + expect(lineTopIndex.rowForTopPixelPosition(80)).toBe(5) + expect(lineTopIndex.rowForTopPixelPosition(90)).toBe(5) + expect(lineTopIndex.rowForTopPixelPosition(100)).toBe(6) + expect(lineTopIndex.rowForTopPixelPosition(110)).toBe(6) + + lineTopIndex.removeBlock(block1) + lineTopIndex.removeBlock(block3) + + expect(lineTopIndex.rowForTopPixelPosition(0)).toBe(0) + expect(lineTopIndex.rowForTopPixelPosition(10)).toBe(1) + expect(lineTopIndex.rowForTopPixelPosition(20)).toBe(2) + expect(lineTopIndex.rowForTopPixelPosition(30)).toBe(3) + expect(lineTopIndex.rowForTopPixelPosition(60)).toBe(4) + expect(lineTopIndex.rowForTopPixelPosition(70)).toBe(5) + expect(lineTopIndex.rowForTopPixelPosition(80)).toBe(6) + }) + + it("moves blocks down/up when splicing regions", function () { + let block1 = lineTopIndex.insertBlock(3, 20) + let block2 = lineTopIndex.insertBlock(5, 30) + + lineTopIndex.splice(0, 0, 4) + + expect(lineTopIndex.rowForTopPixelPosition(0)).toBe(0) + expect(lineTopIndex.rowForTopPixelPosition(60)).toBe(6) + expect(lineTopIndex.rowForTopPixelPosition(70)).toBe(7) + expect(lineTopIndex.rowForTopPixelPosition(100)).toBe(8) + expect(lineTopIndex.rowForTopPixelPosition(110)).toBe(9) + expect(lineTopIndex.rowForTopPixelPosition(150)).toBe(10) + expect(lineTopIndex.rowForTopPixelPosition(160)).toBe(11) + + lineTopIndex.splice(0, 6, 2) + + expect(lineTopIndex.rowForTopPixelPosition(0)).toBe(0) + expect(lineTopIndex.rowForTopPixelPosition(30)).toBe(3) + expect(lineTopIndex.rowForTopPixelPosition(60)).toBe(4) + expect(lineTopIndex.rowForTopPixelPosition(70)).toBe(5) + expect(lineTopIndex.rowForTopPixelPosition(110)).toBe(6) + + lineTopIndex.splice(2, 4, 0) + + expect(lineTopIndex.rowForTopPixelPosition(0)).toBe(0) + expect(lineTopIndex.rowForTopPixelPosition(10)).toBe(1) + expect(lineTopIndex.rowForTopPixelPosition(20)).toBe(2) + expect(lineTopIndex.rowForTopPixelPosition(80)).toBe(3) + expect(lineTopIndex.rowForTopPixelPosition(90)).toBe(4) + expect(lineTopIndex.rowForTopPixelPosition(100)).toBe(5) + expect(lineTopIndex.rowForTopPixelPosition(110)).toBe(6) + expect(lineTopIndex.rowForTopPixelPosition(120)).toBe(7) + expect(lineTopIndex.rowForTopPixelPosition(130)).toBe(8) + expect(lineTopIndex.rowForTopPixelPosition(130)).toBe(8) + }) + }) +}) diff --git a/src/linear-line-top-index.js b/src/linear-line-top-index.js new file mode 100644 index 000000000..638997d49 --- /dev/null +++ b/src/linear-line-top-index.js @@ -0,0 +1,90 @@ +/** @babel */ + +module.exports = +class LineTopIndex { + constructor () { + this.idCounter = 0 + this.blocks = [] + this.maxRow = 0 + this.defaultLineHeight = 0 + } + + setDefaultLineHeight (lineHeight) { + this.defaultLineHeight = lineHeight + } + + setMaxRow (maxRow) { + this.maxRow = maxRow + } + + insertBlock (row, height) { + let id = this.idCounter++ + this.blocks.push({id, row, height}) + this.blocks.sort((a, b) => a.row - b.row) + return id + } + + changeBlockHeight (id, height) { + let block = this.blocks.find((block) => block.id == id) + if (block) { + block.height = height + } + } + + removeBlock (id) { + let index = this.blocks.findIndex((block) => block.id == id) + if (index != -1) { + this.blocks.splice(index, 1) + } + } + + splice (startRow, oldExtent, newExtent) { + let blocksHeight = 0 + this.blocks.forEach(function (block) { + if (block.row >= startRow) { + if (block.row >= startRow + oldExtent) { + block.row += newExtent - oldExtent + } else { + block.row = startRow + newExtent + // invalidate marker? + } + + block.top = block.row * this.defaultLineHeight + blocksHeight + blocksHeight += block.height + } + }) + + this.setMaxRow(this.maxRow + newExtent - oldExtent) + } + + topPixelPositionForRow (row) { + row = Math.min(row, this.maxRow) + let linesHeight = row * this.defaultLineHeight + let blocksHeight = this.blocks.filter((block) => block.row < row).reduce((a, b) => a + b.height, 0) + return linesHeight + blocksHeight + } + + bottomPixelPositionForRow (row) { + return this.topPixelPositionForRow(row + 1) - this.defaultLineHeight + } + + rowForTopPixelPosition (top) { + let blocksHeight = 0 + let lastRow = 0 + let lastTop = 0 + for (let block of this.blocks) { + blocksHeight += block.height + let linesHeight = block.row * this.defaultLineHeight + if (blocksHeight + linesHeight > top) { + break + } else { + lastRow = block.row + lastTop = blocksHeight + linesHeight + } + } + + let remainingHeight = Math.max(0, top - lastTop) + let remainingRows = Math.round(remainingHeight / this.defaultLineHeight) + return Math.min(this.maxRow, lastRow + remainingRows) + } +} From 24b8fcfc88791feaededf994dafd9fd4fd47b9bb Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 1 Dec 2015 13:36:23 -0500 Subject: [PATCH 126/502] Gimme the linters. --- build/package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build/package.json b/build/package.json index 40e5b309a..9996dec81 100644 --- a/build/package.json +++ b/build/package.json @@ -9,6 +9,7 @@ "asar": "^0.8.0", "async": "~0.2.9", "aws-sdk": "^2.2.18", + "babel-eslint": "^5.0.0-beta4", "donna": "^1.0.13", "formidable": "~1.0.14", "fs-plus": "2.x", @@ -35,6 +36,7 @@ "request": "~2.27.0", "rimraf": "~2.2.2", "runas": "^3.1", + "standard": "^5.4.1", "tello": "1.0.5", "temp": "~0.8.1", "underscore-plus": "1.x", From cf30f39047fb9d33f6ac7163f5c22fb6674fd7b1 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 1 Dec 2015 13:36:28 -0500 Subject: [PATCH 127/502] These are fine. --- package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/package.json b/package.json index 5f16bf814..c87f4723d 100644 --- a/package.json +++ b/package.json @@ -163,8 +163,12 @@ "afterEach", "beforeEach", "describe", + "fdescribe", + "xdescribe", "expect", "it", + "fit", + "xit", "jasmine", "runs", "spyOn", From f290596542d5f10b6bc741b0f5a73d68b757a60c Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 1 Dec 2015 13:36:48 -0500 Subject: [PATCH 128/502] Remove some lint. --- spec/git-repository-async-spec.js | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 2e6c1188e..26d767bd4 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -5,18 +5,18 @@ import path from 'path' import temp from 'temp' import Git from 'nodegit' -import {it, ffit, fffit, beforeEach, afterEach} from './async-spec-helpers' +import {it, beforeEach, afterEach} from './async-spec-helpers' import GitRepositoryAsync from '../src/git-repository-async' import Project from '../src/project' temp.track() -function openFixture(fixture) { +function openFixture (fixture) { return GitRepositoryAsync.open(path.join(__dirname, 'fixtures', 'git', fixture)) } -function copyRepository() { +function copyRepository () { let workingDirPath = temp.mkdirSync('atom-working-dir') fs.copySync(path.join(__dirname, 'fixtures', 'git', 'working-dir'), workingDirPath) fs.renameSync(path.join(workingDirPath, 'git.git'), path.join(workingDirPath, '.git')) @@ -37,7 +37,7 @@ describe('GitRepositoryAsync-js', () => { let threw = false try { await repo.repoPromise - } catch(e) { + } catch (e) { threw = true } @@ -89,7 +89,7 @@ describe('GitRepositoryAsync-js', () => { }) it('resolves true if the path is modified', async () => { - fs.writeFileSync(filePath, "change") + fs.writeFileSync(filePath, 'change') let modified = await repo.isPathModified(filePath) expect(modified).toBeTruthy() }) @@ -107,12 +107,11 @@ describe('GitRepositoryAsync-js', () => { }) describe('.isPathNew(path)', () => { - let filePath, newPath + let newPath beforeEach(() => { let workingDirPath = copyRepository() repo = GitRepositoryAsync.open(workingDirPath) - filePath = path.join(workingDirPath, 'a.txt') newPath = path.join(workingDirPath, 'new-path.txt') fs.writeFileSync(newPath, "i'm new here") }) @@ -192,7 +191,7 @@ describe('GitRepositoryAsync-js', () => { }) xit('displays a confirmation dialog by default', () => { - spyOn(atom, 'confirm').andCallFake(buttons, () => buttons[0].OK()) + spyOn(atom, 'confirm').andCallFake(buttons, () => buttons[0].OK()) // eslint-disable-line atom.config.set('editor.confirmCheckoutHeadRevision', true) waitsForPromise(() => repo.checkoutHeadForEditor(editor)) @@ -221,7 +220,7 @@ describe('GitRepositoryAsync-js', () => { }) it('trigger a status-changed event when the new status differs from the last cached one', async () => { - let statusHandler = jasmine.createSpy("statusHandler") + let statusHandler = jasmine.createSpy('statusHandler') repo.onDidChangeStatus(statusHandler) fs.writeFileSync(filePath, '') @@ -261,7 +260,7 @@ describe('GitRepositoryAsync-js', () => { }) describe('.refreshStatus()', () => { - let newPath, modifiedPath, cleanPath, originalModifiedPathText + let newPath, modifiedPath, cleanPath beforeEach(() => { let workingDirectory = copyRepository() @@ -435,7 +434,5 @@ describe('GitRepositoryAsync-js', () => { let relativizedPath = repository.relativize(`${workdir}/a/b.txt`, workdir) expect(relativizedPath).toBe('a/b.txt') }) - }) - }) From 2c004538a3bed5d79e6ec08c43e0c6884116df45 Mon Sep 17 00:00:00 2001 From: BrainCrumbz Date: Tue, 1 Dec 2015 19:44:10 +0100 Subject: [PATCH 129/502] Add examples for Windows script/build The command line parameters for script/build command where missing examples on how they should be run. That might be usefule for people starting active contribution to the project. --- docs/build-instructions/windows.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/build-instructions/windows.md b/docs/build-instructions/windows.md index debdd6570..7eb2ed03d 100644 --- a/docs/build-instructions/windows.md +++ b/docs/build-instructions/windows.md @@ -31,8 +31,14 @@ Note: If you use cmd or Powershell instead of Git Shell, use a backslash instead These instructions will assume the use of Git Shell. ### `script/build` Options - * `--install-dir` - Creates the final built application in this directory. - * `--build-dir` - Build the application in this directory. + * `--install-dir` - Creates the final built application in this directory. Example (trailing slash is optional): +```bash +./script/build --install-dir Z:\Some\Destination\Directory\ +``` + * `--build-dir` - Build the application in this directory. Example (trailing slash is optional): +```bash +./script/build --build-dir Z:\Some\Temporary\Directory\ +``` * `--verbose` - Verbose mode. A lot more information output. ## Why do I have to use GitHub Desktop? From 4e64af91556e29b378936c7d8368148a3224c9c3 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 1 Dec 2015 13:53:57 -0500 Subject: [PATCH 130/502] ES6 some more. --- src/git-repository-async.js | 140 +++++++++++++++++------------------- 1 file changed, 64 insertions(+), 76 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 1022f8cc2..fa568edc9 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -31,15 +31,15 @@ module.exports = class GitRepositoryAsync { this.repoPromise = Git.Repository.open(path) this.isCaseInsensitive = fs.isCaseInsensitive() - let {project} = options + const {project} = options this.project = project if (this.project) { - this.subscriptions.add(this.project.onDidAddBuffer((buffer) => { + this.subscriptions.add(this.project.onDidAddBuffer(buffer => { this.subscribeToBuffer(buffer) })) - this.project.getBuffers().forEach((buffer) => { this.subscribeToBuffer(buffer) }) + this.project.getBuffers().forEach(buffer => { this.subscribeToBuffer(buffer) }) } } @@ -56,47 +56,39 @@ module.exports = class GitRepositoryAsync { } getPath () { - return this.repoPromise.then((repo) => { - return repo.path().replace(/\/$/, '') - }) + return this.repoPromise.then(repo => repo.path().replace(/\/$/, '')) } isPathIgnored (_path) { - return this.repoPromise.then((repo) => { - return Git.Ignore.pathIsIgnored(repo, _path) - }) + return this.repoPromise.then(repo => Git.Ignore.pathIsIgnored(repo, _path)) } isPathModified (_path) { - return this._filterStatusesByPath(_path).then(function (statuses) { - return statuses.filter((status) => { - return status.isModified() - }).length > 0 + return this._filterStatusesByPath(_path).then(statuses => { + return statuses.filter(status => status.isModified()).length > 0 }) } isPathNew (_path) { - return this._filterStatusesByPath(_path).then(function (statuses) { - return statuses.filter((status) => { - return status.isNew() - }).length > 0 + return this._filterStatusesByPath(_path).then(statuses => { + return statuses.filter(status => status.isNew()).length > 0 }) } checkoutHead (_path) { - return this.repoPromise.then((repo) => { - let checkoutOptions = new Git.CheckoutOptions() - checkoutOptions.paths = [this.relativize(_path, repo.workdir())] - checkoutOptions.checkoutStrategy = Git.Checkout.STRATEGY.FORCE | Git.Checkout.STRATEGY.DISABLE_PATHSPEC_MATCH - return Git.Checkout.head(repo, checkoutOptions) - }).then(() => { - return this.getPathStatus(_path) - }) + return this.repoPromise + .then(repo => { + let checkoutOptions = new Git.CheckoutOptions() + checkoutOptions.paths = [this.relativize(_path, repo.workdir())] + checkoutOptions.checkoutStrategy = Git.Checkout.STRATEGY.FORCE | Git.Checkout.STRATEGY.DISABLE_PATHSPEC_MATCH + return Git.Checkout.head(repo, checkoutOptions) + }) + .then(() => this.getPathStatus(_path)) } checkoutHeadForEditor (editor) { - return new Promise(function (resolve, reject) { - let filePath = editor.getPath() + return new Promise((resolve, reject) => { + const filePath = editor.getPath() if (filePath) { if (editor.buffer.isModified()) { editor.buffer.reload() @@ -105,27 +97,27 @@ module.exports = class GitRepositoryAsync { } else { reject() } - }).then((filePath) => { - return this.checkoutHead(filePath) - }) + }).then(filePath => this.checkoutHead(filePath)) } // Returns a Promise that resolves to the status bit of a given path if it has // one, otherwise 'current'. getPathStatus (_path) { let relativePath - return this.repoPromise.then((repo) => { - relativePath = this.relativize(_path, repo.workdir()) - return this._filterStatusesByPath(_path) - }).then((statuses) => { - let cachedStatus = this.pathStatusCache[relativePath] || 0 - let status = statuses[0] ? statuses[0].statusBit() : Git.Status.STATUS.CURRENT - if (status !== cachedStatus && this.emitter != null) { - this.emitter.emit('did-change-status', {path: _path, pathStatus: status}) - } - this.pathStatusCache[relativePath] = status - return status - }) + return this.repoPromise + .then(repo => { + relativePath = this.relativize(_path, repo.workdir()) + return this._filterStatusesByPath(_path) + }) + .then(statuses => { + const cachedStatus = this.pathStatusCache[relativePath] || 0 + const status = statuses[0] ? statuses[0].statusBit() : Git.Status.STATUS.CURRENT + if (status !== cachedStatus && this.emitter != null) { + this.emitter.emit('did-change-status', {path: _path, pathStatus: status}) + } + this.pathStatusCache[relativePath] = status + return status + }) } // Get the status of a directory in the repository's working directory. @@ -138,22 +130,22 @@ module.exports = class GitRepositoryAsync { getDirectoryStatus (directoryPath) { let relativePath // XXX _filterSBD already gets repoPromise - return this.repoPromise.then((repo) => { - relativePath = this.relativize(directoryPath, repo.workdir()) - return this._filterStatusesByDirectory(relativePath) - }).then((statuses) => { - return Promise.all(statuses.map(function (s) { return s.statusBit() })).then(function (bits) { - let directoryStatus = 0 - let filteredBits = bits.filter(function (b) { return b > 0 }) - if (filteredBits.length > 0) { - filteredBits.forEach(function (bit) { - directoryStatus |= bit - }) - } - - return directoryStatus + return this.repoPromise + .then(repo => { + relativePath = this.relativize(directoryPath, repo.workdir()) + return this._filterStatusesByDirectory(relativePath) + }) + .then(statuses => { + return Promise.all(statuses.map(s => s.statusBit())).then(bits => { + let directoryStatus = 0 + const filteredBits = bits.filter(b => b > 0) + if (filteredBits.length > 0) { + filteredBits.forEach(bit => directoryStatus |= bit) + } + + return directoryStatus + }) }) - }) } // Get the current branch and update this.branch. @@ -195,7 +187,7 @@ module.exports = class GitRepositoryAsync { // ================ subscribeToBuffer (buffer) { - let bufferSubscriptions = new CompositeDisposable() + const bufferSubscriptions = new CompositeDisposable() let getBufferPathStatus = () => { let _path = buffer.getPath() @@ -247,7 +239,7 @@ module.exports = class GitRepositoryAsync { } if (this.isCaseInsensitive) { - let lowerCasePath = _path.toLowerCase() + const lowerCasePath = _path.toLowerCase() workingDirectory = workingDirectory.toLowerCase() if (lowerCasePath.indexOf(workingDirectory) === 0) { @@ -263,7 +255,7 @@ module.exports = class GitRepositoryAsync { } getCachedPathStatus (_path) { - return this.repoPromise.then((repo) => { + return this.repoPromise.then(repo => { return this.pathStatusCache[this.relativize(_path, repo.workdir())] }) } @@ -291,24 +283,22 @@ module.exports = class GitRepositoryAsync { _filterStatusesByPath (_path) { // Surely I'm missing a built-in way to do this let basePath = null - return this.repoPromise.then((repo) => { - basePath = repo.workdir() - return repo.getStatus() - }).then((statuses) => { - return statuses.filter(function (status) { - return _path === path.join(basePath, status.path()) + return this.repoPromise + .then(repo => { + basePath = repo.workdir() + return repo.getStatus() + }) + .then(statuses => { + return statuses.filter(status => _path === path.join(basePath, status.path())) }) - }) } _filterStatusesByDirectory (directoryPath) { - return this.repoPromise.then(function (repo) { - return repo.getStatus() - }).then(function (statuses) { - return statuses.filter((status) => { - return status.path().indexOf(directoryPath) === 0 + return this.repoPromise + .then(repo => repo.getStatus()) + .then(statuses => { + return statuses.filter(status => status.path().indexOf(directoryPath) === 0) }) - }) } // Event subscription // ================== @@ -334,9 +324,7 @@ module.exports = class GitRepositoryAsync { isProjectAtRoot () { if (this.projectAtRoot === undefined) { this.projectAtRoot = Promise.resolve(() => { - return this.repoPromise.then((repo) => { - return this.project.relativize(repo.workdir) - }) + return this.repoPromise.then(repo => this.project.relativize(repo.workdir)) }) } From f6a00458cb3d35285bde88230c8e755121990124 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 1 Dec 2015 13:59:29 -0500 Subject: [PATCH 131/502] License overrides. --- build/tasks/license-overrides.coffee | 203 +++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) diff --git a/build/tasks/license-overrides.coffee b/build/tasks/license-overrides.coffee index e716f76a2..173e41144 100644 --- a/build/tasks/license-overrides.coffee +++ b/build/tasks/license-overrides.coffee @@ -123,3 +123,206 @@ module.exports = (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ + 'tweetnacl@0.13.2': + license: 'CC0-1.0' + source: 'https://github.com/dchest/tweetnacl-js/blob/2f328394f74d83564634fb89ea2798caa3a4edb9/README.md says public domain.' + 'json-schema@0.2.2': + license: 'AFLv2.1' + source: 'README links to https://github.com/dojo/dojo/blob/8b6a5e4c42f9cf777dd39eaae8b188e0ebb59a4c/LICENSE' + sourceText: """ + Dojo is available under *either* the terms of the modified BSD license *or* the + Academic Free License version 2.1. As a recipient of Dojo, you may choose which + license to receive this code under (except as noted in per-module LICENSE + files). Some modules may not be the copyright of the Dojo Foundation. These + modules contain explicit declarations of copyright in both the LICENSE files in + the directories in which they reside and in the code itself. No external + contributions are allowed under licenses which are fundamentally incompatible + with the AFL or BSD licenses that Dojo is distributed under. + + The text of the AFL and BSD licenses is reproduced below. + + ------------------------------------------------------------------------------- + The "New" BSD License: + ********************** + + Copyright (c) 2005-2015, The Dojo Foundation + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of the Dojo Foundation nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + ------------------------------------------------------------------------------- + The Academic Free License, v. 2.1: + ********************************** + + This Academic Free License (the "License") applies to any original work of + authorship (the "Original Work") whose owner (the "Licensor") has placed the + following notice immediately following the copyright notice for the Original + Work: + + Licensed under the Academic Free License version 2.1 + + 1) Grant of Copyright License. Licensor hereby grants You a world-wide, + royalty-free, non-exclusive, perpetual, sublicenseable license to do the + following: + + a) to reproduce the Original Work in copies; + + b) to prepare derivative works ("Derivative Works") based upon the Original + Work; + + c) to distribute copies of the Original Work and Derivative Works to the + public; + + d) to perform the Original Work publicly; and + + e) to display the Original Work publicly. + + 2) Grant of Patent License. Licensor hereby grants You a world-wide, + royalty-free, non-exclusive, perpetual, sublicenseable license, under patent + claims owned or controlled by the Licensor that are embodied in the Original + Work as furnished by the Licensor, to make, use, sell and offer for sale the + Original Work and Derivative Works. + + 3) Grant of Source Code License. The term "Source Code" means the preferred + form of the Original Work for making modifications to it and all available + documentation describing how to modify the Original Work. Licensor hereby + agrees to provide a machine-readable copy of the Source Code of the Original + Work along with each copy of the Original Work that Licensor distributes. + Licensor reserves the right to satisfy this obligation by placing a + machine-readable copy of the Source Code in an information repository + reasonably calculated to permit inexpensive and convenient access by You for as + long as Licensor continues to distribute the Original Work, and by publishing + the address of that information repository in a notice immediately following + the copyright notice that applies to the Original Work. + + 4) Exclusions From License Grant. Neither the names of Licensor, nor the names + of any contributors to the Original Work, nor any of their trademarks or + service marks, may be used to endorse or promote products derived from this + Original Work without express prior written permission of the Licensor. Nothing + in this License shall be deemed to grant any rights to trademarks, copyrights, + patents, trade secrets or any other intellectual property of Licensor except as + expressly stated herein. No patent license is granted to make, use, sell or + offer to sell embodiments of any patent claims other than the licensed claims + defined in Section 2. No right is granted to the trademarks of Licensor even if + such marks are included in the Original Work. Nothing in this License shall be + interpreted to prohibit Licensor from licensing under different terms from this + License any Original Work that Licensor otherwise would have a right to + license. + + 5) This section intentionally omitted. + + 6) Attribution Rights. You must retain, in the Source Code of any Derivative + Works that You create, all copyright, patent or trademark notices from the + Source Code of the Original Work, as well as any notices of licensing and any + descriptive text identified therein as an "Attribution Notice." You must cause + the Source Code for any Derivative Works that You create to carry a prominent + Attribution Notice reasonably calculated to inform recipients that You have + modified the Original Work. + + 7) Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that + the copyright in and to the Original Work and the patent rights granted herein + by Licensor are owned by the Licensor or are sublicensed to You under the terms + of this License with the permission of the contributor(s) of those copyrights + and patent rights. Except as expressly stated in the immediately proceeding + sentence, the Original Work is provided under this License on an "AS IS" BASIS + and WITHOUT WARRANTY, either express or implied, including, without limitation, + the warranties of NON-INFRINGEMENT, MERCHANTABILITY or FITNESS FOR A PARTICULAR + PURPOSE. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. + This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No + license to Original Work is granted hereunder except under this disclaimer. + + 8) Limitation of Liability. Under no circumstances and under no legal theory, + whether in tort (including negligence), contract, or otherwise, shall the + Licensor be liable to any person for any direct, indirect, special, incidental, + or consequential damages of any character arising as a result of this License + or the use of the Original Work including, without limitation, damages for loss + of goodwill, work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses. This limitation of liability shall not + apply to liability for death or personal injury resulting from Licensor's + negligence to the extent applicable law prohibits such limitation. Some + jurisdictions do not allow the exclusion or limitation of incidental or + consequential damages, so this exclusion and limitation may not apply to You. + + 9) Acceptance and Termination. If You distribute copies of the Original Work or + a Derivative Work, You must make a reasonable effort under the circumstances to + obtain the express assent of recipients to the terms of this License. Nothing + else but this License (or another written agreement between Licensor and You) + grants You permission to create Derivative Works based upon the Original Work + or to exercise any of the rights granted in Section 1 herein, and any attempt + to do so except under the terms of this License (or another written agreement + between Licensor and You) is expressly prohibited by U.S. copyright law, the + equivalent laws of other countries, and by international treaty. Therefore, by + exercising any of the rights granted to You in Section 1 herein, You indicate + Your acceptance of this License and all of its terms and conditions. + + 10) Termination for Patent Action. This License shall terminate automatically + and You may no longer exercise any of the rights granted to You by this License + as of the date You commence an action, including a cross-claim or counterclaim, + against Licensor or any licensee alleging that the Original Work infringes a + patent. This termination provision shall not apply for an action alleging + patent infringement by combinations of the Original Work with other software or + hardware. + + 11) Jurisdiction, Venue and Governing Law. Any action or suit relating to this + License may be brought only in the courts of a jurisdiction wherein the + Licensor resides or in which Licensor conducts its primary business, and under + the laws of that jurisdiction excluding its conflict-of-law provisions. The + application of the United Nations Convention on Contracts for the International + Sale of Goods is expressly excluded. Any use of the Original Work outside the + scope of this License or after its termination shall be subject to the + requirements and penalties of the U.S. Copyright Act, 17 U.S.C. § 101 et + seq., the equivalent laws of other countries, and international treaty. This + section shall survive the termination of this License. + + 12) Attorneys Fees. In any action to enforce the terms of this License or + seeking damages relating thereto, the prevailing party shall be entitled to + recover its costs and expenses, including, without limitation, reasonable + attorneys' fees and costs incurred in connection with such action, including + any appeal of such action. This section shall survive the termination of this + License. + + 13) Miscellaneous. This License represents the complete agreement concerning + the subject matter hereof. If any provision of this License is held to be + unenforceable, such provision shall be reformed only to the extent necessary to + make it enforceable. + + 14) Definition of "You" in This License. "You" throughout this License, whether + in upper or lower case, means an individual or a legal entity exercising rights + under, and complying with all of the terms of, this License. For legal + entities, "You" includes any entity that controls, is controlled by, or is + under common control with you. For purposes of this definition, "control" means + (i) the power, direct or indirect, to cause the direction or management of such + entity, whether by contract or otherwise, or (ii) ownership of fifty percent + (50%) or more of the outstanding shares, or (iii) beneficial ownership of such + entity. + + 15) Right to Use. You may use the Original Work in all ways not otherwise + restricted or conditioned by this License or by law, and Licensor promises not + to interfere with or be responsible for such uses by You. + + This license is Copyright (C) 2003-2004 Lawrence E. Rosen. All rights reserved. + Permission is hereby granted to copy and distribute this license without + modification. This license may not be modified without the express written + permission of its copyright owner. + """ From 5019cc31ab407022412f579b2382e26d7f2ea48d Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 1 Dec 2015 14:16:50 -0500 Subject: [PATCH 132/502] These can be const too. --- src/git-repository-async.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index fa568edc9..b6ac9c08a 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -78,7 +78,7 @@ module.exports = class GitRepositoryAsync { checkoutHead (_path) { return this.repoPromise .then(repo => { - let checkoutOptions = new Git.CheckoutOptions() + const checkoutOptions = new Git.CheckoutOptions() checkoutOptions.paths = [this.relativize(_path, repo.workdir())] checkoutOptions.checkoutStrategy = Git.Checkout.STRATEGY.FORCE | Git.Checkout.STRATEGY.DISABLE_PATHSPEC_MATCH return Git.Checkout.head(repo, checkoutOptions) @@ -189,8 +189,8 @@ module.exports = class GitRepositoryAsync { subscribeToBuffer (buffer) { const bufferSubscriptions = new CompositeDisposable() - let getBufferPathStatus = () => { - let _path = buffer.getPath() + const getBufferPathStatus = () => { + const _path = buffer.getPath() if (_path) { // We don't need to do anything with this promise, we just want the // emitted event side effect From 4559936af1d5000eb9bb0c4a41525d3060f285e5 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 1 Dec 2015 16:45:02 -0500 Subject: [PATCH 133/502] More const'ing. --- spec/git-repository-async-spec.js | 99 ++++++++++++++++--------------- 1 file changed, 51 insertions(+), 48 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 26d767bd4..e6bdd801e 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -51,21 +51,25 @@ describe('GitRepositoryAsync-js', () => { it('returns the repository path for a repository path', async () => { repo = openFixture('master.git') - let repoPath = await repo.getPath() + const repoPath = await repo.getPath() expect(repoPath).toBe(path.join(__dirname, 'fixtures', 'git', 'master.git')) }) }) describe('.isPathIgnored(path)', () => { - it('resolves true for an ignored path', async () => { + let repo + + beforeEach(() => { repo = openFixture('ignore.git') - let ignored = await repo.isPathIgnored('a.txt') + }) + + it('resolves true for an ignored path', async () => { + const ignored = await repo.isPathIgnored('a.txt') expect(ignored).toBeTruthy() }) it('resolves false for a non-ignored path', async () => { - repo = openFixture('ignore.git') - let ignored = await repo.isPathIgnored('b.txt') + const ignored = await repo.isPathIgnored('b.txt') expect(ignored).toBeFalsy() }) }) @@ -74,7 +78,7 @@ describe('GitRepositoryAsync-js', () => { let filePath, newPath, emptyPath beforeEach(() => { - let workingDirPath = copyRepository() + const workingDirPath = copyRepository() repo = GitRepositoryAsync.open(workingDirPath) filePath = path.join(workingDirPath, 'a.txt') newPath = path.join(workingDirPath, 'new-path.txt') @@ -84,23 +88,23 @@ describe('GitRepositoryAsync-js', () => { describe('when the path is unstaged', () => { it('resolves false if the path has not been modified', async () => { - let modified = await repo.isPathModified(filePath) + const modified = await repo.isPathModified(filePath) expect(modified).toBeFalsy() }) it('resolves true if the path is modified', async () => { fs.writeFileSync(filePath, 'change') - let modified = await repo.isPathModified(filePath) + const modified = await repo.isPathModified(filePath) expect(modified).toBeTruthy() }) it('resolves false if the path is new', async () => { - let modified = await repo.isPathModified(newPath) + const modified = await repo.isPathModified(newPath) expect(modified).toBeFalsy() }) it('resolves false if the path is invalid', async () => { - let modified = await repo.isPathModified(emptyPath) + const modified = await repo.isPathModified(emptyPath) expect(modified).toBeFalsy() }) }) @@ -110,7 +114,7 @@ describe('GitRepositoryAsync-js', () => { let newPath beforeEach(() => { - let workingDirPath = copyRepository() + const workingDirPath = copyRepository() repo = GitRepositoryAsync.open(workingDirPath) newPath = path.join(workingDirPath, 'new-path.txt') fs.writeFileSync(newPath, "i'm new here") @@ -118,12 +122,12 @@ describe('GitRepositoryAsync-js', () => { describe('when the path is unstaged', () => { it('returns true if the path is new', async () => { - let isNew = await repo.isPathNew(newPath) + const isNew = await repo.isPathNew(newPath) expect(isNew).toBeTruthy() }) it("returns false if the path isn't new", async () => { - let modified = await repo.isPathModified(newPath) + const modified = await repo.isPathModified(newPath) expect(modified).toBeFalsy() }) }) @@ -133,7 +137,7 @@ describe('GitRepositoryAsync-js', () => { let filePath beforeEach(() => { - let workingDirPath = copyRepository() + const workingDirPath = copyRepository() repo = GitRepositoryAsync.open(workingDirPath) filePath = path.join(workingDirPath, 'a.txt') }) @@ -164,7 +168,7 @@ describe('GitRepositoryAsync-js', () => { await repo.getPathStatus(filePath) - let statusHandler = jasmine.createSpy('statusHandler') + const statusHandler = jasmine.createSpy('statusHandler') repo.onDidChangeStatus(statusHandler) await repo.checkoutHead(filePath) @@ -181,7 +185,7 @@ describe('GitRepositoryAsync-js', () => { let filePath, editor beforeEach(() => { - let workingDirPath = copyRepository() + const workingDirPath = copyRepository() repo = GitRepositoryAsync.open(workingDirPath) filePath = path.join(workingDirPath, 'a.txt') fs.writeFileSync(filePath, 'ch ch changes') @@ -214,20 +218,20 @@ describe('GitRepositoryAsync-js', () => { let filePath beforeEach(() => { - let workingDirectory = copyRepository() + const workingDirectory = copyRepository() repo = GitRepositoryAsync.open(workingDirectory) filePath = path.join(workingDirectory, 'file.txt') }) it('trigger a status-changed event when the new status differs from the last cached one', async () => { - let statusHandler = jasmine.createSpy('statusHandler') + const statusHandler = jasmine.createSpy('statusHandler') repo.onDidChangeStatus(statusHandler) fs.writeFileSync(filePath, '') await repo.getPathStatus(filePath) expect(statusHandler.callCount).toBe(1) - let status = Git.Status.STATUS.WT_MODIFIED + const status = Git.Status.STATUS.WT_MODIFIED expect(statusHandler.argsForCall[0][0]).toEqual({path: filePath, pathStatus: status}) fs.writeFileSync(filePath, 'abc') @@ -240,7 +244,7 @@ describe('GitRepositoryAsync-js', () => { let directoryPath, filePath beforeEach(() => { - let workingDirectory = copyRepository() + const workingDirectory = copyRepository() repo = GitRepositoryAsync.open(workingDirectory) directoryPath = path.join(workingDirectory, 'dir') filePath = path.join(directoryPath, 'b.txt') @@ -263,7 +267,7 @@ describe('GitRepositoryAsync-js', () => { let newPath, modifiedPath, cleanPath beforeEach(() => { - let workingDirectory = copyRepository() + const workingDirectory = copyRepository() repo = GitRepositoryAsync.open(workingDirectory) modifiedPath = path.join(workingDirectory, 'file.txt') newPath = path.join(workingDirectory, 'untracked.txt') @@ -284,6 +288,8 @@ describe('GitRepositoryAsync-js', () => { }) describe('buffer events', () => { + let repository + beforeEach(() => { const workingDirectory = copyRepository() atom.project.setPaths([workingDirectory]) @@ -293,16 +299,15 @@ describe('GitRepositoryAsync-js', () => { // we're in a known state. *But* it's really hard to observe that from the // outside in a non-racy fashion. So let's refresh again and wait for it // to complete before we continue. - let repository = atom.project.getRepositories()[0].async - waitsForPromise(() => { return repository.refreshStatus() }) + repository = atom.project.getRepositories()[0].async + waitsForPromise(() => repository.refreshStatus()) }) it('emits a status-changed event when a buffer is saved', async () => { - let editor = await atom.workspace.open('other.txt') + const editor = await atom.workspace.open('other.txt') editor.insertNewline() - let repository = atom.project.getRepositories()[0].async let called repository.onDidChangeStatus(c => called = c) editor.save() @@ -312,14 +317,11 @@ describe('GitRepositoryAsync-js', () => { }) it('emits a status-changed event when a buffer is reloaded', async () => { - let statusHandler = jasmine.createSpy('statusHandler') - let reloadHandler = jasmine.createSpy('reloadHandler') - - let editor = await atom.workspace.open('other.txt') + const editor = await atom.workspace.open('other.txt') fs.writeFileSync(editor.getPath(), 'changed') - let repository = atom.project.getRepositories()[0].async + const statusHandler = jasmine.createSpy('statusHandler') repository.onDidChangeStatus(statusHandler) editor.getBuffer().reload() @@ -328,7 +330,8 @@ describe('GitRepositoryAsync-js', () => { expect(statusHandler.callCount).toBe(1) expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) - let buffer = editor.getBuffer() + const buffer = editor.getBuffer() + const reloadHandler = jasmine.createSpy('reloadHandler') buffer.onDidReload(reloadHandler) buffer.reload() @@ -338,32 +341,32 @@ describe('GitRepositoryAsync-js', () => { }) it("emits a status-changed event when a buffer's path changes", async () => { - let editor = await atom.workspace.open('other.txt') + const editor = await atom.workspace.open('other.txt') fs.writeFileSync(editor.getPath(), 'changed') - let statusHandler = jasmine.createSpy('statusHandler') - let repository = atom.project.getRepositories()[0].async + const statusHandler = jasmine.createSpy('statusHandler') repository.onDidChangeStatus(statusHandler) editor.getBuffer().emitter.emit('did-change-path') + waitsFor(() => statusHandler.callCount > 0) runs(() => { expect(statusHandler.callCount).toBe(1) expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) - let pathHandler = jasmine.createSpy('pathHandler') - let buffer = editor.getBuffer() + const pathHandler = jasmine.createSpy('pathHandler') + const buffer = editor.getBuffer() buffer.onDidChangePath(pathHandler) buffer.emitter.emit('did-change-path') waitsFor(() => pathHandler.callCount > 0) - runs(() => expect(statusHandler.callCount).toBe(1)) + runs(() => expect(pathHandler.callCount).toBe(1)) }) }) it('stops listening to the buffer when the repository is destroyed (regression)', async () => { - let editor = await atom.workspace.open('other.txt') - atom.project.getRepositories()[0].destroy() + const editor = await atom.workspace.open('other.txt') + repository.destroy() expect(() => editor.save()).not.toThrow() }) }) @@ -376,7 +379,7 @@ describe('GitRepositoryAsync-js', () => { // See the comment in the 'buffer events' beforeEach for why we need to do // this. - let repository = atom.project.getRepositories()[0] + const repository = atom.project.getRepositories()[0].async waitsForPromise(() => repository.refreshStatus()) }) @@ -389,13 +392,13 @@ describe('GitRepositoryAsync-js', () => { project2 = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) project2.deserialize(atom.project.serialize(), atom.deserializers) - let buffer = project2.getBuffers()[0] + const buffer = project2.getBuffers()[0] waitsFor(() => buffer.loaded) runs(() => { buffer.append('changes') - let statusHandler = jasmine.createSpy('statusHandler') + const statusHandler = jasmine.createSpy('statusHandler') project2.getRepositories()[0].async.onDidChangeStatus(statusHandler) buffer.save() @@ -418,20 +421,20 @@ describe('GitRepositoryAsync-js', () => { // This is a change in implementation from the git-utils version it('just returns path if workdir is not provided', () => { - let _path = '/foo/bar/baz.txt' - let relPath = repository.relativize(_path) + const _path = '/foo/bar/baz.txt' + const relPath = repository.relativize(_path) expect(_path).toEqual(relPath) }) it('relativizes a repo path', () => { - let workdir = '/tmp/foo/bar/baz/' - let relativizedPath = repository.relativize(`${workdir}a/b.txt`, workdir) + const workdir = '/tmp/foo/bar/baz/' + const relativizedPath = repository.relativize(`${workdir}a/b.txt`, workdir) expect(relativizedPath).toBe('a/b.txt') }) it("doesn't require workdir to end in a slash", () => { - let workdir = '/tmp/foo/bar/baz' - let relativizedPath = repository.relativize(`${workdir}/a/b.txt`, workdir) + const workdir = '/tmp/foo/bar/baz' + const relativizedPath = repository.relativize(`${workdir}/a/b.txt`, workdir) expect(relativizedPath).toBe('a/b.txt') }) }) From 5fa8d6d5dbfaa60a1d82da867d60a5079b853482 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 1 Dec 2015 16:45:09 -0500 Subject: [PATCH 134/502] Update the overrides. --- build/tasks/license-overrides.coffee | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/build/tasks/license-overrides.coffee b/build/tasks/license-overrides.coffee index 173e41144..19f32fb84 100644 --- a/build/tasks/license-overrides.coffee +++ b/build/tasks/license-overrides.coffee @@ -124,10 +124,12 @@ module.exports = THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ 'tweetnacl@0.13.2': - license: 'CC0-1.0' + repository: 'https://github.com/dchest/tweetnacl-js' + license: 'Public Domain' source: 'https://github.com/dchest/tweetnacl-js/blob/2f328394f74d83564634fb89ea2798caa3a4edb9/README.md says public domain.' 'json-schema@0.2.2': - license: 'AFLv2.1' + repository: 'https://github.com/kriszyp/json-schema' + license: 'BSD' source: 'README links to https://github.com/dojo/dojo/blob/8b6a5e4c42f9cf777dd39eaae8b188e0ebb59a4c/LICENSE' sourceText: """ Dojo is available under *either* the terms of the modified BSD license *or* the From ca2fc8dbb0c86ec91df152792aa3f1efbc55912b Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 1 Dec 2015 17:39:58 -0500 Subject: [PATCH 135/502] Make sure we're testing the right thing. --- spec/git-repository-async-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index e6bdd801e..c0540f7b7 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -336,7 +336,7 @@ describe('GitRepositoryAsync-js', () => { buffer.reload() waitsFor(() => reloadHandler.callCount > 0) - runs(() => expect(statusHandler.callCount).toBe(1)) + runs(() => expect(reloadHandler.callCount).toBe(1)) }) }) From 36604141b91ab585cce1935898451f134cc4a2d9 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 1 Dec 2015 17:41:51 -0500 Subject: [PATCH 136/502] Let's use a separate method for side-effecting. --- src/git-repository-async.js | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index b6ac9c08a..4f74cf620 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -83,7 +83,7 @@ module.exports = class GitRepositoryAsync { checkoutOptions.checkoutStrategy = Git.Checkout.STRATEGY.FORCE | Git.Checkout.STRATEGY.DISABLE_PATHSPEC_MATCH return Git.Checkout.head(repo, checkoutOptions) }) - .then(() => this.getPathStatus(_path)) + .then(() => this.refreshStatusForPath(_path)) } checkoutHeadForEditor (editor) { @@ -100,9 +100,17 @@ module.exports = class GitRepositoryAsync { }).then(filePath => this.checkoutHead(filePath)) } - // Returns a Promise that resolves to the status bit of a given path if it has - // one, otherwise 'current'. - getPathStatus (_path) { + // Refresh the status bit for the given path. + // + // Note that if the status of the path has changed, this will emit a + // 'did-change-status' event. + // + // path :: String + // The path whose status should be refreshed. + // + // Returns :: Promise + // The refreshed status bit for the path. + refreshStatusForPath (_path) { let relativePath return this.repoPromise .then(repo => { @@ -112,14 +120,21 @@ module.exports = class GitRepositoryAsync { .then(statuses => { const cachedStatus = this.pathStatusCache[relativePath] || 0 const status = statuses[0] ? statuses[0].statusBit() : Git.Status.STATUS.CURRENT - if (status !== cachedStatus && this.emitter != null) { + if (status !== cachedStatus) { + this.pathStatusCache[relativePath] = status this.emitter.emit('did-change-status', {path: _path, pathStatus: status}) } - this.pathStatusCache[relativePath] = status + return status }) } + // Returns a Promise that resolves to the status bit of a given path if it has + // one, otherwise 'current'. + getPathStatus (_path) { + return this.refreshStatusForPath(_path) + } + // Get the status of a directory in the repository's working directory. // // * `directoryPath` The {String} path to check. @@ -192,9 +207,7 @@ module.exports = class GitRepositoryAsync { const getBufferPathStatus = () => { const _path = buffer.getPath() if (_path) { - // We don't need to do anything with this promise, we just want the - // emitted event side effect - this.getPathStatus(_path) + this.refreshStatusForPath(_path) } } From 1e07b8df0553572369c4f8c2ea8249e646573be9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 2 Dec 2015 09:13:06 +0100 Subject: [PATCH 137/502] Handle position between rows correctly --- spec/line-top-index-spec.js | 7 ++++++- src/linear-line-top-index.js | 11 ++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/spec/line-top-index-spec.js b/spec/line-top-index-spec.js index 9a9e33e3d..f2c3f9ca5 100644 --- a/spec/line-top-index-spec.js +++ b/spec/line-top-index-spec.js @@ -130,8 +130,13 @@ describe("LineTopIndex", function () { expect(lineTopIndex.rowForTopPixelPosition(70)).toBe(4) expect(lineTopIndex.rowForTopPixelPosition(80)).toBe(5) expect(lineTopIndex.rowForTopPixelPosition(90)).toBe(5) - expect(lineTopIndex.rowForTopPixelPosition(100)).toBe(6) + expect(lineTopIndex.rowForTopPixelPosition(95)).toBe(5) + expect(lineTopIndex.rowForTopPixelPosition(105)).toBe(6) expect(lineTopIndex.rowForTopPixelPosition(110)).toBe(6) + expect(lineTopIndex.rowForTopPixelPosition(160)).toBe(11) + expect(lineTopIndex.rowForTopPixelPosition(166)).toBe(12) + expect(lineTopIndex.rowForTopPixelPosition(170)).toBe(12) + expect(lineTopIndex.rowForTopPixelPosition(240)).toBe(12) lineTopIndex.removeBlock(block1) lineTopIndex.removeBlock(block3) diff --git a/src/linear-line-top-index.js b/src/linear-line-top-index.js index 638997d49..7d84058d1 100644 --- a/src/linear-line-top-index.js +++ b/src/linear-line-top-index.js @@ -73,11 +73,16 @@ class LineTopIndex { let lastRow = 0 let lastTop = 0 for (let block of this.blocks) { - blocksHeight += block.height + let nextBlocksHeight = blocksHeight + block.height let linesHeight = block.row * this.defaultLineHeight - if (blocksHeight + linesHeight > top) { - break + if (nextBlocksHeight + linesHeight > top) { + while (lastRow < block.row && lastTop + this.defaultLineHeight / 2 < top) { + lastTop += this.defaultLineHeight + lastRow++ + } + return lastRow } else { + blocksHeight = nextBlocksHeight lastRow = block.row lastTop = blocksHeight + linesHeight } From 60b1d20667b6855b6b9ff27c5513fc738af02045 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 2 Dec 2015 09:55:47 +0100 Subject: [PATCH 138/502] :fire: Remove old code --- src/linear-line-top-index.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/linear-line-top-index.js b/src/linear-line-top-index.js index 7d84058d1..727c3b9af 100644 --- a/src/linear-line-top-index.js +++ b/src/linear-line-top-index.js @@ -39,7 +39,6 @@ class LineTopIndex { } splice (startRow, oldExtent, newExtent) { - let blocksHeight = 0 this.blocks.forEach(function (block) { if (block.row >= startRow) { if (block.row >= startRow + oldExtent) { @@ -48,9 +47,6 @@ class LineTopIndex { block.row = startRow + newExtent // invalidate marker? } - - block.top = block.row * this.defaultLineHeight + blocksHeight - blocksHeight += block.height } }) From 1f20ab517045396638ab3395e35511207f96cb40 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 2 Dec 2015 14:49:36 +0100 Subject: [PATCH 139/502] Use LinearLineTopIndex in BlockDecorationsPresenter --- src/block-decorations-presenter.js | 150 ++++++++++++----------------- src/linear-line-top-index.js | 16 ++- src/text-editor-presenter.coffee | 7 +- 3 files changed, 78 insertions(+), 95 deletions(-) diff --git a/src/block-decorations-presenter.js b/src/block-decorations-presenter.js index 762372b15..4d787c25f 100644 --- a/src/block-decorations-presenter.js +++ b/src/block-decorations-presenter.js @@ -1,6 +1,7 @@ /** @babel */ const {CompositeDisposable, Emitter} = require('event-kit') +const LineTopIndex = require('./linear-line-top-index') module.exports = class BlockDecorationsPresenter { @@ -8,14 +9,11 @@ class BlockDecorationsPresenter { this.model = model this.disposables = new CompositeDisposable() this.emitter = new Emitter() - this.decorationsByScreenRow = new Map - this.heightByScreenRow = new Map - this.screenRowByDecoration = new Map - this.dimensionsByDecoration = new Map - this.moveOperationsByDecoration = new Map - this.addOperationsByDecoration = new Map - this.changeOperationsByDecoration = new Map this.firstUpdate = true + this.lineTopIndex = new LineTopIndex + this.blocksByDecoration = new Map + this.decorationsByBlock = new Map + this.observedDecorations = new Set this.observeModel() } @@ -28,10 +26,21 @@ class BlockDecorationsPresenter { return this.emitter.on("did-update-state", callback) } + setLineHeight (lineHeight) { + this.lineTopIndex.setDefaultLineHeight(lineHeight) + } + observeModel () { - this.disposables.add( - this.model.onDidAddDecoration((decoration) => this.observeDecoration(decoration)) - ) + this.lineTopIndex.setMaxRow(this.model.getScreenLineCount()) + this.lineTopIndex.setDefaultLineHeight(this.model.getLineHeightInPixels()) + this.disposables.add(this.model.onDidAddDecoration((decoration) => { + this.observeDecoration(decoration) + })) + this.disposables.add(this.model.onDidChange(({start, end, screenDelta}) => { + let oldExtent = end - start + let newExtent = Math.max(0, end - start + screenDelta) + this.lineTopIndex.splice(start, oldExtent, newExtent) + })) } update () { @@ -44,101 +53,57 @@ class BlockDecorationsPresenter { } fullUpdate () { - this.decorationsByScreenRow.clear() - this.screenRowByDecoration.clear() - this.moveOperationsByDecoration.clear() - this.addOperationsByDecoration.clear() - for (let decoration of this.model.getDecorations({type: "block"})) { - let screenRow = decoration.getMarker().getHeadScreenPosition().row - this.addDecorationToScreenRow(screenRow, decoration) this.observeDecoration(decoration) } } incrementalUpdate () { - for (let [changedDecoration] of this.changeOperationsByDecoration) { - let screenRow = changedDecoration.getMarker().getHeadScreenPosition().row - this.recalculateScreenRowHeight(screenRow) - } - - for (let [addedDecoration] of this.addOperationsByDecoration) { - let screenRow = addedDecoration.getMarker().getHeadScreenPosition().row - this.addDecorationToScreenRow(screenRow, addedDecoration) - } - - for (let [movedDecoration, moveOperations] of this.moveOperationsByDecoration) { - let {oldHeadScreenPosition} = moveOperations[0] - let {newHeadScreenPosition} = moveOperations[moveOperations.length - 1] - this.removeDecorationFromScreenRow(oldHeadScreenPosition.row, movedDecoration) - this.addDecorationToScreenRow(newHeadScreenPosition.row, movedDecoration) - } - - this.addOperationsByDecoration.clear() - this.moveOperationsByDecoration.clear() - this.changeOperationsByDecoration.clear() } setDimensionsForDecoration (decoration, width, height) { - this.changeOperationsByDecoration.set(decoration, true) - this.dimensionsByDecoration.set(decoration, {width, height}) + let block = this.blocksByDecoration.get(decoration) + if (block) { + this.lineTopIndex.resizeBlock(block, height) + } else { + this.observeDecoration(decoration) + block = this.blocksByDecoration.get(decoration) + this.lineTopIndex.resizeBlock(block, height) + } + this.emitter.emit("did-update-state") } heightForScreenRow (screenRow) { - return this.heightByScreenRow.get(screenRow) || 0 - } - - addDecorationToScreenRow (screenRow, decoration) { - let decorations = this.decorationsForScreenRow(screenRow) - if (!decorations.has(decoration)) { - decorations.add(decoration) - this.screenRowByDecoration.set(decoration, screenRow) - this.recalculateScreenRowHeight(screenRow) - } - } - - removeDecorationFromScreenRow (screenRow, decoration) { - if (!Number.isInteger(screenRow) || !decoration) { - return - } - - let decorations = this.decorationsForScreenRow(screenRow) - if (decorations.has(decoration)) { - decorations.delete(decoration) - this.recalculateScreenRowHeight(screenRow) - } + return this.lineTopIndex.bottomPixelPositionForRow(screenRow) - this.lineTopIndex.topPixelPositionForRow(screenRow) } decorationsForScreenRow (screenRow) { - if (!this.decorationsByScreenRow.has(screenRow)) { - this.decorationsByScreenRow.set(screenRow, new Set()) - } - - return this.decorationsByScreenRow.get(screenRow) + let blocks = this.lineTopIndex.allBlocks().filter((block) => block.row == screenRow) + return blocks.map((block) => this.decorationsByBlock.get(block.id)).filter((decoration) => decoration) } getAllDecorationsByScreenRow () { - return this.decorationsByScreenRow - } - - getDecorationDimensions (decoration) { - return this.dimensionsByDecoration.get(decoration) || {width: 0, height: 0} - } - - recalculateScreenRowHeight (screenRow) { - let height = 0 - for (let decoration of this.decorationsForScreenRow(screenRow)) { - height += this.getDecorationDimensions(decoration).height + let blocks = this.lineTopIndex.allBlocks() + let decorationsByScreenRow = new Map + for (let block of blocks) { + let decoration = this.decorationsByBlock.get(block.id) + if (decoration) { + let decorations = decorationsByScreenRow.get(block.row) || [] + decorations.push(decoration) + decorationsByScreenRow.set(block.row, decorations) + } } - this.heightByScreenRow.set(screenRow, height) + + return decorationsByScreenRow } observeDecoration (decoration) { - if (!decoration.isType("block")) { + if (!decoration.isType("block") || this.observedDecorations.has(decoration)) { return } + // TODO: change this with a "on manual did change" event. let didMoveDisposable = decoration.getMarker().onDidChange((markerEvent) => { this.didMoveDecoration(decoration, markerEvent) }) @@ -146,31 +111,36 @@ class BlockDecorationsPresenter { let didDestroyDisposable = decoration.onDidDestroy(() => { didMoveDisposable.dispose() didDestroyDisposable.dispose() + this.observedDecorations.delete(decoration) this.didDestroyDecoration(decoration) }) this.didAddDecoration(decoration) + this.observedDecorations.add(decoration) } didAddDecoration (decoration) { - this.addOperationsByDecoration.set(decoration, true) + let screenRow = decoration.getMarker().getHeadScreenPosition().row + let block = this.lineTopIndex.insertBlock(screenRow, 0) + this.decorationsByBlock.set(block, decoration) + this.blocksByDecoration.set(decoration, block) this.emitter.emit("did-update-state") } - didMoveDecoration (decoration, markerEvent) { - let moveOperations = this.moveOperationsByDecoration.get(decoration) || [] - moveOperations.push(markerEvent) - this.moveOperationsByDecoration.set(decoration, moveOperations) + didMoveDecoration (decoration, {oldHeadScreenPosition, newHeadScreenPosition}) { + let block = this.blocksByDecoration.get(decoration) + let newScreenRow = decoration.getMarker().getHeadScreenPosition().row + this.lineTopIndex.moveBlock(block, newScreenRow) this.emitter.emit("did-update-state") } didDestroyDecoration (decoration) { - this.moveOperationsByDecoration.delete(decoration) - this.addOperationsByDecoration.delete(decoration) - - this.removeDecorationFromScreenRow( - this.screenRowByDecoration.get(decoration), decoration - ) + let block = this.blocksByDecoration.get(decoration) + if (block) { + this.lineTopIndex.removeBlock(block) + this.blocksByDecoration.delete(decoration) + this.decorationsByBlock.delete(block) + } this.emitter.emit("did-update-state") } } diff --git a/src/linear-line-top-index.js b/src/linear-line-top-index.js index 727c3b9af..1f4a62fb9 100644 --- a/src/linear-line-top-index.js +++ b/src/linear-line-top-index.js @@ -3,7 +3,7 @@ module.exports = class LineTopIndex { constructor () { - this.idCounter = 0 + this.idCounter = 1 this.blocks = [] this.maxRow = 0 this.defaultLineHeight = 0 @@ -24,13 +24,21 @@ class LineTopIndex { return id } - changeBlockHeight (id, height) { + resizeBlock (id, height) { let block = this.blocks.find((block) => block.id == id) if (block) { block.height = height } } + moveBlock (id, newRow) { + let block = this.blocks.find((block) => block.id == id) + if (block) { + block.row = newRow + this.blocks.sort((a, b) => a.row - b.row) + } + } + removeBlock (id) { let index = this.blocks.findIndex((block) => block.id == id) if (index != -1) { @@ -38,6 +46,10 @@ class LineTopIndex { } } + allBlocks () { + return this.blocks + } + splice (startRow, oldExtent, newExtent) { this.blocks.forEach(function (block) { if (block.row >= startRow) { diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 91ff18012..41209d128 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -503,7 +503,7 @@ class TextEditorPresenter lineState = tileState.lines[line.id] lineState.screenRow = screenRow lineState.decorationClasses = @lineDecorationClassesForRow(screenRow) - lineState.hasBlockDecorations = blockDecorations.size isnt 0 + lineState.hasBlockDecorations = blockDecorations.length isnt 0 else tileState.lines[line.id] = screenRow: screenRow @@ -520,7 +520,7 @@ class TextEditorPresenter tabLength: line.tabLength fold: line.fold decorationClasses: @lineDecorationClassesForRow(screenRow) - hasBlockDecorations: blockDecorations.size isnt 0 + hasBlockDecorations: blockDecorations.length isnt 0 for id, line of tileState.lines delete tileState.lines[id] unless visibleLineIds.hasOwnProperty(id) @@ -1140,6 +1140,7 @@ class TextEditorPresenter @lineHeight = lineHeight @restoreScrollTopIfNeeded() @model.setLineHeightInPixels(lineHeight) + @blockDecorationsPresenter.setLineHeight(lineHeight) @shouldUpdateHeightState = true @shouldUpdateHorizontalScrollState = true @shouldUpdateVerticalScrollState = true @@ -1222,7 +1223,7 @@ class TextEditorPresenter @state.content.blockDecorations = {} @blockDecorationsPresenter.getAllDecorationsByScreenRow().forEach (decorations, screenRow) => - decorations.forEach (decoration) => + for decoration in decorations @state.content.blockDecorations[decoration.id] = {decoration, screenRow} updateLineDecorations: -> From f30e4ccc9de3a437d030ea74388bb04c0f6b69b7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 2 Dec 2015 16:01:55 +0100 Subject: [PATCH 140/502] Use the new LineTopIndex in TextEditorPresenter --- spec/fake-lines-yardstick.coffee | 41 +++--------------------- spec/text-editor-component-spec.js | 12 +++---- spec/text-editor-presenter-spec.coffee | 5 ++- src/block-decorations-presenter.js | 4 +-- src/linear-line-top-index.js | 15 +++++++-- src/lines-yardstick.coffee | 43 +++----------------------- src/text-editor-component.coffee | 6 ++-- src/text-editor-presenter.coffee | 41 ++++++++++-------------- 8 files changed, 54 insertions(+), 113 deletions(-) diff --git a/spec/fake-lines-yardstick.coffee b/spec/fake-lines-yardstick.coffee index e6d1f4f53..3d2ebe7d5 100644 --- a/spec/fake-lines-yardstick.coffee +++ b/spec/fake-lines-yardstick.coffee @@ -2,7 +2,7 @@ module.exports = class FakeLinesYardstick - constructor: (@model, @presenter) -> + constructor: (@model, @presenter, @lineTopIndex) -> @characterWidthsByScope = {} prepareScreenRowsForMeasurement: -> @@ -31,7 +31,7 @@ class FakeLinesYardstick targetColumn = screenPosition.column baseCharacterWidth = @model.getDefaultCharWidth() - top = @bottomPixelPositionForRow(targetRow) + top = @lineTopIndex.bottomPixelPositionForRow(targetRow) left = 0 column = 0 @@ -60,48 +60,15 @@ class FakeLinesYardstick {top, left} - rowForTopPixelPosition: (position, floor = true) -> - top = 0 - for tileStartRow in [0..@model.getScreenLineCount()] by @presenter.getTileSize() - tileEndRow = Math.min(tileStartRow + @presenter.getTileSize(), @model.getScreenLineCount()) - for row in [tileStartRow...tileEndRow] by 1 - nextTop = top + @presenter.getScreenRowHeight(row) - if floor - return row if nextTop > position - else - return row if top >= position - top = nextTop - @model.getScreenLineCount() - - topPixelPositionForRow: (targetRow) -> - top = 0 - for row in [0..targetRow] - return top if targetRow is row - top += @presenter.getScreenRowHeight(row) - top - - bottomPixelPositionForRow: (targetRow) -> - @topPixelPositionForRow(targetRow + 1) - @model.getLineHeightInPixels() - - topPixelPositionForRows: (startRow, endRow, step) -> - results = {} - top = 0 - for tileStartRow in [0..endRow] by step - tileEndRow = Math.min(tileStartRow + step, @model.getScreenLineCount()) - results[tileStartRow] = top - for row in [tileStartRow...tileEndRow] by 1 - top += @presenter.getScreenRowHeight(row) - results - pixelRectForScreenRange: (screenRange) -> if screenRange.end.row > screenRange.start.row top = @pixelPositionForScreenPosition(screenRange.start).top left = 0 - height = @topPixelPositionForRow(screenRange.end.row + 1) - top + height = @lineTopIndex.topPixelPositionForRow(screenRange.end.row + 1) - top width = @presenter.getScrollWidth() else {top, left} = @pixelPositionForScreenPosition(screenRange.start, false) - height = @topPixelPositionForRow(screenRange.end.row + 1) - top + height = @lineTopIndex.topPixelPositionForRow(screenRange.end.row + 1) - top width = @pixelPositionForScreenPosition(screenRange.end, false).left - left {top, left, width, height} diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 18ad030c0..85d404b1e 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -1707,13 +1707,13 @@ describe('TextEditorComponent', function () { await nextViewUpdatePromise() - expect(component.getDomNode().querySelectorAll(".line").length).toBe(9) + expect(component.getDomNode().querySelectorAll(".line").length).toBe(7) expect(component.tileNodesForLines()[0].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + "px") expect(component.tileNodesForLines()[0].style.webkitTransform).toBe("translate3d(0px, 0px, 0px)") expect(component.tileNodesForLines()[1].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 100 + 40 + "px") expect(component.tileNodesForLines()[1].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight}px, 0px)`) - expect(component.tileNodesForLines()[2].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 120 + "px") + expect(component.tileNodesForLines()[2].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + "px") expect(component.tileNodesForLines()[2].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight + component.tileNodesForLines()[1].offsetHeight}px, 0px)`) expect(component.getTopmostDOMNode().querySelector(".decoration-1")).toBeNull() @@ -1724,7 +1724,7 @@ describe('TextEditorComponent', function () { expect(item1.getBoundingClientRect().height).toBe(0) // hidden expect(item2.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 3) expect(item3.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 5 + 40) - expect(item4.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 8 + 40 + 100) + expect(item4.getBoundingClientRect().height).toBe(0) // hidden await nextViewUpdatePromise() @@ -1734,13 +1734,13 @@ describe('TextEditorComponent', function () { await nextViewUpdatePromise() - expect(component.getDomNode().querySelectorAll(".line").length).toBe(9) + expect(component.getDomNode().querySelectorAll(".line").length).toBe(7) expect(component.tileNodesForLines()[0].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + "px") expect(component.tileNodesForLines()[0].style.webkitTransform).toBe("translate3d(0px, 0px, 0px)") expect(component.tileNodesForLines()[1].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 100 + 60 + "px") expect(component.tileNodesForLines()[1].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight}px, 0px)`) - expect(component.tileNodesForLines()[2].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 120 + "px") + expect(component.tileNodesForLines()[2].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + "px") expect(component.tileNodesForLines()[2].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight + component.tileNodesForLines()[1].offsetHeight}px, 0px)`) expect(component.getTopmostDOMNode().querySelector(".decoration-1")).toBeNull() @@ -1751,7 +1751,7 @@ describe('TextEditorComponent', function () { expect(item1.getBoundingClientRect().height).toBe(0) // hidden expect(item2.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 3) expect(item3.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 5 + 60) - expect(item4.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 8 + 60 + 100) + expect(item4.getBoundingClientRect().height).toBe(0) // hidden }) }) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 83f0d8236..0e80ed113 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -5,6 +5,7 @@ TextBuffer = require 'text-buffer' TextEditor = require '../src/text-editor' TextEditorPresenter = require '../src/text-editor-presenter' FakeLinesYardstick = require './fake-lines-yardstick' +LineTopIndex = require '../src/linear-line-top-index' describe "TextEditorPresenter", -> # These `describe` and `it` blocks mirror the structure of the ::state object. @@ -26,12 +27,14 @@ describe "TextEditorPresenter", -> buffer.destroy() buildPresenterWithoutMeasurements = (params={}) -> + lineTopIndex = new LineTopIndex _.defaults params, model: editor config: atom.config contentFrameWidth: 500 + lineTopIndex: lineTopIndex presenter = new TextEditorPresenter(params) - presenter.setLinesYardstick(new FakeLinesYardstick(editor, presenter)) + presenter.setLinesYardstick(new FakeLinesYardstick(editor, presenter, lineTopIndex)) presenter buildPresenter = (params={}) -> diff --git a/src/block-decorations-presenter.js b/src/block-decorations-presenter.js index 4d787c25f..80f4de6f4 100644 --- a/src/block-decorations-presenter.js +++ b/src/block-decorations-presenter.js @@ -5,12 +5,12 @@ const LineTopIndex = require('./linear-line-top-index') module.exports = class BlockDecorationsPresenter { - constructor (model) { + constructor (model, lineTopIndex) { this.model = model this.disposables = new CompositeDisposable() this.emitter = new Emitter() this.firstUpdate = true - this.lineTopIndex = new LineTopIndex + this.lineTopIndex = lineTopIndex this.blocksByDecoration = new Map this.decorationsByBlock = new Map this.observedDecorations = new Set diff --git a/src/linear-line-top-index.js b/src/linear-line-top-index.js index 1f4a62fb9..7b6103f55 100644 --- a/src/linear-line-top-index.js +++ b/src/linear-line-top-index.js @@ -76,7 +76,7 @@ class LineTopIndex { return this.topPixelPositionForRow(row + 1) - this.defaultLineHeight } - rowForTopPixelPosition (top) { + rowForTopPixelPosition (top, roundingStrategy='round') { let blocksHeight = 0 let lastRow = 0 let lastTop = 0 @@ -97,7 +97,16 @@ class LineTopIndex { } let remainingHeight = Math.max(0, top - lastTop) - let remainingRows = Math.round(remainingHeight / this.defaultLineHeight) - return Math.min(this.maxRow, lastRow + remainingRows) + let remainingRows = Math.min(this.maxRow, lastRow + remainingHeight / this.defaultLineHeight) + switch (roundingStrategy) { + case "round": + return Math.round(remainingRows) + case "floor": + return Math.floor(remainingRows) + case "ceil": + return Math.ceil(remainingRows) + default: + throw new Error(`Cannot use '${roundingStrategy}' as a rounding strategy!`) + } } } diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index bb2c7faba..c1faa6399 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -3,7 +3,7 @@ TokenIterator = require './token-iterator' module.exports = class LinesYardstick - constructor: (@model, @presenter, @lineNodesProvider, grammarRegistry) -> + constructor: (@model, @presenter, @lineNodesProvider, @lineTopIndex, grammarRegistry) -> @tokenIterator = new TokenIterator({grammarRegistry}) @rangeForMeasurement = document.createRange() @invalidateCache() @@ -22,7 +22,7 @@ class LinesYardstick targetTop = pixelPosition.top targetLeft = pixelPosition.left defaultCharWidth = @model.getDefaultCharWidth() - row = @rowForTopPixelPosition(targetTop) + row = @lineTopIndex.rowForTopPixelPosition(targetTop, 'floor') targetLeft = 0 if targetTop < 0 targetLeft = Infinity if row > @model.getLastScreenRow() @@ -90,7 +90,7 @@ class LinesYardstick @prepareScreenRowsForMeasurement([targetRow]) unless measureVisibleLinesOnly - top = @bottomPixelPositionForRow(targetRow) + top = @lineTopIndex.bottomPixelPositionForRow(targetRow) left = @leftPixelPositionForScreenPosition(targetRow, targetColumn) @clearScreenRowsForMeasurement() unless measureVisibleLinesOnly @@ -172,48 +172,15 @@ class LinesYardstick left + width - offset - rowForTopPixelPosition: (position, floor = true) -> - top = 0 - for tileStartRow in [0..@model.getScreenLineCount()] by @presenter.getTileSize() - tileEndRow = Math.min(tileStartRow + @presenter.getTileSize(), @model.getScreenLineCount()) - for row in [tileStartRow...tileEndRow] by 1 - nextTop = top + @presenter.getScreenRowHeight(row) - if floor - return row if nextTop > position - else - return row if top >= position - top = nextTop - @model.getScreenLineCount() - - topPixelPositionForRow: (targetRow) -> - top = 0 - for row in [0..targetRow] - return top if targetRow is row - top += @presenter.getScreenRowHeight(row) - top - - bottomPixelPositionForRow: (targetRow) -> - @topPixelPositionForRow(targetRow + 1) - @model.getLineHeightInPixels() - - topPixelPositionForRows: (startRow, endRow, step) -> - results = {} - top = 0 - for tileStartRow in [0..endRow] by step - tileEndRow = Math.min(tileStartRow + step, @model.getScreenLineCount()) - results[tileStartRow] = top - for row in [tileStartRow...tileEndRow] by 1 - top += @presenter.getScreenRowHeight(row) - results - pixelRectForScreenRange: (screenRange, measureVisibleLinesOnly) -> if screenRange.end.row > screenRange.start.row top = @pixelPositionForScreenPosition(screenRange.start, true, measureVisibleLinesOnly).top left = 0 - height = @topPixelPositionForRow(screenRange.end.row + 1) - top + height = @lineTopIndex.topPixelPositionForRow(screenRange.end.row + 1) - top width = @presenter.getScrollWidth() else {top, left} = @pixelPositionForScreenPosition(screenRange.start, false, measureVisibleLinesOnly) - height = @topPixelPositionForRow(screenRange.end.row + 1) - top + height = @lineTopIndex.topPixelPositionForRow(screenRange.end.row + 1) - top width = @pixelPositionForScreenPosition(screenRange.end, false, measureVisibleLinesOnly).left - left {top, left, width, height} diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index faf6a6715..7fc51b9a4 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -14,6 +14,7 @@ OverlayManager = require './overlay-manager' DOMElementPool = require './dom-element-pool' LinesYardstick = require './lines-yardstick' BlockDecorationsComponent = require './block-decorations-component' +LineTopIndex = require './linear-line-top-index' module.exports = class TextEditorComponent @@ -49,6 +50,7 @@ class TextEditorComponent @observeConfig() @setScrollSensitivity(@config.get('editor.scrollSensitivity')) + lineTopIndex = new LineTopIndex(@editor) @presenter = new TextEditorPresenter model: @editor tileSize: tileSize @@ -56,11 +58,11 @@ class TextEditorComponent cursorBlinkResumeDelay: @cursorBlinkResumeDelay stoppedScrollingDelay: 200 config: @config + lineTopIndex: lineTopIndex @presenter.onDidUpdateState(@requestUpdate) @domElementPool = new DOMElementPool - @domNode = document.createElement('div') if @useShadowDOM @domNode.classList.add('editor-contents--private') @@ -85,7 +87,7 @@ class TextEditorComponent @linesComponent = new LinesComponent({@presenter, @hostElement, @useShadowDOM, @domElementPool, @assert, @grammars}) @scrollViewNode.appendChild(@linesComponent.getDomNode()) - @linesYardstick = new LinesYardstick(@editor, @presenter, @linesComponent, @grammars) + @linesYardstick = new LinesYardstick(@editor, @presenter, @linesComponent, lineTopIndex, @grammars) @presenter.setLinesYardstick(@linesYardstick) @horizontalScrollbarComponent = new ScrollbarComponent({orientation: 'horizontal', onScroll: @onHorizontalScroll}) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 41209d128..29428c1d4 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -14,7 +14,7 @@ class TextEditorPresenter minimumReflowInterval: 200 constructor: (params) -> - {@model, @config} = params + {@model, @config, @lineTopIndex} = params {@cursorBlinkPeriod, @cursorBlinkResumeDelay, @stoppedScrollingDelay, @tileSize} = params {@contentFrameWidth} = params @@ -29,7 +29,7 @@ class TextEditorPresenter @lineDecorationsByScreenRow = {} @lineNumberDecorationsByScreenRow = {} @customGutterDecorationsByGutterName = {} - @blockDecorationsPresenter = new BlockDecorationsPresenter(@model) + @blockDecorationsPresenter = new BlockDecorationsPresenter(@model, @lineTopIndex) @screenRowsToMeasure = [] @transferMeasurementsToModel() @transferMeasurementsFromModel() @@ -434,12 +434,6 @@ class TextEditorPresenter screenRowIndex = screenRows.length - 1 zIndex = 0 - tilesPositions = @linesYardstick.topPixelPositionForRows( - @tileForRow(startRow), - @tileForRow(endRow) + @tileSize, - @tileSize - ) - for tileStartRow in [@tileForRow(endRow)..@tileForRow(startRow)] by -@tileSize tileEndRow = @constrainRow(tileStartRow + @tileSize) rowsWithinTile = [] @@ -452,8 +446,9 @@ class TextEditorPresenter continue if rowsWithinTile.length is 0 - top = Math.round(tilesPositions[tileStartRow]) - height = Math.round(tilesPositions[tileEndRow] - top) + top = Math.round(@lineTopIndex.topPixelPositionForRow(tileStartRow)) + bottom = Math.round(@lineTopIndex.topPixelPositionForRow(tileEndRow)) + height = bottom - top tile = @state.content.tiles[tileStartRow] ?= {} tile.top = top - @scrollTop @@ -667,8 +662,8 @@ class TextEditorPresenter continue unless @gutterIsVisible(gutter) for decorationId, {properties, screenRange} of @customGutterDecorationsByGutterName[gutterName] - top = @linesYardstick.topPixelPositionForRow(screenRange.start.row) - bottom = @linesYardstick.topPixelPositionForRow(screenRange.end.row + 1) + top = @lineTopIndex.topPixelPositionForRow(screenRange.start.row) + bottom = @lineTopIndex.topPixelPositionForRow(screenRange.end.row + 1) @customGutterDecorations[gutterName][decorationId] = top: top height: bottom - top @@ -735,12 +730,12 @@ class TextEditorPresenter updateStartRow: -> return unless @scrollTop? and @lineHeight? - @startRow = Math.max(0, @linesYardstick.rowForTopPixelPosition(@scrollTop)) + @startRow = Math.max(0, @lineTopIndex.rowForTopPixelPosition(@scrollTop, "floor")) updateEndRow: -> return unless @scrollTop? and @lineHeight? and @height? - @endRow = @linesYardstick.rowForTopPixelPosition(@scrollTop + @height + @lineHeight, false) + @endRow = @lineTopIndex.rowForTopPixelPosition(@scrollTop + @height + @lineHeight, 'ceil') updateRowsPerPage: -> rowsPerPage = Math.floor(@getClientHeight() / @lineHeight) @@ -775,9 +770,7 @@ class TextEditorPresenter updateVerticalDimensions: -> if @lineHeight? oldContentHeight = @contentHeight - @contentHeight = Math.round( - @linesYardstick.topPixelPositionForRow(@model.getScreenLineCount()) - ) + @contentHeight = Math.round(@lineTopIndex.topPixelPositionForRow(Infinity)) if @contentHeight isnt oldContentHeight @updateHeight() @@ -1138,9 +1131,9 @@ class TextEditorPresenter setLineHeight: (lineHeight) -> unless @lineHeight is lineHeight @lineHeight = lineHeight + @model.setLineHeightInPixels(@lineHeight) + @lineTopIndex.setDefaultLineHeight(@lineHeight) @restoreScrollTopIfNeeded() - @model.setLineHeightInPixels(lineHeight) - @blockDecorationsPresenter.setLineHeight(lineHeight) @shouldUpdateHeightState = true @shouldUpdateHorizontalScrollState = true @shouldUpdateVerticalScrollState = true @@ -1348,7 +1341,7 @@ class TextEditorPresenter screenRange.end.column = 0 repositionRegionWithinTile: (region, tileStartRow) -> - region.top += @scrollTop - @linesYardstick.topPixelPositionForRow(tileStartRow) + region.top += @scrollTop - @lineTopIndex.topPixelPositionForRow(tileStartRow) region.left += @scrollLeft buildHighlightRegions: (screenRange) -> @@ -1500,7 +1493,7 @@ class TextEditorPresenter @emitDidUpdateState() didChangeFirstVisibleScreenRow: (screenRow) -> - @updateScrollTop(@linesYardstick.topPixelPositionForRow(screenRow)) + @updateScrollTop(@lineTopIndex.topPixelPositionForRow(screenRow)) getVerticalScrollMarginInPixels: -> Math.round(@model.getVerticalScrollMargin() * @lineHeight) @@ -1521,8 +1514,8 @@ class TextEditorPresenter verticalScrollMarginInPixels = @getVerticalScrollMarginInPixels() - top = @linesYardstick.topPixelPositionForRow(screenRange.start.row) - bottom = @linesYardstick.topPixelPositionForRow(screenRange.end.row + 1) + top = @lineTopIndex.topPixelPositionForRow(screenRange.start.row) + bottom = @lineTopIndex.topPixelPositionForRow(screenRange.end.row + 1) if options?.center desiredScrollCenter = (top + bottom) / 2 @@ -1594,7 +1587,7 @@ class TextEditorPresenter restoreScrollTopIfNeeded: -> unless @scrollTop? - @updateScrollTop(@linesYardstick.topPixelPositionForRow(@model.getFirstVisibleScreenRow())) + @updateScrollTop(@lineTopIndex.topPixelPositionForRow(@model.getFirstVisibleScreenRow())) restoreScrollLeftIfNeeded: -> unless @scrollLeft? From 5bcdcbeef6bb7e60f2c00119778bcd642eb78bbd Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 2 Dec 2015 16:13:44 +0100 Subject: [PATCH 141/502] :art: --- spec/text-editor-presenter-spec.coffee | 10 +++++----- src/linear-line-top-index.js | 6 ++---- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 0e80ed113..9b6f5b78e 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -191,17 +191,17 @@ describe "TextEditorPresenter", -> expect(stateFn(presenter).tiles[6].top).toBe((20 + 40) + (20 + 30) + 20 + 1) expect(stateFn(presenter).tiles[8]).toBeUndefined() - presenter.setScrollTop(21) + presenter.setScrollTop(22) expect(stateFn(presenter).tiles[0]).toBeUndefined() expect(stateFn(presenter).tiles[2].height).toBe(20 + 30) - expect(stateFn(presenter).tiles[2].top).toBe(0) + expect(stateFn(presenter).tiles[2].top).toBe(-1) expect(stateFn(presenter).tiles[4].height).toBe(20 + 40) - expect(stateFn(presenter).tiles[4].top).toBe(30 + 20) + expect(stateFn(presenter).tiles[4].top).toBe(30 + 20 - 1) expect(stateFn(presenter).tiles[6].height).toBe(20) - expect(stateFn(presenter).tiles[6].top).toBe((20 + 40) + (20 + 30)) + expect(stateFn(presenter).tiles[6].top).toBe((20 + 40) + (20 + 30) - 1) expect(stateFn(presenter).tiles[8].height).toBe(20) - expect(stateFn(presenter).tiles[8].top).toBe((20 + 40) + (20 + 30) + 20) + expect(stateFn(presenter).tiles[8].top).toBe((20 + 40) + (20 + 30) + 20 - 1) expect(stateFn(presenter).tiles[10]).toBeUndefined() it "includes state for all tiles if no external ::explicitHeight is assigned", -> diff --git a/src/linear-line-top-index.js b/src/linear-line-top-index.js index 7b6103f55..84a3bdf04 100644 --- a/src/linear-line-top-index.js +++ b/src/linear-line-top-index.js @@ -76,7 +76,7 @@ class LineTopIndex { return this.topPixelPositionForRow(row + 1) - this.defaultLineHeight } - rowForTopPixelPosition (top, roundingStrategy='round') { + rowForTopPixelPosition (top, roundingStrategy='floor') { let blocksHeight = 0 let lastRow = 0 let lastTop = 0 @@ -84,7 +84,7 @@ class LineTopIndex { let nextBlocksHeight = blocksHeight + block.height let linesHeight = block.row * this.defaultLineHeight if (nextBlocksHeight + linesHeight > top) { - while (lastRow < block.row && lastTop + this.defaultLineHeight / 2 < top) { + while (lastRow < block.row && lastTop + this.defaultLineHeight < top) { lastTop += this.defaultLineHeight lastRow++ } @@ -99,8 +99,6 @@ class LineTopIndex { let remainingHeight = Math.max(0, top - lastTop) let remainingRows = Math.min(this.maxRow, lastRow + remainingHeight / this.defaultLineHeight) switch (roundingStrategy) { - case "round": - return Math.round(remainingRows) case "floor": return Math.floor(remainingRows) case "ceil": From e10fdc234bd3600afcae1b3d4babc6c8a0806cba Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 2 Dec 2015 16:26:10 +0100 Subject: [PATCH 142/502] :bug: Coordinate conversion is hard --- spec/line-top-index-spec.js | 8 ++++---- spec/text-editor-presenter-spec.coffee | 10 +++++----- src/linear-line-top-index.js | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/spec/line-top-index-spec.js b/spec/line-top-index-spec.js index f2c3f9ca5..62d3e1839 100644 --- a/spec/line-top-index-spec.js +++ b/spec/line-top-index-spec.js @@ -97,7 +97,7 @@ describe("LineTopIndex", function () { expect(lineTopIndex.rowForTopPixelPosition(0)).toBe(0) expect(lineTopIndex.rowForTopPixelPosition(40)).toBe(4) expect(lineTopIndex.rowForTopPixelPosition(44)).toBe(4) - expect(lineTopIndex.rowForTopPixelPosition(46)).toBe(5) + expect(lineTopIndex.rowForTopPixelPosition(46)).toBe(4) expect(lineTopIndex.rowForTopPixelPosition(50)).toBe(5) expect(lineTopIndex.rowForTopPixelPosition(120)).toBe(12) expect(lineTopIndex.rowForTopPixelPosition(130)).toBe(12) @@ -123,7 +123,7 @@ describe("LineTopIndex", function () { expect(lineTopIndex.rowForTopPixelPosition(6)).toBe(0) expect(lineTopIndex.rowForTopPixelPosition(10)).toBe(0) expect(lineTopIndex.rowForTopPixelPosition(12)).toBe(0) - expect(lineTopIndex.rowForTopPixelPosition(17)).toBe(1) + expect(lineTopIndex.rowForTopPixelPosition(17)).toBe(0) expect(lineTopIndex.rowForTopPixelPosition(20)).toBe(1) expect(lineTopIndex.rowForTopPixelPosition(30)).toBe(2) expect(lineTopIndex.rowForTopPixelPosition(40)).toBe(3) @@ -131,10 +131,10 @@ describe("LineTopIndex", function () { expect(lineTopIndex.rowForTopPixelPosition(80)).toBe(5) expect(lineTopIndex.rowForTopPixelPosition(90)).toBe(5) expect(lineTopIndex.rowForTopPixelPosition(95)).toBe(5) - expect(lineTopIndex.rowForTopPixelPosition(105)).toBe(6) + expect(lineTopIndex.rowForTopPixelPosition(106)).toBe(5) expect(lineTopIndex.rowForTopPixelPosition(110)).toBe(6) expect(lineTopIndex.rowForTopPixelPosition(160)).toBe(11) - expect(lineTopIndex.rowForTopPixelPosition(166)).toBe(12) + expect(lineTopIndex.rowForTopPixelPosition(166)).toBe(11) expect(lineTopIndex.rowForTopPixelPosition(170)).toBe(12) expect(lineTopIndex.rowForTopPixelPosition(240)).toBe(12) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 9b6f5b78e..0e80ed113 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -191,17 +191,17 @@ describe "TextEditorPresenter", -> expect(stateFn(presenter).tiles[6].top).toBe((20 + 40) + (20 + 30) + 20 + 1) expect(stateFn(presenter).tiles[8]).toBeUndefined() - presenter.setScrollTop(22) + presenter.setScrollTop(21) expect(stateFn(presenter).tiles[0]).toBeUndefined() expect(stateFn(presenter).tiles[2].height).toBe(20 + 30) - expect(stateFn(presenter).tiles[2].top).toBe(-1) + expect(stateFn(presenter).tiles[2].top).toBe(0) expect(stateFn(presenter).tiles[4].height).toBe(20 + 40) - expect(stateFn(presenter).tiles[4].top).toBe(30 + 20 - 1) + expect(stateFn(presenter).tiles[4].top).toBe(30 + 20) expect(stateFn(presenter).tiles[6].height).toBe(20) - expect(stateFn(presenter).tiles[6].top).toBe((20 + 40) + (20 + 30) - 1) + expect(stateFn(presenter).tiles[6].top).toBe((20 + 40) + (20 + 30)) expect(stateFn(presenter).tiles[8].height).toBe(20) - expect(stateFn(presenter).tiles[8].top).toBe((20 + 40) + (20 + 30) + 20 - 1) + expect(stateFn(presenter).tiles[8].top).toBe((20 + 40) + (20 + 30) + 20) expect(stateFn(presenter).tiles[10]).toBeUndefined() it "includes state for all tiles if no external ::explicitHeight is assigned", -> diff --git a/src/linear-line-top-index.js b/src/linear-line-top-index.js index 84a3bdf04..1c7fa0503 100644 --- a/src/linear-line-top-index.js +++ b/src/linear-line-top-index.js @@ -84,7 +84,7 @@ class LineTopIndex { let nextBlocksHeight = blocksHeight + block.height let linesHeight = block.row * this.defaultLineHeight if (nextBlocksHeight + linesHeight > top) { - while (lastRow < block.row && lastTop + this.defaultLineHeight < top) { + while (lastRow < block.row && lastTop + this.defaultLineHeight <= top) { lastTop += this.defaultLineHeight lastRow++ } From 5228471bc52da9575653e411de9cbda7e9f5ce19 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 2 Dec 2015 17:04:33 +0100 Subject: [PATCH 143/502] Write failing spec for measuring invisible elements --- spec/text-editor-component-spec.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 85d404b1e..3b8409469 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -1699,7 +1699,8 @@ describe('TextEditorComponent', function () { expect(item1.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 0) expect(item2.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 2 + 80) expect(item3.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 4 + 80 + 40) - expect(item4.getBoundingClientRect().height).toBe(0) // hidden + expect(item4.getBoundingClientRect().top).toBe(0) // hidden + expect(item4.getBoundingClientRect().height).toBe(120) editor.setCursorScreenPosition([0, 0]) editor.insertNewline() @@ -1713,7 +1714,7 @@ describe('TextEditorComponent', function () { expect(component.tileNodesForLines()[0].style.webkitTransform).toBe("translate3d(0px, 0px, 0px)") expect(component.tileNodesForLines()[1].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 100 + 40 + "px") expect(component.tileNodesForLines()[1].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight}px, 0px)`) - expect(component.tileNodesForLines()[2].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + "px") + expect(component.tileNodesForLines()[2].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 120 + "px") expect(component.tileNodesForLines()[2].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight + component.tileNodesForLines()[1].offsetHeight}px, 0px)`) expect(component.getTopmostDOMNode().querySelector(".decoration-1")).toBeNull() @@ -1721,10 +1722,11 @@ describe('TextEditorComponent', function () { expect(component.getTopmostDOMNode().querySelector(".decoration-3")).toBe(item3) expect(component.getTopmostDOMNode().querySelector(".decoration-4")).toBe(item4) - expect(item1.getBoundingClientRect().height).toBe(0) // hidden + expect(item1.getBoundingClientRect().height).toBe(0) // deleted expect(item2.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 3) expect(item3.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 5 + 40) - expect(item4.getBoundingClientRect().height).toBe(0) // hidden + expect(item4.getBoundingClientRect().top).toBe(0) // hidden + expect(item4.getBoundingClientRect().height).toBe(120) await nextViewUpdatePromise() @@ -1740,7 +1742,7 @@ describe('TextEditorComponent', function () { expect(component.tileNodesForLines()[0].style.webkitTransform).toBe("translate3d(0px, 0px, 0px)") expect(component.tileNodesForLines()[1].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 100 + 60 + "px") expect(component.tileNodesForLines()[1].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight}px, 0px)`) - expect(component.tileNodesForLines()[2].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + "px") + expect(component.tileNodesForLines()[2].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 120 + "px") expect(component.tileNodesForLines()[2].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight + component.tileNodesForLines()[1].offsetHeight}px, 0px)`) expect(component.getTopmostDOMNode().querySelector(".decoration-1")).toBeNull() @@ -1748,10 +1750,11 @@ describe('TextEditorComponent', function () { expect(component.getTopmostDOMNode().querySelector(".decoration-3")).toBe(item3) expect(component.getTopmostDOMNode().querySelector(".decoration-4")).toBe(item4) - expect(item1.getBoundingClientRect().height).toBe(0) // hidden + expect(item1.getBoundingClientRect().height).toBe(0) // deleted expect(item2.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 3) expect(item3.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 5 + 60) - expect(item4.getBoundingClientRect().height).toBe(0) // hidden + expect(item4.getBoundingClientRect().top).toBe(0) // hidden + expect(item4.getBoundingClientRect().height).toBe(120) // hidden }) }) From 87c8694d01111bbfe04c85395bdbff5621781c13 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 2 Dec 2015 17:21:12 +0100 Subject: [PATCH 144/502] Use ::bottomPixelPositionForRow to scroll logically --- src/text-editor-presenter.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 29428c1d4..44cf14794 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -1514,8 +1514,8 @@ class TextEditorPresenter verticalScrollMarginInPixels = @getVerticalScrollMarginInPixels() - top = @lineTopIndex.topPixelPositionForRow(screenRange.start.row) - bottom = @lineTopIndex.topPixelPositionForRow(screenRange.end.row + 1) + top = @lineTopIndex.bottomPixelPositionForRow(screenRange.start.row) + bottom = @lineTopIndex.bottomPixelPositionForRow(screenRange.end.row + 1) if options?.center desiredScrollCenter = (top + bottom) / 2 From db7a3063e94ee146b52f3a5cb12a3507dfd83ea6 Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 2 Dec 2015 11:22:40 -0500 Subject: [PATCH 145/502] Call destroy on the synchronous repo. --- spec/git-repository-async-spec.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index c0540f7b7..7611bfc55 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -366,7 +366,8 @@ describe('GitRepositoryAsync-js', () => { it('stops listening to the buffer when the repository is destroyed (regression)', async () => { const editor = await atom.workspace.open('other.txt') - repository.destroy() + const repo = atom.project.getRepositories()[0] + repo.destroy() expect(() => editor.save()).not.toThrow() }) }) From c9813d80259e77835e5038b074f6d5472a27c099 Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 2 Dec 2015 11:40:16 -0500 Subject: [PATCH 146/502] Ensure the repo is destroyed properly after testing. --- spec/git-repository-async-spec.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 7611bfc55..37d5cb898 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -413,29 +413,27 @@ describe('GitRepositoryAsync-js', () => { }) describe('GitRepositoryAsync::relativize(filePath, workdir)', () => { - let repository - beforeEach(() => { - atom.project.setPaths([copyRepository()]) - repository = atom.project.getRepositories()[0].async + const workingDirectory = copyRepository() + repo = GitRepositoryAsync.open(workingDirectory) }) // This is a change in implementation from the git-utils version it('just returns path if workdir is not provided', () => { const _path = '/foo/bar/baz.txt' - const relPath = repository.relativize(_path) + const relPath = repo.relativize(_path) expect(_path).toEqual(relPath) }) it('relativizes a repo path', () => { const workdir = '/tmp/foo/bar/baz/' - const relativizedPath = repository.relativize(`${workdir}a/b.txt`, workdir) + const relativizedPath = repo.relativize(`${workdir}a/b.txt`, workdir) expect(relativizedPath).toBe('a/b.txt') }) it("doesn't require workdir to end in a slash", () => { const workdir = '/tmp/foo/bar/baz' - const relativizedPath = repository.relativize(`${workdir}/a/b.txt`, workdir) + const relativizedPath = repo.relativize(`${workdir}/a/b.txt`, workdir) expect(relativizedPath).toBe('a/b.txt') }) }) From 9ef3ecf378734ba541476cbe070f58742ac205f5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 2 Dec 2015 18:34:57 +0100 Subject: [PATCH 147/502] Handle off-screen measurements properly --- spec/text-editor-presenter-spec.coffee | 26 +++++++++++++++++++++++++- src/block-decorations-component.coffee | 14 ++++++++++++++ src/text-editor-component.coffee | 2 ++ src/text-editor-presenter.coffee | 6 ++++-- 4 files changed, 45 insertions(+), 3 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 0e80ed113..be8ce974e 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -2106,23 +2106,27 @@ describe "TextEditorPresenter", -> blockDecoration2 = editor.addBlockDecorationForScreenRow(4, item) blockDecoration3 = editor.addBlockDecorationForScreenRow(4, item) blockDecoration4 = editor.addBlockDecorationForScreenRow(10, item) - presenter = buildPresenter(explicitHeight: 30, scrollTop: 0) + presenter = buildPresenter(explicitHeight: 30, lineHeight: 10, tileSize: 2, scrollTop: 0) expectValues stateForBlockDecoration(presenter, blockDecoration1), { decoration: blockDecoration1 screenRow: 0 + isVisible: true } expectValues stateForBlockDecoration(presenter, blockDecoration2), { decoration: blockDecoration2 screenRow: 4 + isVisible: true } expectValues stateForBlockDecoration(presenter, blockDecoration3), { decoration: blockDecoration3 screenRow: 4 + isVisible: true } expectValues stateForBlockDecoration(presenter, blockDecoration4), { decoration: blockDecoration4 screenRow: 10 + isVisible: false } waitsForStateToUpdate presenter, -> @@ -2132,36 +2136,56 @@ describe "TextEditorPresenter", -> expectValues stateForBlockDecoration(presenter, blockDecoration1), { decoration: blockDecoration1 screenRow: 4 + isVisible: true } expectValues stateForBlockDecoration(presenter, blockDecoration2), { decoration: blockDecoration2 screenRow: 8 + isVisible: false } expectValues stateForBlockDecoration(presenter, blockDecoration3), { decoration: blockDecoration3 screenRow: 8 + isVisible: false } expectValues stateForBlockDecoration(presenter, blockDecoration4), { decoration: blockDecoration4 screenRow: 14 + isVisible: false } waitsForStateToUpdate presenter, -> blockDecoration2.destroy() blockDecoration4.destroy() + presenter.setBlockDecorationDimensions(blockDecoration1, 0, 20) runs -> expectValues stateForBlockDecoration(presenter, blockDecoration1), { decoration: blockDecoration1 screenRow: 4 + isVisible: true } expect(stateForBlockDecoration(presenter, blockDecoration2)).toBeUndefined() expectValues stateForBlockDecoration(presenter, blockDecoration3), { decoration: blockDecoration3 screenRow: 8 + isVisible: false } expect(stateForBlockDecoration(presenter, blockDecoration4)).toBeUndefined() + presenter.setScrollTop(80) + + expectValues stateForBlockDecoration(presenter, blockDecoration1), { + decoration: blockDecoration1 + screenRow: 4 + isVisible: false + } + expectValues stateForBlockDecoration(presenter, blockDecoration3), { + decoration: blockDecoration3 + screenRow: 8 + isVisible: true + } + describe ".overlays", -> [item] = [] stateForOverlay = (presenter, decoration) -> diff --git a/src/block-decorations-component.coffee b/src/block-decorations-component.coffee index 0f9270660..b214fd584 100644 --- a/src/block-decorations-component.coffee +++ b/src/block-decorations-component.coffee @@ -9,6 +9,13 @@ class BlockDecorationsComponent @newState = null @oldState = null @blockDecorationNodesById = {} + @domNode = @domElementPool.buildElement("content") + @domNode.setAttribute("select", ".atom--invisible-block-decoration") + @domNode.style.visibility = "hidden" + @domNode.style.position = "absolute" + + getDomNode: -> + @domNode updateSync: (state) -> @newState = state.content @@ -41,6 +48,8 @@ class BlockDecorationsComponent blockDecorationState = @newState.blockDecorations[id] blockDecorationNode = @views.getView(blockDecorationState.decoration.getProperties().item) blockDecorationNode.classList.add("block-decoration-row-#{blockDecorationState.screenRow}") + unless blockDecorationState.isVisible + blockDecorationNode.classList.add("atom--invisible-block-decoration") @container.appendChild(blockDecorationNode) @@ -51,6 +60,11 @@ class BlockDecorationsComponent oldBlockDecorationState = @oldState.blockDecorations[id] blockDecorationNode = @blockDecorationNodesById[id] + if newBlockDecorationState.isVisible and not oldBlockDecorationState.isVisible + blockDecorationNode.classList.remove("atom--invisible-block-decoration") + else if not newBlockDecorationState.isVisible and oldBlockDecorationState.isVisible + blockDecorationNode.classList.add("atom--invisible-block-decoration") + if newBlockDecorationState.screenRow isnt oldBlockDecorationState.screenRow blockDecorationNode.classList.remove("block-decoration-row-#{oldBlockDecorationState.screenRow}") blockDecorationNode.classList.add("block-decoration-row-#{newBlockDecorationState.screenRow}") diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 7fc51b9a4..e5e63c371 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -87,6 +87,8 @@ class TextEditorComponent @linesComponent = new LinesComponent({@presenter, @hostElement, @useShadowDOM, @domElementPool, @assert, @grammars}) @scrollViewNode.appendChild(@linesComponent.getDomNode()) + @scrollViewNode.appendChild(@blockDecorationsComponent.getDomNode()) + @linesYardstick = new LinesYardstick(@editor, @presenter, @linesComponent, lineTopIndex, @grammars) @presenter.setLinesYardstick(@linesYardstick) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 44cf14794..54ffc041f 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -76,7 +76,6 @@ class TextEditorPresenter @blockDecorationsPresenter.update() - @updateBlockDecorationsState() @updateVerticalDimensions() @updateScrollbarDimensions() @@ -88,6 +87,8 @@ class TextEditorPresenter @updateCommonGutterState() @updateReflowState() + @updateBlockDecorationsState() + if @shouldUpdateDecorations @fetchDecorations() @updateLineDecorations() @@ -1217,7 +1218,8 @@ class TextEditorPresenter @blockDecorationsPresenter.getAllDecorationsByScreenRow().forEach (decorations, screenRow) => for decoration in decorations - @state.content.blockDecorations[decoration.id] = {decoration, screenRow} + isVisible = @getStartTileRow() <= screenRow < @getEndTileRow() + @tileSize + @state.content.blockDecorations[decoration.id] = {decoration, screenRow, isVisible} updateLineDecorations: -> @lineDecorationsByScreenRow = {} From 8a6ab81325850c5eda5e555eae351f8dfa1c0d71 Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 2 Dec 2015 13:26:06 -0500 Subject: [PATCH 148/502] 100% less racy. --- spec/git-repository-async-spec.js | 22 ++++++++-------------- src/git-repository-async.js | 19 +++++++++++++++---- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 37d5cb898..829c33fcd 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -296,11 +296,9 @@ describe('GitRepositoryAsync-js', () => { // When the path is added to the project, the repository is refreshed. We // need to wait for that to complete before the tests continue so that - // we're in a known state. *But* it's really hard to observe that from the - // outside in a non-racy fashion. So let's refresh again and wait for it - // to complete before we continue. + // we're in a known state. repository = atom.project.getRepositories()[0].async - waitsForPromise(() => repository.refreshStatus()) + waitsFor(() => !repository._isRefreshing()) }) it('emits a status-changed event when a buffer is saved', async () => { @@ -329,14 +327,6 @@ describe('GitRepositoryAsync-js', () => { runs(() => { expect(statusHandler.callCount).toBe(1) expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) - - const buffer = editor.getBuffer() - const reloadHandler = jasmine.createSpy('reloadHandler') - buffer.onDidReload(reloadHandler) - buffer.reload() - - waitsFor(() => reloadHandler.callCount > 0) - runs(() => expect(reloadHandler.callCount).toBe(1)) }) }) @@ -381,7 +371,7 @@ describe('GitRepositoryAsync-js', () => { // See the comment in the 'buffer events' beforeEach for why we need to do // this. const repository = atom.project.getRepositories()[0].async - waitsForPromise(() => repository.refreshStatus()) + waitsFor(() => !repository._isRefreshing()) }) afterEach(() => { @@ -393,6 +383,10 @@ describe('GitRepositoryAsync-js', () => { project2 = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) project2.deserialize(atom.project.serialize(), atom.deserializers) + + const repo = project2.getRepositories()[0].async + waitsFor(() => !repo._isRefreshing()) + const buffer = project2.getBuffers()[0] waitsFor(() => buffer.loaded) @@ -400,7 +394,7 @@ describe('GitRepositoryAsync-js', () => { buffer.append('changes') const statusHandler = jasmine.createSpy('statusHandler') - project2.getRepositories()[0].async.onDidChangeStatus(statusHandler) + repo.onDidChangeStatus(statusHandler) buffer.save() waitsFor(() => statusHandler.callCount > 0) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 4f74cf620..3475684a1 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -30,6 +30,7 @@ module.exports = class GitRepositoryAsync { this.pathStatusCache = {} this.repoPromise = Git.Repository.open(path) this.isCaseInsensitive = fs.isCaseInsensitive() + this._refreshingCount = 0 const {project} = options this.project = project @@ -174,15 +175,21 @@ module.exports = class GitRepositoryAsync { .then(branchRef => this.branch = branchRef) } - // Refreshes the git status. Note: the sync GitRepository class does this with - // a separate process, let's see if we can avoid that. + // Refreshes the git status. + // + // Returns :: Promise + // Resolves when refresh has completed. refreshStatus () { + this._refreshingCount++ + // TODO add upstream, branch, and submodule tracking const status = this.repoPromise .then(repo => repo.getStatus()) .then(statuses => { + console.log(Object.keys(statuses)) // update the status cache - return Promise.all(statuses.map(status => [status.path(), status.statusBit()])) + const statusPairs = statuses.map(status => [status.path(), status.statusBit()]) + return Promise.all(statusPairs) .then(statusesByPath => _.object(statusesByPath)) }) .then(newPathStatusCache => { @@ -195,12 +202,16 @@ module.exports = class GitRepositoryAsync { const branch = this._refreshBranch() - return Promise.all([status, branch]) + return Promise.all([status, branch]).then(_ => this._refreshingCount--) } // Section: Private // ================ + _isRefreshing () { + return this._refreshingCount === 0 + } + subscribeToBuffer (buffer) { const bufferSubscriptions = new CompositeDisposable() From aa31c6c96fce96734e71f05e8e6bfe214873a38c Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 2 Dec 2015 13:54:53 -0500 Subject: [PATCH 149/502] Don't log anymore. --- src/git-repository-async.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 3475684a1..00b325268 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -186,7 +186,6 @@ module.exports = class GitRepositoryAsync { const status = this.repoPromise .then(repo => repo.getStatus()) .then(statuses => { - console.log(Object.keys(statuses)) // update the status cache const statusPairs = statuses.map(status => [status.path(), status.statusBit()]) return Promise.all(statusPairs) From 38cf3b8f649a051ab8afb149ae8246ebcaf01d82 Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 2 Dec 2015 13:55:10 -0500 Subject: [PATCH 150/502] Use refreshStatusForBuffer. --- src/git-repository-async.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 00b325268..8e942ff1c 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -36,11 +36,8 @@ module.exports = class GitRepositoryAsync { this.project = project if (this.project) { - this.subscriptions.add(this.project.onDidAddBuffer(buffer => { - this.subscribeToBuffer(buffer) - })) - - this.project.getBuffers().forEach(buffer => { this.subscribeToBuffer(buffer) }) + this.project.getBuffers().forEach(buffer => this.subscribeToBuffer(buffer)) + this.subscriptions.add(this.project.onDidAddBuffer(buffer => this.subscribeToBuffer(buffer))) } } @@ -214,7 +211,7 @@ module.exports = class GitRepositoryAsync { subscribeToBuffer (buffer) { const bufferSubscriptions = new CompositeDisposable() - const getBufferPathStatus = () => { + const refreshStatusForBuffer = () => { const _path = buffer.getPath() if (_path) { this.refreshStatusForPath(_path) @@ -222,9 +219,9 @@ module.exports = class GitRepositoryAsync { } bufferSubscriptions.add( - buffer.onDidSave(getBufferPathStatus), - buffer.onDidReload(getBufferPathStatus), - buffer.onDidChangePath(getBufferPathStatus), + buffer.onDidSave(refreshStatusForBuffer), + buffer.onDidReload(refreshStatusForBuffer), + buffer.onDidChangePath(refreshStatusForBuffer), buffer.onDidDestroy(() => { bufferSubscriptions.dispose() this.subscriptions.remove(bufferSubscriptions) From 11fb5080397862e7ee54c887162dff1ada855230 Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 2 Dec 2015 14:22:57 -0500 Subject: [PATCH 151/502] :fire: all the disabled tests. --- spec/git-repository-async-spec.js | 35 ------------------------------- 1 file changed, 35 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 829c33fcd..19ee38c80 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -47,8 +47,6 @@ describe('GitRepositoryAsync-js', () => { }) describe('.getPath()', () => { - xit('returns the repository path for a .git directory path') - it('returns the repository path for a repository path', async () => { repo = openFixture('master.git') const repoPath = await repo.getPath() @@ -181,39 +179,6 @@ describe('GitRepositoryAsync-js', () => { }) }) - xdescribe('.checkoutHeadForEditor(editor)', () => { - let filePath, editor - - beforeEach(() => { - const workingDirPath = copyRepository() - repo = GitRepositoryAsync.open(workingDirPath) - filePath = path.join(workingDirPath, 'a.txt') - fs.writeFileSync(filePath, 'ch ch changes') - - waitsForPromise(() => atom.workspace.open(filePath)) - runs(() => editor = atom.workspace.getActiveTextEditor()) - }) - - xit('displays a confirmation dialog by default', () => { - spyOn(atom, 'confirm').andCallFake(buttons, () => buttons[0].OK()) // eslint-disable-line - atom.config.set('editor.confirmCheckoutHeadRevision', true) - - waitsForPromise(() => repo.checkoutHeadForEditor(editor)) - runs(() => expect(fs.readFileSync(filePath, 'utf8')).toBe('')) - }) - - xit('does not display a dialog when confirmation is disabled', () => { - spyOn(atom, 'confirm') - atom.config.set('editor.confirmCheckoutHeadRevision', false) - - waitsForPromise(() => repo.checkoutHeadForEditor(editor)) - runs(() => { - expect(fs.readFileSync(filePath, 'utf8')).toBe('') - expect(atom.confirm).not.toHaveBeenCalled() - }) - }) - }) - describe('.getPathStatus(path)', () => { let filePath From fb7f2cce9531a70aac3264afa97661819f801298 Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 2 Dec 2015 14:23:11 -0500 Subject: [PATCH 152/502] These may be called more than once and that's ok. --- spec/git-repository-async-spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 19ee38c80..941c1dce3 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -290,7 +290,7 @@ describe('GitRepositoryAsync-js', () => { waitsFor(() => statusHandler.callCount > 0) runs(() => { - expect(statusHandler.callCount).toBe(1) + expect(statusHandler.callCount).toBeGreaterThan(0) expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) }) }) @@ -306,7 +306,7 @@ describe('GitRepositoryAsync-js', () => { waitsFor(() => statusHandler.callCount > 0) runs(() => { - expect(statusHandler.callCount).toBe(1) + expect(statusHandler.callCount).toBeGreaterThan(0) expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) const pathHandler = jasmine.createSpy('pathHandler') @@ -315,7 +315,7 @@ describe('GitRepositoryAsync-js', () => { buffer.emitter.emit('did-change-path') waitsFor(() => pathHandler.callCount > 0) - runs(() => expect(pathHandler.callCount).toBe(1)) + runs(() => expect(pathHandler.callCount).toBeGreaterThan(0)) }) }) From e8a5864707e0f898e08d97e64acc614dd0fa2025 Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 2 Dec 2015 14:23:27 -0500 Subject: [PATCH 153/502] Do the work after waiting. --- spec/git-repository-async-spec.js | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 941c1dce3..b83cc4b83 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -351,21 +351,22 @@ describe('GitRepositoryAsync-js', () => { const repo = project2.getRepositories()[0].async waitsFor(() => !repo._isRefreshing()) - - const buffer = project2.getBuffers()[0] - - waitsFor(() => buffer.loaded) runs(() => { - buffer.append('changes') + const buffer = project2.getBuffers()[0] - const statusHandler = jasmine.createSpy('statusHandler') - repo.onDidChangeStatus(statusHandler) - buffer.save() - - waitsFor(() => statusHandler.callCount > 0) + waitsFor(() => buffer.loaded) runs(() => { - expect(statusHandler.callCount).toBe(1) - expect(statusHandler).toHaveBeenCalledWith({path: buffer.getPath(), pathStatus: 256}) + buffer.append('changes') + + const statusHandler = jasmine.createSpy('statusHandler') + repo.onDidChangeStatus(statusHandler) + buffer.save() + + waitsFor(() => statusHandler.callCount > 0) + runs(() => { + expect(statusHandler.callCount).toBeGreaterThan(0) + expect(statusHandler).toHaveBeenCalledWith({path: buffer.getPath(), pathStatus: 256}) + }) }) }) }) From 6c58550bf4043c0a98fccce4d819a1215282a18a Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 2 Dec 2015 14:36:25 -0500 Subject: [PATCH 154/502] Of course it's JS. --- spec/git-repository-async-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index b83cc4b83..dc8ae0107 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -23,7 +23,7 @@ function copyRepository () { return fs.realpathSync(workingDirPath) } -describe('GitRepositoryAsync-js', () => { +describe('GitRepositoryAsync', () => { let repo afterEach(() => { From d0b148a97a5d3daf65c074491feee3a50e32727d Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 2 Dec 2015 14:39:00 -0500 Subject: [PATCH 155/502] Call the better-named method. --- src/git-repository.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/git-repository.coffee b/src/git-repository.coffee index 9ff0771b3..30cce92d0 100644 --- a/src/git-repository.coffee +++ b/src/git-repository.coffee @@ -324,7 +324,7 @@ class GitRepository # {::isStatusModified} or {::isStatusNew} to get more information. getPathStatus: (path) -> # Trigger events emitted on the async repo as well - @async.getPathStatus(path) + @async.refreshStatusForPath(path) repo = @getRepo(path) relativePath = @relativize(path) From bb14169e75675f374ac16fd804384f567ff19893 Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 2 Dec 2015 14:39:16 -0500 Subject: [PATCH 156/502] Take refreshStatusForPath into account for _refreshingCount. --- src/git-repository-async.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 8e942ff1c..7f173b737 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -109,6 +109,8 @@ module.exports = class GitRepositoryAsync { // Returns :: Promise // The refreshed status bit for the path. refreshStatusForPath (_path) { + this._refreshingCount++ + let relativePath return this.repoPromise .then(repo => { @@ -125,6 +127,7 @@ module.exports = class GitRepositoryAsync { return status }) + .then(_ => this._refreshingCount--) } // Returns a Promise that resolves to the status bit of a given path if it has From 3ca4448afc784bc3ab5fc94692613e67fa22891e Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 2 Dec 2015 14:53:57 -0500 Subject: [PATCH 157/502] Use the ES6 export syntax. --- src/git-repository-async.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 7f173b737..a9f365e13 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -13,7 +13,7 @@ const indexStatusFlags = Git.Status.STATUS.INDEX_NEW | Git.Status.STATUS.INDEX_M // Just using this for _.isEqual and _.object, we should impl our own here import _ from 'underscore-plus' -module.exports = class GitRepositoryAsync { +export default class GitRepositoryAsync { static open (path, options = {}) { // QUESTION: Should this wrap Git.Repository and reject with a nicer message? return new GitRepositoryAsync(path, options) From 428797c393a20f7c10001e001ff2b78a2a1a1de4 Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 2 Dec 2015 14:59:09 -0500 Subject: [PATCH 158/502] Add back window refreshing. --- src/git-repository-async.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index a9f365e13..38e098b6a 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -3,7 +3,7 @@ import fs from 'fs-plus' import Git from 'nodegit' import path from 'path' -import {Emitter, CompositeDisposable} from 'event-kit' +import {Emitter, CompositeDisposable, Disposable} from 'event-kit' const modifiedStatusFlags = Git.Status.STATUS.WT_MODIFIED | Git.Status.STATUS.INDEX_MODIFIED | Git.Status.STATUS.WT_DELETED | Git.Status.STATUS.INDEX_DELETED | Git.Status.STATUS.WT_TYPECHANGE | Git.Status.STATUS.INDEX_TYPECHANGE const newStatusFlags = Git.Status.STATUS.WT_NEW | Git.Status.STATUS.INDEX_NEW @@ -32,9 +32,15 @@ export default class GitRepositoryAsync { this.isCaseInsensitive = fs.isCaseInsensitive() this._refreshingCount = 0 + let {refreshOnWindowFocus} = options || true + if (refreshOnWindowFocus) { + const onWindowFocus = () => this.refreshStatus() + window.addEventListener('focus', onWindowFocus) + this.subscriptions.add(new Disposable(() => window.removeEventListener('focus', onWindowFocus))) + } + const {project} = options this.project = project - if (this.project) { this.project.getBuffers().forEach(buffer => this.subscribeToBuffer(buffer)) this.subscriptions.add(this.project.onDidAddBuffer(buffer => this.subscribeToBuffer(buffer))) From fe4d3601d55f487b71a601b22446187e88a9fa8a Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 2 Dec 2015 15:28:09 -0500 Subject: [PATCH 159/502] Organize similarly to git-repository.coffee so we can more easily tell what we're missing. --- src/git-repository-async.js | 447 ++++++++++++++++++++++++------------ 1 file changed, 294 insertions(+), 153 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 38e098b6a..77c07013a 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -59,14 +59,209 @@ export default class GitRepositoryAsync { } } + // Event subscription + // ================== + + onDidDestroy (callback) { + return this.emitter.on('did-destroy', callback) + } + + onDidChangeStatus (callback) { + return this.emitter.on('did-change-status', callback) + } + + onDidChangeStatuses (callback) { + return this.emitter.on('did-change-statuses', callback) + } + + // Repository details + // ================== + + // Public: A {String} indicating the type of version control system used by + // this repository. + // + // Returns `"git"`. + getType () { + return 'git' + } + + // Public: Returns a {Promise} which resolves to the {String} path of the + // repository. getPath () { return this.repoPromise.then(repo => repo.path().replace(/\/$/, '')) } - isPathIgnored (_path) { - return this.repoPromise.then(repo => Git.Ignore.pathIsIgnored(repo, _path)) + // Public: Returns a {Promise} which resolves to the {String} working + // directory path of the repository. + getWorkingDirectory () { + throw new Error('Unimplemented') } + // Public: Returns a {Promise} that resolves to true if at the root, false if + // in a subfolder of the repository. + isProjectAtRoot () { + if (!this.projectAtRoot && this.project) { + this.projectAtRoot = Promise.resolve(() => { + return this.repoPromise.then(repo => this.project.relativize(repo.workdir)) + }) + } + + return this.projectAtRoot + } + + // Public: Makes a path relative to the repository's working directory. + relativize (_path, workingDirectory) { + // Cargo-culted from git-utils. The original implementation also handles + // this.openedWorkingDirectory, which is set by git-utils when the + // repository is opened. Those branches of the if tree aren't included here + // yet, but if we determine we still need that here it should be simple to + // port. + // + // The original implementation also handled null workingDirectory as it + // pulled it from a sync function that could return null. We require it + // to be passed here. + if (!_path || !workingDirectory) { + return _path + } + + if (process.platform === 'win32') { + _path = _path.replace(/\\/g, '/') + } else { + if (_path[0] !== '/') { + return _path + } + } + + if (!/\/$/.test(workingDirectory)) { + workingDirectory = `${workingDirectory}/` + } + + if (this.isCaseInsensitive) { + const lowerCasePath = _path.toLowerCase() + + workingDirectory = workingDirectory.toLowerCase() + if (lowerCasePath.indexOf(workingDirectory) === 0) { + return _path.substring(workingDirectory.length) + } else { + if (lowerCasePath === workingDirectory) { + return '' + } + } + } + + return _path + } + + // Public: Returns true if the given branch exists. + hasBranch (branch) { + throw new Error('Unimplemented') + } + + // Public: Retrieves a shortened version of the HEAD reference value. + // + // This removes the leading segments of `refs/heads`, `refs/tags`, or + // `refs/remotes`. It also shortens the SHA-1 of a detached `HEAD` to 7 + // characters. + // + // * `path` An optional {String} path in the repository to get this information + // for, only needed if the repository contains submodules. + // + // Returns a {String}. + getShortHead (path) { + throw new Error('Unimplemented') + } + + // Public: Is the given path a submodule in the repository? + // + // * `path` The {String} path to check. + // + // Returns a {Promise} that resolves true if the given path is a submodule in + // the repository. + isSubmodule (_path) { + return this.repoPromise + .then(repo => repo.openIndex()) + .then(index => { + const entry = index.getByPath(_path) + const submoduleMode = 57344 // TODO compose this from libgit2 constants + return entry.mode === submoduleMode + }) + } + + // Public: Returns the number of commits behind the current branch is from the + // its upstream remote branch. + // + // * `reference` The {String} branch reference name. + // * `path` The {String} path in the repository to get this information for, + // only needed if the repository contains submodules. + getAheadBehindCount (reference, path) { + throw new Error('Unimplemented') + } + + // Public: Get the cached ahead/behind commit counts for the current branch's + // upstream branch. + // + // * `path` An optional {String} path in the repository to get this information + // for, only needed if the repository has submodules. + // + // Returns an {Object} with the following keys: + // * `ahead` The {Number} of commits ahead. + // * `behind` The {Number} of commits behind. + getCachedUpstreamAheadBehindCount (path) { + throw new Error('Unimplemented') + } + + // Public: Returns the git configuration value specified by the key. + // + // * `path` An optional {String} path in the repository to get this information + // for, only needed if the repository has submodules. + getConfigValue (key, path) { + throw new Error('Unimplemented') + } + + // Public: Returns the origin url of the repository. + // + // * `path` (optional) {String} path in the repository to get this information + // for, only needed if the repository has submodules. + getOriginURL (path) { + throw new Error('Unimplemented') + } + + // Public: Returns the upstream branch for the current HEAD, or null if there + // is no upstream branch for the current HEAD. + // + // * `path` An optional {String} path in the repo to get this information for, + // only needed if the repository contains submodules. + // + // Returns a {String} branch name such as `refs/remotes/origin/master`. + getUpstreamBranch (path) { + throw new Error('Unimplemented') + } + + // Public: Gets all the local and remote references. + // + // * `path` An optional {String} path in the repository to get this information + // for, only needed if the repository has submodules. + // + // Returns an {Object} with the following keys: + // * `heads` An {Array} of head reference names. + // * `remotes` An {Array} of remote reference names. + // * `tags` An {Array} of tag reference names. + getReferences (path) { + throw new Error('Unimplemented') + } + + // Public: Returns the current {String} SHA for the given reference. + // + // * `reference` The {String} reference to get the target of. + // * `path` An optional {String} path in the repo to get the reference target + // for. Only needed if the repository contains submodules. + getReferenceTarget (reference, path) { + throw new Error('Unimplemented') + } + + // Reading Status + // ============== + isPathModified (_path) { return this._filterStatusesByPath(_path).then(statuses => { return statuses.filter(status => status.isModified()).length > 0 @@ -79,29 +274,36 @@ export default class GitRepositoryAsync { }) } - checkoutHead (_path) { - return this.repoPromise - .then(repo => { - const checkoutOptions = new Git.CheckoutOptions() - checkoutOptions.paths = [this.relativize(_path, repo.workdir())] - checkoutOptions.checkoutStrategy = Git.Checkout.STRATEGY.FORCE | Git.Checkout.STRATEGY.DISABLE_PATHSPEC_MATCH - return Git.Checkout.head(repo, checkoutOptions) - }) - .then(() => this.refreshStatusForPath(_path)) + isPathIgnored (_path) { + return this.repoPromise.then(repo => Git.Ignore.pathIsIgnored(repo, _path)) } - checkoutHeadForEditor (editor) { - return new Promise((resolve, reject) => { - const filePath = editor.getPath() - if (filePath) { - if (editor.buffer.isModified()) { - editor.buffer.reload() - } - resolve(filePath) - } else { - reject() - } - }).then(filePath => this.checkoutHead(filePath)) + // Get the status of a directory in the repository's working directory. + // + // * `directoryPath` The {String} path to check. + // + // Returns a promise resolving to a {Number} representing the status. This value can be passed to + // {::isStatusModified} or {::isStatusNew} to get more information. + + getDirectoryStatus (directoryPath) { + let relativePath + // XXX _filterSBD already gets repoPromise + return this.repoPromise + .then(repo => { + relativePath = this.relativize(directoryPath, repo.workdir()) + return this._filterStatusesByDirectory(relativePath) + }) + .then(statuses => { + return Promise.all(statuses.map(s => s.statusBit())).then(bits => { + let directoryStatus = 0 + const filteredBits = bits.filter(b => b > 0) + if (filteredBits.length > 0) { + filteredBits.forEach(bit => directoryStatus |= bit) + } + + return directoryStatus + }) + }) } // Refresh the status bit for the given path. @@ -142,34 +344,83 @@ export default class GitRepositoryAsync { return this.refreshStatusForPath(_path) } - // Get the status of a directory in the repository's working directory. + // Public: Get the cached status for the given path. // - // * `directoryPath` The {String} path to check. + // * `path` A {String} path in the repository, relative or absolute. // - // Returns a promise resolving to a {Number} representing the status. This value can be passed to - // {::isStatusModified} or {::isStatusNew} to get more information. + // Returns a {Promise} which resolves to a status {Number} or null if the + // path is not in the cache. + getCachedPathStatus (_path) { + return this.repoPromise + .then(repo => this.relativize(_path, repo.workdir())) + .then(relativePath => this.pathStatusCache[relativePath]) + } - getDirectoryStatus (directoryPath) { - let relativePath - // XXX _filterSBD already gets repoPromise + isStatusNew (statusBit) { + return (statusBit & newStatusFlags) > 0 + } + + isStatusModified (statusBit) { + return (statusBit & modifiedStatusFlags) > 0 + } + + isStatusStaged (statusBit) { + return (statusBit & indexStatusFlags) > 0 + } + + isStatusIgnored (statusBit) { + return (statusBit & (1 << 14)) > 0 + } + + isStatusDeleted (statusBit) { + return (statusBit & deletedStatusFlags) > 0 + } + + // Checking Out + // ============ + + // Public: Restore the contents of a path in the working directory and index + // to the version at `HEAD`. + // + // This is essentially the same as running: + // + // ```sh + // git reset HEAD -- + // git checkout HEAD -- + // ``` + // + // * `path` The {String} path to checkout. + // + // Returns a {Promise} that resolves or rejects depending on whether the + // method was successful. + checkoutHead (_path) { return this.repoPromise .then(repo => { - relativePath = this.relativize(directoryPath, repo.workdir()) - return this._filterStatusesByDirectory(relativePath) - }) - .then(statuses => { - return Promise.all(statuses.map(s => s.statusBit())).then(bits => { - let directoryStatus = 0 - const filteredBits = bits.filter(b => b > 0) - if (filteredBits.length > 0) { - filteredBits.forEach(bit => directoryStatus |= bit) - } - - return directoryStatus - }) + const checkoutOptions = new Git.CheckoutOptions() + checkoutOptions.paths = [this.relativize(_path, repo.workdir())] + checkoutOptions.checkoutStrategy = Git.Checkout.STRATEGY.FORCE | Git.Checkout.STRATEGY.DISABLE_PATHSPEC_MATCH + return Git.Checkout.head(repo, checkoutOptions) }) + .then(() => this.refreshStatusForPath(_path)) } + checkoutHeadForEditor (editor) { + return new Promise((resolve, reject) => { + const filePath = editor.getPath() + if (filePath) { + if (editor.buffer.isModified()) { + editor.buffer.reload() + } + resolve(filePath) + } else { + reject() + } + }).then(filePath => this.checkoutHead(filePath)) + } + + // Private + // ======= + // Get the current branch and update this.branch. // // Returns :: Promise @@ -241,74 +492,6 @@ export default class GitRepositoryAsync { return } - relativize (_path, workingDirectory) { - // Cargo-culted from git-utils. The original implementation also handles - // this.openedWorkingDirectory, which is set by git-utils when the - // repository is opened. Those branches of the if tree aren't included here - // yet, but if we determine we still need that here it should be simple to - // port. - // - // The original implementation also handled null workingDirectory as it - // pulled it from a sync function that could return null. We require it - // to be passed here. - if (!_path || !workingDirectory) { - return _path - } - - if (process.platform === 'win32') { - _path = _path.replace(/\\/g, '/') - } else { - if (_path[0] !== '/') { - return _path - } - } - - if (!/\/$/.test(workingDirectory)) { - workingDirectory = `${workingDirectory}/` - } - - if (this.isCaseInsensitive) { - const lowerCasePath = _path.toLowerCase() - - workingDirectory = workingDirectory.toLowerCase() - if (lowerCasePath.indexOf(workingDirectory) === 0) { - return _path.substring(workingDirectory.length) - } else { - if (lowerCasePath === workingDirectory) { - return '' - } - } - } - - return _path - } - - getCachedPathStatus (_path) { - return this.repoPromise.then(repo => { - return this.pathStatusCache[this.relativize(_path, repo.workdir())] - }) - } - - isStatusNew (statusBit) { - return (statusBit & newStatusFlags) > 0 - } - - isStatusModified (statusBit) { - return (statusBit & modifiedStatusFlags) > 0 - } - - isStatusStaged (statusBit) { - return (statusBit & indexStatusFlags) > 0 - } - - isStatusIgnored (statusBit) { - return (statusBit & (1 << 14)) > 0 - } - - isStatusDeleted (statusBit) { - return (statusBit & deletedStatusFlags) > 0 - } - _filterStatusesByPath (_path) { // Surely I'm missing a built-in way to do this let basePath = null @@ -329,46 +512,4 @@ export default class GitRepositoryAsync { return statuses.filter(status => status.path().indexOf(directoryPath) === 0) }) } - // Event subscription - // ================== - - onDidChangeStatus (callback) { - return this.emitter.on('did-change-status', callback) - } - - onDidChangeStatuses (callback) { - return this.emitter.on('did-change-statuses', callback) - } - - onDidDestroy (callback) { - return this.emitter.on('did-destroy', callback) - } - - // - // Section: Repository Details - // - - // Returns a {Promise} that resolves true if at the root, false if in a - // subfolder of the repository. - isProjectAtRoot () { - if (this.projectAtRoot === undefined) { - this.projectAtRoot = Promise.resolve(() => { - return this.repoPromise.then(repo => this.project.relativize(repo.workdir)) - }) - } - - return this.projectAtRoot - } - - // Returns a {Promise} that resolves true if the given path is a submodule in - // the repository. - isSubmodule (_path) { - return this.repoPromise - .then(repo => repo.openIndex()) - .then(index => { - const entry = index.getByPath(_path) - const submoduleMode = 57344 // TODO compose this from libgit2 constants - return entry.mode === submoduleMode - }) - } } From aa634570e6371ee529dfb4ecc48a3e0795e03999 Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 2 Dec 2015 16:16:30 -0500 Subject: [PATCH 160/502] Added getShortHead --- spec/git-repository-async-spec.js | 12 ++++++++++++ src/git-repository-async.js | 21 ++++++++++++++++++--- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index dc8ae0107..99162920f 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -397,4 +397,16 @@ describe('GitRepositoryAsync', () => { expect(relativizedPath).toBe('a/b.txt') }) }) + + describe('.getShortHead(path)', () => { + beforeEach(() => { + const workingDirectory = copyRepository() + repo = GitRepositoryAsync.open(workingDirectory) + }) + + it('returns the human-readable branch name', async () => { + const head = await repo.getShortHead() + expect(head).toBe('master') + }) + }) }) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 77c07013a..283b0a77d 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -166,9 +166,11 @@ export default class GitRepositoryAsync { // * `path` An optional {String} path in the repository to get this information // for, only needed if the repository contains submodules. // - // Returns a {String}. - getShortHead (path) { - throw new Error('Unimplemented') + // Returns a {Promise} which resolves to a {String}. + getShortHead (_path) { + return this._getRepo(_path) + .then(repo => repo.getCurrentBranch()) + .then(branch => branch.shorthand()) } // Public: Is the given path a submodule in the repository? @@ -468,6 +470,19 @@ export default class GitRepositoryAsync { return this._refreshingCount === 0 } + _getRepo (_path) { + if (!_path) return this.repoPromise + + return this.isSubmodule(_path) + .then(isSubmodule => { + if (isSubmodule) { + return Git.Repository.open(_path) + } else { + return this.repoPromise + } + }) + } + subscribeToBuffer (buffer) { const bufferSubscriptions = new CompositeDisposable() From cb62d917de57b953221c0d7002b2cb1e52c62b6e Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 2 Dec 2015 17:20:38 -0500 Subject: [PATCH 161/502] Added getAheadBehindCount. --- spec/git-repository-async-spec.js | 27 ++++++++++++ src/git-repository-async.js | 69 ++++++++++++++++++++----------- 2 files changed, 73 insertions(+), 23 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 99162920f..1b45d12f6 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -409,4 +409,31 @@ describe('GitRepositoryAsync', () => { expect(head).toBe('master') }) }) + + describe('.getAheadBehindCount(reference, path)', () => { + beforeEach(() => { + const workingDirectory = copyRepository() + repo = GitRepositoryAsync.open(workingDirectory) + }) + + it('returns 0, 0 for a branch with no upstream', async () => { + const {ahead, behind} = await repo.getAheadBehindCount('master') + expect(ahead).toBe(0) + expect(behind).toBe(0) + }) + }) + + describe('.getCachedUpstreamAheadBehindCount(path)', () => { + beforeEach(() => { + const workingDirectory = copyRepository() + repo = GitRepositoryAsync.open(workingDirectory) + }) + + it('returns 0, 0 for a branch with no upstream', async () => { + await repo.refreshStatus() + const {ahead, behind} = repo.getCachedUpstreamAheadBehindCount() + expect(ahead).toBe(0) + expect(behind).toBe(0) + }) + }) }) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 283b0a77d..91954fab6 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -23,13 +23,15 @@ export default class GitRepositoryAsync { return Git } - constructor (path, options) { + constructor (_path, options) { this.repo = null this.emitter = new Emitter() this.subscriptions = new CompositeDisposable() this.pathStatusCache = {} - this.repoPromise = Git.Repository.open(path) + this.repoPromise = Git.Repository.open(_path) this.isCaseInsensitive = fs.isCaseInsensitive() + this.upstreamByPath = {} + this._refreshingCount = 0 let {refreshOnWindowFocus} = options || true @@ -193,10 +195,21 @@ export default class GitRepositoryAsync { // its upstream remote branch. // // * `reference` The {String} branch reference name. - // * `path` The {String} path in the repository to get this information for, - // only needed if the repository contains submodules. - getAheadBehindCount (reference, path) { - throw new Error('Unimplemented') + // * `path` The {String} path in the repository to get this information + // for, only needed if the repository contains submodules. + // + // Returns a {Promise} which resolves to an {Object} with the following keys: + // * `ahead` The {Number} of commits ahead. + // * `behind` The {Number} of commits behind. + getAheadBehindCount (reference, _path) { + return this._getRepo(_path) + .then(repo => Promise.all([repo, repo.getBranch(reference)])) + .then(([repo, local]) => Promise.all([repo, local, Git.Branch.upstream(local)])) + .then(([repo, local, upstream]) => { + if (!upstream) return {ahead: 0, behind: 0} + + return Git.Graph.aheadBehind(repo, local.target(), upstream.target()) + }) } // Public: Get the cached ahead/behind commit counts for the current branch's @@ -208,15 +221,15 @@ export default class GitRepositoryAsync { // Returns an {Object} with the following keys: // * `ahead` The {Number} of commits ahead. // * `behind` The {Number} of commits behind. - getCachedUpstreamAheadBehindCount (path) { - throw new Error('Unimplemented') + getCachedUpstreamAheadBehindCount (_path) { + return this.upstreamByPath[_path || '.'] } // Public: Returns the git configuration value specified by the key. // // * `path` An optional {String} path in the repository to get this information // for, only needed if the repository has submodules. - getConfigValue (key, path) { + getConfigValue (key, _path) { throw new Error('Unimplemented') } @@ -224,7 +237,7 @@ export default class GitRepositoryAsync { // // * `path` (optional) {String} path in the repository to get this information // for, only needed if the repository has submodules. - getOriginURL (path) { + getOriginURL (_path) { throw new Error('Unimplemented') } @@ -235,7 +248,7 @@ export default class GitRepositoryAsync { // only needed if the repository contains submodules. // // Returns a {String} branch name such as `refs/remotes/origin/master`. - getUpstreamBranch (path) { + getUpstreamBranch (_path) { throw new Error('Unimplemented') } @@ -248,7 +261,7 @@ export default class GitRepositoryAsync { // * `heads` An {Array} of head reference names. // * `remotes` An {Array} of remote reference names. // * `tags` An {Array} of tag reference names. - getReferences (path) { + getReferences (_path) { throw new Error('Unimplemented') } @@ -257,7 +270,7 @@ export default class GitRepositoryAsync { // * `reference` The {String} reference to get the target of. // * `path` An optional {String} path in the repo to get the reference target // for. Only needed if the repository contains submodules. - getReferenceTarget (reference, path) { + getReferenceTarget (reference, _path) { throw new Error('Unimplemented') } @@ -431,18 +444,16 @@ export default class GitRepositoryAsync { return this.repoPromise .then(repo => repo.getCurrentBranch()) .then(ref => ref.name()) - .then(branchRef => this.branch = branchRef) + .then(branchName => this.branch = branchName) } - // Refreshes the git status. - // - // Returns :: Promise - // Resolves when refresh has completed. - refreshStatus () { - this._refreshingCount++ + _refreshAheadBehindCount (branchName) { + return this.getAheadBehindCount(branchName) + .then(counts => this.upstreamByPath['.'] = counts) + } - // TODO add upstream, branch, and submodule tracking - const status = this.repoPromise + _refreshStatus () { + return this.repoPromise .then(repo => repo.getStatus()) .then(statuses => { // update the status cache @@ -457,10 +468,22 @@ export default class GitRepositoryAsync { this.pathStatusCache = newPathStatusCache return newPathStatusCache }) + } + // Refreshes the git status. + // + // Returns :: Promise + // Resolves when refresh has completed. + refreshStatus () { + this._refreshingCount++ + + // TODO add submodule tracking + + const status = this._refreshStatus() const branch = this._refreshBranch() + const aheadBehind = branch.then(branchName => this._refreshAheadBehindCount(branchName)) - return Promise.all([status, branch]).then(_ => this._refreshingCount--) + return Promise.all([status, branch, aheadBehind]).then(_ => this._refreshingCount--) } // Section: Private From 8da08724ad5e54ae19a15a359b62a070f3653b3e Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 2 Dec 2015 23:51:33 -0500 Subject: [PATCH 162/502] Added getDiffStats. --- spec/git-repository-async-spec.js | 17 ++++++++++++++ src/git-repository-async.js | 38 +++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 1b45d12f6..6aa2e6f81 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -436,4 +436,21 @@ describe('GitRepositoryAsync', () => { expect(behind).toBe(0) }) }) + + describe('.getDiffStats(path)', () => { + let workingDirectory + beforeEach(() => { + workingDirectory = copyRepository() + repo = GitRepositoryAsync.open(workingDirectory) + }) + + it('returns the diff stat', async () => { + const filePath = path.join(workingDirectory, 'a.txt') + fs.writeFileSync(filePath, 'change') + + const {added, deleted} = await repo.getDiffStats('a.txt') + expect(added).toBe(1) + expect(deleted).toBe(0) + }) + }) }) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 91954fab6..131b1b51c 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -391,6 +391,44 @@ export default class GitRepositoryAsync { return (statusBit & deletedStatusFlags) > 0 } + // Retrieving Diffs + // ================ + // Public: Retrieves the number of lines added and removed to a path. + // + // This compares the working directory contents of the path to the `HEAD` + // version. + // + // * `path` The {String} path to check. + // + // Returns a {Promise} which resolves to an {Object} with the following keys: + // * `added` The {Number} of added lines. + // * `deleted` The {Number} of deleted lines. + getDiffStats (_path) { + return this.repoPromise + .then(repo => Promise.all([repo, repo.getHeadCommit()])) + .then(([repo, headCommit]) => Promise.all([repo, headCommit.getTree()])) + .then(([repo, tree]) => { + const options = new Git.DiffOptions() + options.pathspec = _path + return Git.Diff.treeToWorkdir(repo, tree, options) + }) + .then(diff => diff.patches()) // :: Array + .then(patches => Promise.all(patches.map(p => p.hunks()))) // :: Array> + .then(hunks => Promise.all(_.flatten(hunks).map(h => h.lines()))) // :: Array> + .then(lines => { + const stats = {added: 0, deleted: 0} + for (const line of _.flatten(lines)) { + const origin = line.origin() + if (origin === Git.Diff.LINE.ADDITION) { + stats.added++ + } else if (origin === Git.Diff.LINE.DELETION) { + stats.deleted++ + } + } + return stats + }) + } + // Checking Out // ============ From fcb8a13f4a934de1c58eb9fe6338971f4e7f1442 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 2 Dec 2015 19:11:51 +0100 Subject: [PATCH 163/502] Use buffer marker events to avoid conversions --- src/block-decorations-presenter.js | 33 +++++++++++++----------------- src/linear-line-top-index.js | 5 +++++ src/text-editor-presenter.coffee | 5 +---- 3 files changed, 20 insertions(+), 23 deletions(-) diff --git a/src/block-decorations-presenter.js b/src/block-decorations-presenter.js index 80f4de6f4..31571f57a 100644 --- a/src/block-decorations-presenter.js +++ b/src/block-decorations-presenter.js @@ -45,22 +45,13 @@ class BlockDecorationsPresenter { update () { if (this.firstUpdate) { - this.fullUpdate() + for (let decoration of this.model.getDecorations({type: "block"})) { + this.observeDecoration(decoration) + } this.firstUpdate = false - } else { - this.incrementalUpdate() } } - fullUpdate () { - for (let decoration of this.model.getDecorations({type: "block"})) { - this.observeDecoration(decoration) - } - } - - incrementalUpdate () { - } - setDimensionsForDecoration (decoration, width, height) { let block = this.blocksByDecoration.get(decoration) if (block) { @@ -74,10 +65,6 @@ class BlockDecorationsPresenter { this.emitter.emit("did-update-state") } - heightForScreenRow (screenRow) { - return this.lineTopIndex.bottomPixelPositionForRow(screenRow) - this.lineTopIndex.topPixelPositionForRow(screenRow) - } - decorationsForScreenRow (screenRow) { let blocks = this.lineTopIndex.allBlocks().filter((block) => block.row == screenRow) return blocks.map((block) => this.decorationsByBlock.get(block.id)).filter((decoration) => decoration) @@ -103,18 +90,21 @@ class BlockDecorationsPresenter { return } - // TODO: change this with a "on manual did change" event. - let didMoveDisposable = decoration.getMarker().onDidChange((markerEvent) => { + let didMoveDisposable = decoration.getMarker().bufferMarker.onDidChange((markerEvent) => { this.didMoveDecoration(decoration, markerEvent) }) let didDestroyDisposable = decoration.onDidDestroy(() => { + this.disposables.remove(didMoveDisposable) + this.disposables.remove(didDestroyDisposable) didMoveDisposable.dispose() didDestroyDisposable.dispose() this.observedDecorations.delete(decoration) this.didDestroyDecoration(decoration) }) + this.disposables.add(didMoveDisposable) + this.disposables.add(didDestroyDisposable) this.didAddDecoration(decoration) this.observedDecorations.add(decoration) } @@ -127,7 +117,12 @@ class BlockDecorationsPresenter { this.emitter.emit("did-update-state") } - didMoveDecoration (decoration, {oldHeadScreenPosition, newHeadScreenPosition}) { + didMoveDecoration (decoration, {textChanged}) { + if (textChanged) { + // No need to move blocks because of a text change, because we already splice on buffer change. + return + } + let block = this.blocksByDecoration.get(decoration) let newScreenRow = decoration.getMarker().getHeadScreenPosition().row this.lineTopIndex.moveBlock(block, newScreenRow) diff --git a/src/linear-line-top-index.js b/src/linear-line-top-index.js index 1c7fa0503..82778933f 100644 --- a/src/linear-line-top-index.js +++ b/src/linear-line-top-index.js @@ -50,6 +50,11 @@ class LineTopIndex { return this.blocks } + blocksHeightForRow (row) { + let blocksForRow = this.blocks.filter((block) => block.row == row) + return blocksForRow.reduce((a, b) => a + b.height, 0) + } + splice (startRow, oldExtent, newExtent) { this.blocks.forEach(function (block) { if (block.row >= startRow) { diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 54ffc041f..72d1cec54 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -715,7 +715,7 @@ class TextEditorPresenter line = @model.tokenizedLineForScreenRow(screenRow) decorationClasses = @lineNumberDecorationClassesForRow(screenRow) foldable = @model.isFoldableAtScreenRow(screenRow) - blockDecorationsHeight = @blockDecorationsPresenter.heightForScreenRow(screenRow) + blockDecorationsHeight = @lineTopIndex.blocksHeightForRow(screenRow) tileState.lineNumbers[line.id] = {screenRow, bufferRow, softWrapped, decorationClasses, foldable, blockDecorationsHeight} visibleLineNumberIds[line.id] = true @@ -725,9 +725,6 @@ class TextEditorPresenter return - getScreenRowHeight: (screenRow) -> - @lineHeight + @blockDecorationsPresenter.heightForScreenRow(screenRow) - updateStartRow: -> return unless @scrollTop? and @lineHeight? From 937116a2808771b8a6b43f8017463b136bffcb2d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 3 Dec 2015 11:26:45 +0100 Subject: [PATCH 164/502] Render only visible and yet-to-be-measured block decorations --- spec/text-editor-presenter-spec.coffee | 101 ++++++++++++------------- src/block-decorations-component.coffee | 4 +- src/block-decorations-presenter.js | 15 +++- src/text-editor-component.coffee | 3 + src/text-editor-element.coffee | 3 + src/text-editor-presenter.coffee | 11 ++- 6 files changed, 78 insertions(+), 59 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index be8ce974e..52d2e2bc0 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -2129,62 +2129,61 @@ describe "TextEditorPresenter", -> isVisible: false } - waitsForStateToUpdate presenter, -> - editor.getBuffer().insert([0, 0], 'Hello world \n\n\n\n') + presenter.setBlockDecorationDimensions(blockDecoration1, 0, 10) + presenter.setBlockDecorationDimensions(blockDecoration4, 0, 20) - runs -> - expectValues stateForBlockDecoration(presenter, blockDecoration1), { - decoration: blockDecoration1 - screenRow: 4 - isVisible: true - } - expectValues stateForBlockDecoration(presenter, blockDecoration2), { - decoration: blockDecoration2 - screenRow: 8 - isVisible: false - } - expectValues stateForBlockDecoration(presenter, blockDecoration3), { - decoration: blockDecoration3 - screenRow: 8 - isVisible: false - } - expectValues stateForBlockDecoration(presenter, blockDecoration4), { - decoration: blockDecoration4 - screenRow: 14 - isVisible: false - } + expectValues stateForBlockDecoration(presenter, blockDecoration1), { + decoration: blockDecoration1 + screenRow: 0 + isVisible: true + } + expectValues stateForBlockDecoration(presenter, blockDecoration2), { + decoration: blockDecoration2 + screenRow: 4 + isVisible: false + } + expectValues stateForBlockDecoration(presenter, blockDecoration3), { + decoration: blockDecoration3 + screenRow: 4 + isVisible: false + } + expect(stateForBlockDecoration(presenter, blockDecoration4)).toBeUndefined() - waitsForStateToUpdate presenter, -> - blockDecoration2.destroy() - blockDecoration4.destroy() - presenter.setBlockDecorationDimensions(blockDecoration1, 0, 20) + presenter.setScrollTop(90) - runs -> - expectValues stateForBlockDecoration(presenter, blockDecoration1), { - decoration: blockDecoration1 - screenRow: 4 - isVisible: true - } - expect(stateForBlockDecoration(presenter, blockDecoration2)).toBeUndefined() - expectValues stateForBlockDecoration(presenter, blockDecoration3), { - decoration: blockDecoration3 - screenRow: 8 - isVisible: false - } - expect(stateForBlockDecoration(presenter, blockDecoration4)).toBeUndefined() + expect(stateForBlockDecoration(presenter, blockDecoration1)).toBeUndefined() + expectValues stateForBlockDecoration(presenter, blockDecoration2), { + decoration: blockDecoration2 + screenRow: 4 + isVisible: false + } + expectValues stateForBlockDecoration(presenter, blockDecoration3), { + decoration: blockDecoration3 + screenRow: 4 + isVisible: false + } + expectValues stateForBlockDecoration(presenter, blockDecoration4), { + decoration: blockDecoration4 + screenRow: 10 + isVisible: true + } - presenter.setScrollTop(80) + presenter.invalidateBlockDecorationDimensions(blockDecoration1) + presenter.setBlockDecorationDimensions(blockDecoration2, 0, 10) + presenter.setBlockDecorationDimensions(blockDecoration3, 0, 10) - expectValues stateForBlockDecoration(presenter, blockDecoration1), { - decoration: blockDecoration1 - screenRow: 4 - isVisible: false - } - expectValues stateForBlockDecoration(presenter, blockDecoration3), { - decoration: blockDecoration3 - screenRow: 8 - isVisible: true - } + expectValues stateForBlockDecoration(presenter, blockDecoration1), { + decoration: blockDecoration1 + screenRow: 0 + isVisible: false + } + expect(stateForBlockDecoration(presenter, blockDecoration2)).toBeUndefined() + expect(stateForBlockDecoration(presenter, blockDecoration3)).toBeUndefined() + expectValues stateForBlockDecoration(presenter, blockDecoration4), { + decoration: blockDecoration4 + screenRow: 10 + isVisible: true + } describe ".overlays", -> [item] = [] diff --git a/src/block-decorations-component.coffee b/src/block-decorations-component.coffee index b214fd584..50ac52fc7 100644 --- a/src/block-decorations-component.coffee +++ b/src/block-decorations-component.coffee @@ -60,9 +60,9 @@ class BlockDecorationsComponent oldBlockDecorationState = @oldState.blockDecorations[id] blockDecorationNode = @blockDecorationNodesById[id] - if newBlockDecorationState.isVisible and not oldBlockDecorationState.isVisible + if newBlockDecorationState.isVisible blockDecorationNode.classList.remove("atom--invisible-block-decoration") - else if not newBlockDecorationState.isVisible and oldBlockDecorationState.isVisible + else blockDecorationNode.classList.add("atom--invisible-block-decoration") if newBlockDecorationState.screenRow isnt oldBlockDecorationState.screenRow diff --git a/src/block-decorations-presenter.js b/src/block-decorations-presenter.js index 31571f57a..a5306a7ef 100644 --- a/src/block-decorations-presenter.js +++ b/src/block-decorations-presenter.js @@ -14,6 +14,7 @@ class BlockDecorationsPresenter { this.blocksByDecoration = new Map this.decorationsByBlock = new Map this.observedDecorations = new Set + this.measuredDecorations = new Set this.observeModel() } @@ -62,6 +63,12 @@ class BlockDecorationsPresenter { this.lineTopIndex.resizeBlock(block, height) } + this.measuredDecorations.add(decoration) + this.emitter.emit("did-update-state") + } + + invalidateDimensionsForDecoration (decoration) { + this.measuredDecorations.delete(decoration) this.emitter.emit("did-update-state") } @@ -70,14 +77,16 @@ class BlockDecorationsPresenter { return blocks.map((block) => this.decorationsByBlock.get(block.id)).filter((decoration) => decoration) } - getAllDecorationsByScreenRow () { + decorationsForScreenRowRange (startRow, endRow) { let blocks = this.lineTopIndex.allBlocks() let decorationsByScreenRow = new Map for (let block of blocks) { let decoration = this.decorationsByBlock.get(block.id) - if (decoration) { + let hasntMeasuredDecoration = !this.measuredDecorations.has(decoration) + let isVisible = startRow <= block.row && block.row < endRow + if (decoration && (isVisible || hasntMeasuredDecoration)) { let decorations = decorationsByScreenRow.get(block.row) || [] - decorations.push(decoration) + decorations.push({decoration, isVisible}) decorationsByScreenRow.set(block.row, decorations) } } diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index e5e63c371..afe46c705 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -456,6 +456,9 @@ class TextEditorComponent @editor.screenPositionForBufferPosition(bufferPosition) ) + invalidateBlockDecorationDimensions: -> + @presenter.invalidateBlockDecorationDimensions(arguments...) + onMouseDown: (event) => unless event.button is 0 or (event.button is 1 and process.platform is 'linux') # Only handle mouse down events for left mouse button on all platforms diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index 1a55eb002..6722f51df 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -347,4 +347,7 @@ class TextEditorElement extends HTMLElement getHeight: -> @offsetHeight + invalidateBlockDecorationDimensions: -> + @component.invalidateBlockDecorationDimensions(arguments...) + module.exports = TextEditorElement = document.registerElement 'atom-text-editor', prototype: TextEditorElement.prototype diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 72d1cec54..b2bc16ca9 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -1213,9 +1213,11 @@ class TextEditorPresenter updateBlockDecorationsState: -> @state.content.blockDecorations = {} - @blockDecorationsPresenter.getAllDecorationsByScreenRow().forEach (decorations, screenRow) => - for decoration in decorations - isVisible = @getStartTileRow() <= screenRow < @getEndTileRow() + @tileSize + startRow = @getStartTileRow() + endRow = @getEndTileRow() + @tileSize + decorations = @blockDecorationsPresenter.decorationsForScreenRowRange(startRow, endRow) + decorations.forEach (decorations, screenRow) => + for {decoration, isVisible} in decorations @state.content.blockDecorations[decoration.id] = {decoration, screenRow, isVisible} updateLineDecorations: -> @@ -1414,6 +1416,9 @@ class TextEditorPresenter setBlockDecorationDimensions: -> @blockDecorationsPresenter.setDimensionsForDecoration(arguments...) + invalidateBlockDecorationDimensions: -> + @blockDecorationsPresenter.invalidateDimensionsForDecoration(arguments...) + observeCursor: (cursor) -> didChangePositionDisposable = cursor.onDidChangePosition => @shouldUpdateHiddenInputState = true if cursor.isLastCursor() From f22bd5d0aebbb1cd51596a058762342468833342 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 3 Dec 2015 11:52:10 +0100 Subject: [PATCH 165/502] :racehorse: Use ids instead of classes --- spec/text-editor-presenter-spec.coffee | 84 +++++++++++++------------- src/block-decorations-component.coffee | 7 +-- src/lines-tile-component.coffee | 6 +- src/text-editor-presenter.coffee | 6 +- 4 files changed, 50 insertions(+), 53 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 52d2e2bc0..f28dade69 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -1273,8 +1273,8 @@ describe "TextEditorPresenter", -> expect(lineStateForScreenRow(presenter, 0).endOfLineInvisibles).toEqual [atom.config.get('editor.invisibles.eol')] expect(lineStateForScreenRow(presenter, 1).endOfLineInvisibles).toEqual [atom.config.get('editor.invisibles.cr'), atom.config.get('editor.invisibles.eol')] - describe ".hasBlockDecorations", -> - it "is true when block decorations are present before a line, both initially and when decorations change", -> + describe ".blockDecorations", -> + it "contains all block decorations that are present before a line, both initially and when decorations change", -> blockDecoration1 = editor.addBlockDecorationForScreenRow(0) presenter = buildPresenter() blockDecoration2 = editor.addBlockDecorationForScreenRow(3) @@ -1282,58 +1282,58 @@ describe "TextEditorPresenter", -> waitsForStateToUpdate presenter runs -> - expect(lineStateForScreenRow(presenter, 0).hasBlockDecorations).toBe(true) - expect(lineStateForScreenRow(presenter, 1).hasBlockDecorations).toBe(false) - expect(lineStateForScreenRow(presenter, 2).hasBlockDecorations).toBe(false) - expect(lineStateForScreenRow(presenter, 3).hasBlockDecorations).toBe(true) - expect(lineStateForScreenRow(presenter, 4).hasBlockDecorations).toBe(false) - expect(lineStateForScreenRow(presenter, 5).hasBlockDecorations).toBe(false) - expect(lineStateForScreenRow(presenter, 6).hasBlockDecorations).toBe(false) - expect(lineStateForScreenRow(presenter, 7).hasBlockDecorations).toBe(true) - expect(lineStateForScreenRow(presenter, 8).hasBlockDecorations).toBe(false) - expect(lineStateForScreenRow(presenter, 9).hasBlockDecorations).toBe(false) - expect(lineStateForScreenRow(presenter, 10).hasBlockDecorations).toBe(false) - expect(lineStateForScreenRow(presenter, 11).hasBlockDecorations).toBe(false) - expect(lineStateForScreenRow(presenter, 12).hasBlockDecorations).toBe(false) + expect(lineStateForScreenRow(presenter, 0).blockDecorations).toEqual([blockDecoration1]) + expect(lineStateForScreenRow(presenter, 1).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 2).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 3).blockDecorations).toEqual([blockDecoration2]) + expect(lineStateForScreenRow(presenter, 4).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 5).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 6).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 7).blockDecorations).toEqual([blockDecoration3]) + expect(lineStateForScreenRow(presenter, 8).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 9).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 10).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 11).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 12).blockDecorations).toEqual([]) waitsForStateToUpdate presenter, -> blockDecoration1.getMarker().setHeadBufferPosition([1, 0]) - blockDecoration2.getMarker().setHeadBufferPosition([5, 0]) + blockDecoration2.getMarker().setHeadBufferPosition([9, 0]) blockDecoration3.getMarker().setHeadBufferPosition([9, 0]) runs -> - expect(lineStateForScreenRow(presenter, 0).hasBlockDecorations).toBe(false) - expect(lineStateForScreenRow(presenter, 1).hasBlockDecorations).toBe(true) - expect(lineStateForScreenRow(presenter, 2).hasBlockDecorations).toBe(false) - expect(lineStateForScreenRow(presenter, 3).hasBlockDecorations).toBe(false) - expect(lineStateForScreenRow(presenter, 4).hasBlockDecorations).toBe(false) - expect(lineStateForScreenRow(presenter, 5).hasBlockDecorations).toBe(true) - expect(lineStateForScreenRow(presenter, 6).hasBlockDecorations).toBe(false) - expect(lineStateForScreenRow(presenter, 7).hasBlockDecorations).toBe(false) - expect(lineStateForScreenRow(presenter, 8).hasBlockDecorations).toBe(false) - expect(lineStateForScreenRow(presenter, 9).hasBlockDecorations).toBe(true) - expect(lineStateForScreenRow(presenter, 10).hasBlockDecorations).toBe(false) - expect(lineStateForScreenRow(presenter, 11).hasBlockDecorations).toBe(false) - expect(lineStateForScreenRow(presenter, 12).hasBlockDecorations).toBe(false) + expect(lineStateForScreenRow(presenter, 0).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 1).blockDecorations).toEqual([blockDecoration1]) + expect(lineStateForScreenRow(presenter, 2).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 3).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 4).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 5).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 6).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 7).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 8).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 9).blockDecorations).toEqual([blockDecoration3, blockDecoration2]) + expect(lineStateForScreenRow(presenter, 10).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 11).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 12).blockDecorations).toEqual([]) waitsForStateToUpdate presenter, -> blockDecoration1.destroy() blockDecoration3.destroy() runs -> - expect(lineStateForScreenRow(presenter, 0).hasBlockDecorations).toBe(false) - expect(lineStateForScreenRow(presenter, 1).hasBlockDecorations).toBe(false) - expect(lineStateForScreenRow(presenter, 2).hasBlockDecorations).toBe(false) - expect(lineStateForScreenRow(presenter, 3).hasBlockDecorations).toBe(false) - expect(lineStateForScreenRow(presenter, 4).hasBlockDecorations).toBe(false) - expect(lineStateForScreenRow(presenter, 5).hasBlockDecorations).toBe(true) - expect(lineStateForScreenRow(presenter, 6).hasBlockDecorations).toBe(false) - expect(lineStateForScreenRow(presenter, 7).hasBlockDecorations).toBe(false) - expect(lineStateForScreenRow(presenter, 8).hasBlockDecorations).toBe(false) - expect(lineStateForScreenRow(presenter, 9).hasBlockDecorations).toBe(false) - expect(lineStateForScreenRow(presenter, 10).hasBlockDecorations).toBe(false) - expect(lineStateForScreenRow(presenter, 11).hasBlockDecorations).toBe(false) - expect(lineStateForScreenRow(presenter, 12).hasBlockDecorations).toBe(false) + expect(lineStateForScreenRow(presenter, 0).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 1).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 2).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 3).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 4).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 5).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 6).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 7).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 8).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 9).blockDecorations).toEqual([blockDecoration2]) + expect(lineStateForScreenRow(presenter, 10).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 11).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 12).blockDecorations).toEqual([]) describe ".decorationClasses", -> it "adds decoration classes to the relevant line state objects, both initially and when decorations change", -> diff --git a/src/block-decorations-component.coffee b/src/block-decorations-component.coffee index 50ac52fc7..2a8574ef6 100644 --- a/src/block-decorations-component.coffee +++ b/src/block-decorations-component.coffee @@ -47,7 +47,7 @@ class BlockDecorationsComponent createAndAppendBlockDecorationNode: (id) -> blockDecorationState = @newState.blockDecorations[id] blockDecorationNode = @views.getView(blockDecorationState.decoration.getProperties().item) - blockDecorationNode.classList.add("block-decoration-row-#{blockDecorationState.screenRow}") + blockDecorationNode.id = "atom--block-decoration-#{id}" unless blockDecorationState.isVisible blockDecorationNode.classList.add("atom--invisible-block-decoration") @@ -57,14 +57,9 @@ class BlockDecorationsComponent updateBlockDecorationNode: (id) -> newBlockDecorationState = @newState.blockDecorations[id] - oldBlockDecorationState = @oldState.blockDecorations[id] blockDecorationNode = @blockDecorationNodesById[id] if newBlockDecorationState.isVisible blockDecorationNode.classList.remove("atom--invisible-block-decoration") else blockDecorationNode.classList.add("atom--invisible-block-decoration") - - if newBlockDecorationState.screenRow isnt oldBlockDecorationState.screenRow - blockDecorationNode.classList.remove("block-decoration-row-#{oldBlockDecorationState.screenRow}") - blockDecorationNode.classList.add("block-decoration-row-#{newBlockDecorationState.screenRow}") diff --git a/src/lines-tile-component.coffee b/src/lines-tile-component.coffee index f4b0dbab5..f447c3352 100644 --- a/src/lines-tile-component.coffee +++ b/src/lines-tile-component.coffee @@ -138,11 +138,11 @@ class LinesTileComponent @updateBlockDecorationInsertionPoint(id) updateBlockDecorationInsertionPoint: (id) -> - {screenRow} = @newTileState.lines[id] - + {blockDecorations, screenRow} = @newTileState.lines[id] + elementsIds = blockDecorations.map((d) -> "#atom--block-decoration-#{d.id}").join(',') if insertionPoint = @insertionPointsByLineId[id] insertionPoint.dataset.screenRow = screenRow - insertionPoint.setAttribute("select", ".block-decoration-row-#{screenRow}") + insertionPoint.setAttribute("select", elementsIds) findNodeNextTo: (node) -> for nextNode, index in @domNode.children diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index b2bc16ca9..637a3a011 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -499,7 +499,8 @@ class TextEditorPresenter lineState = tileState.lines[line.id] lineState.screenRow = screenRow lineState.decorationClasses = @lineDecorationClassesForRow(screenRow) - lineState.hasBlockDecorations = blockDecorations.length isnt 0 + lineState.blockDecorations = blockDecorations + lineState.hasBlockDecorations = blockDecorations.length > 0 else tileState.lines[line.id] = screenRow: screenRow @@ -516,7 +517,8 @@ class TextEditorPresenter tabLength: line.tabLength fold: line.fold decorationClasses: @lineDecorationClassesForRow(screenRow) - hasBlockDecorations: blockDecorations.length isnt 0 + blockDecorations: blockDecorations + hasBlockDecorations: blockDecorations.length > 0 for id, line of tileState.lines delete tileState.lines[id] unless visibleLineIds.hasOwnProperty(id) From e75263b5e08a524eb2096c6e8462426562daaa80 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 3 Dec 2015 13:07:14 +0100 Subject: [PATCH 166/502] :fire: --- src/text-editor-presenter.coffee | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 637a3a011..dcae0966c 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -195,7 +195,6 @@ class TextEditorPresenter @shouldUpdateLineNumbersState = true @shouldUpdateDecorations = true @shouldUpdateOverlaysState = true - @shouldUpdateVerticalScrollState = true @shouldUpdateCustomGutterDecorationState = true @emitDidUpdateState() @@ -212,7 +211,6 @@ class TextEditorPresenter @shouldUpdateLineNumbersState = true @shouldUpdateGutterOrderState = true @shouldUpdateCustomGutterDecorationState = true - @emitDidUpdateState() @disposables.add @model.onDidChangeGrammar(@didChangeGrammar.bind(this)) From 47644ee48738bf13dc3b35f30580b35225843e63 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 3 Dec 2015 13:09:10 +0100 Subject: [PATCH 167/502] More :fire: --- src/text-editor-presenter.coffee | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index dcae0966c..2a869045f 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -391,9 +391,6 @@ class TextEditorPresenter getEndTileRow: -> @constrainRow(@tileForRow(@endRow)) - getTileSize: -> - @tileSize - isValidScreenRow: (screenRow) -> screenRow >= 0 and screenRow < @model.getScreenLineCount() @@ -533,7 +530,6 @@ class TextEditorPresenter return unless cursor.isVisible() and @startRow <= screenRange.start.row < @endRow pixelRect = @pixelRectForScreenRange(screenRange) - pixelRect.height = @lineHeight pixelRect.width = Math.round(@baseCharacterWidth) if pixelRect.width is 0 @state.content.cursors[cursor.id] = pixelRect @@ -762,9 +758,6 @@ class TextEditorPresenter @scrollHeight = scrollHeight @updateScrollTop(@scrollTop) - getLinesHeight: -> - @lineHeight * @model.getScreenLineCount() - updateVerticalDimensions: -> if @lineHeight? oldContentHeight = @contentHeight From e23af02606ef6ed6e575c0344ea2027e97a11ce2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 3 Dec 2015 13:11:41 +0100 Subject: [PATCH 168/502] Fix linting errors --- src/block-decorations-presenter.js | 28 ++++++++++++++-------------- src/linear-line-top-index.js | 14 +++++++------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/block-decorations-presenter.js b/src/block-decorations-presenter.js index a5306a7ef..276fbb401 100644 --- a/src/block-decorations-presenter.js +++ b/src/block-decorations-presenter.js @@ -11,10 +11,10 @@ class BlockDecorationsPresenter { this.emitter = new Emitter() this.firstUpdate = true this.lineTopIndex = lineTopIndex - this.blocksByDecoration = new Map - this.decorationsByBlock = new Map - this.observedDecorations = new Set - this.measuredDecorations = new Set + this.blocksByDecoration = new Map() + this.decorationsByBlock = new Map() + this.observedDecorations = new Set() + this.measuredDecorations = new Set() this.observeModel() } @@ -24,7 +24,7 @@ class BlockDecorationsPresenter { } onDidUpdateState (callback) { - return this.emitter.on("did-update-state", callback) + return this.emitter.on('did-update-state', callback) } setLineHeight (lineHeight) { @@ -46,7 +46,7 @@ class BlockDecorationsPresenter { update () { if (this.firstUpdate) { - for (let decoration of this.model.getDecorations({type: "block"})) { + for (let decoration of this.model.getDecorations({type: 'block'})) { this.observeDecoration(decoration) } this.firstUpdate = false @@ -64,22 +64,22 @@ class BlockDecorationsPresenter { } this.measuredDecorations.add(decoration) - this.emitter.emit("did-update-state") + this.emitter.emit('did-update-state') } invalidateDimensionsForDecoration (decoration) { this.measuredDecorations.delete(decoration) - this.emitter.emit("did-update-state") + this.emitter.emit('did-update-state') } decorationsForScreenRow (screenRow) { - let blocks = this.lineTopIndex.allBlocks().filter((block) => block.row == screenRow) + let blocks = this.lineTopIndex.allBlocks().filter((block) => block.row === screenRow) return blocks.map((block) => this.decorationsByBlock.get(block.id)).filter((decoration) => decoration) } decorationsForScreenRowRange (startRow, endRow) { let blocks = this.lineTopIndex.allBlocks() - let decorationsByScreenRow = new Map + let decorationsByScreenRow = new Map() for (let block of blocks) { let decoration = this.decorationsByBlock.get(block.id) let hasntMeasuredDecoration = !this.measuredDecorations.has(decoration) @@ -95,7 +95,7 @@ class BlockDecorationsPresenter { } observeDecoration (decoration) { - if (!decoration.isType("block") || this.observedDecorations.has(decoration)) { + if (!decoration.isType('block') || this.observedDecorations.has(decoration)) { return } @@ -123,7 +123,7 @@ class BlockDecorationsPresenter { let block = this.lineTopIndex.insertBlock(screenRow, 0) this.decorationsByBlock.set(block, decoration) this.blocksByDecoration.set(decoration, block) - this.emitter.emit("did-update-state") + this.emitter.emit('did-update-state') } didMoveDecoration (decoration, {textChanged}) { @@ -135,7 +135,7 @@ class BlockDecorationsPresenter { let block = this.blocksByDecoration.get(decoration) let newScreenRow = decoration.getMarker().getHeadScreenPosition().row this.lineTopIndex.moveBlock(block, newScreenRow) - this.emitter.emit("did-update-state") + this.emitter.emit('did-update-state') } didDestroyDecoration (decoration) { @@ -145,6 +145,6 @@ class BlockDecorationsPresenter { this.blocksByDecoration.delete(decoration) this.decorationsByBlock.delete(block) } - this.emitter.emit("did-update-state") + this.emitter.emit('did-update-state') } } diff --git a/src/linear-line-top-index.js b/src/linear-line-top-index.js index 82778933f..a930b4a38 100644 --- a/src/linear-line-top-index.js +++ b/src/linear-line-top-index.js @@ -25,14 +25,14 @@ class LineTopIndex { } resizeBlock (id, height) { - let block = this.blocks.find((block) => block.id == id) + let block = this.blocks.find((block) => block.id === id) if (block) { block.height = height } } moveBlock (id, newRow) { - let block = this.blocks.find((block) => block.id == id) + let block = this.blocks.find((block) => block.id === id) if (block) { block.row = newRow this.blocks.sort((a, b) => a.row - b.row) @@ -40,8 +40,8 @@ class LineTopIndex { } removeBlock (id) { - let index = this.blocks.findIndex((block) => block.id == id) - if (index != -1) { + let index = this.blocks.findIndex((block) => block.id === id) + if (index !== -1) { this.blocks.splice(index, 1) } } @@ -51,7 +51,7 @@ class LineTopIndex { } blocksHeightForRow (row) { - let blocksForRow = this.blocks.filter((block) => block.row == row) + let blocksForRow = this.blocks.filter((block) => block.row === row) return blocksForRow.reduce((a, b) => a + b.height, 0) } @@ -104,9 +104,9 @@ class LineTopIndex { let remainingHeight = Math.max(0, top - lastTop) let remainingRows = Math.min(this.maxRow, lastRow + remainingHeight / this.defaultLineHeight) switch (roundingStrategy) { - case "floor": + case 'floor': return Math.floor(remainingRows) - case "ceil": + case 'ceil': return Math.ceil(remainingRows) default: throw new Error(`Cannot use '${roundingStrategy}' as a rounding strategy!`) From 5f6f99259e39b349bccdac46e2418cb8203beb02 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 3 Dec 2015 15:05:14 +0100 Subject: [PATCH 169/502] Ensure custom gutters work properly --- spec/text-editor-presenter-spec.coffee | 48 ++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index f28dade69..1dce76e7c 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -2978,6 +2978,54 @@ describe "TextEditorPresenter", -> expect(decorationState[decoration1.id]).toBeUndefined() expect(decorationState[decoration3.id].top).toBeDefined() + it "updates when block decorations are added, changed or removed", -> + # block decoration before decoration1 + blockDecoration1 = editor.addBlockDecorationForScreenRow(0) + presenter.setBlockDecorationDimensions(blockDecoration1, 0, 3) + # block decoration between decoration1 and decoration2 + blockDecoration2 = editor.addBlockDecorationForScreenRow(3) + presenter.setBlockDecorationDimensions(blockDecoration2, 0, 5) + # block decoration between decoration2 and decoration3 + blockDecoration3 = editor.addBlockDecorationForScreenRow(10) + presenter.setBlockDecorationDimensions(blockDecoration3, 0, 7) + + decorationState = getContentForGutterWithName(presenter, 'test-gutter') + expect(decorationState[decoration1.id].top).toBe lineHeight * marker1.getScreenRange().start.row + expect(decorationState[decoration1.id].height).toBe lineHeight * marker1.getScreenRange().getRowCount() + 3 + expect(decorationState[decoration1.id].item).toBe decorationItem + expect(decorationState[decoration1.id].class).toBe 'test-class' + expect(decorationState[decoration2.id].top).toBe lineHeight * marker2.getScreenRange().start.row + 3 + 5 + expect(decorationState[decoration2.id].height).toBe lineHeight * marker2.getScreenRange().getRowCount() + 7 + expect(decorationState[decoration2.id].item).toBe decorationItem + expect(decorationState[decoration2.id].class).toBe 'test-class' + expect(decorationState[decoration3.id]).toBeUndefined() + + presenter.setScrollTop(scrollTop + lineHeight * 5) + + decorationState = getContentForGutterWithName(presenter, 'test-gutter') + expect(decorationState[decoration1.id]).toBeUndefined() + expect(decorationState[decoration2.id].top).toBe lineHeight * marker2.getScreenRange().start.row + 3 + 5 + expect(decorationState[decoration2.id].height).toBe lineHeight * marker2.getScreenRange().getRowCount() + 7 + expect(decorationState[decoration2.id].item).toBe decorationItem + expect(decorationState[decoration2.id].class).toBe 'test-class' + expect(decorationState[decoration3.id].top).toBe lineHeight * marker3.getScreenRange().start.row + 3 + 5 + 7 + expect(decorationState[decoration3.id].height).toBe lineHeight * marker3.getScreenRange().getRowCount() + expect(decorationState[decoration3.id].item).toBe decorationItem + expect(decorationState[decoration3.id].class).toBe 'test-class' + + waitsForStateToUpdate presenter, -> blockDecoration1.destroy() + runs -> + decorationState = getContentForGutterWithName(presenter, 'test-gutter') + expect(decorationState[decoration1.id]).toBeUndefined() + expect(decorationState[decoration2.id].top).toBe lineHeight * marker2.getScreenRange().start.row + 5 + expect(decorationState[decoration2.id].height).toBe lineHeight * marker2.getScreenRange().getRowCount() + 7 + expect(decorationState[decoration2.id].item).toBe decorationItem + expect(decorationState[decoration2.id].class).toBe 'test-class' + expect(decorationState[decoration3.id].top).toBe lineHeight * marker3.getScreenRange().start.row + 5 + 7 + expect(decorationState[decoration3.id].height).toBe lineHeight * marker3.getScreenRange().getRowCount() + expect(decorationState[decoration3.id].item).toBe decorationItem + expect(decorationState[decoration3.id].class).toBe 'test-class' + it "updates when ::scrollTop changes", -> # This update will scroll decoration1 out of view, and decoration3 into view. expectStateUpdate presenter, -> presenter.setScrollTop(scrollTop + lineHeight * 5) From da42fc74ed1596706e6aec5a10fe77dbd6f0c8c4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 3 Dec 2015 15:05:41 +0100 Subject: [PATCH 170/502] :fire: :lipstick: --- src/block-decorations-presenter.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/block-decorations-presenter.js b/src/block-decorations-presenter.js index 276fbb401..f1a05da85 100644 --- a/src/block-decorations-presenter.js +++ b/src/block-decorations-presenter.js @@ -1,7 +1,6 @@ /** @babel */ const {CompositeDisposable, Emitter} = require('event-kit') -const LineTopIndex = require('./linear-line-top-index') module.exports = class BlockDecorationsPresenter { From e4655c62e4b9d8d1bd33dba61088d1cadb77635c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 3 Dec 2015 15:07:54 +0100 Subject: [PATCH 171/502] :green_heart: Fix false negative --- spec/text-editor-presenter-spec.coffee | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 1dce76e7c..1ae540005 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -910,7 +910,7 @@ describe "TextEditorPresenter", -> expect(presenter.getState().content.scrollTop).toBe 13 it "scrolls down automatically when the model is changed", -> - presenter = buildPresenter(scrollTop: 0, lineHeight: 10, explicitHeight: 20) + presenter = buildPresenter(scrollTop: 0, lineHeight: 10, explicitHeight: 10) editor.setText("") editor.insertNewline() @@ -919,6 +919,9 @@ describe "TextEditorPresenter", -> editor.insertNewline() expect(presenter.getState().content.scrollTop).toBe(10) + editor.insertNewline() + expect(presenter.getState().content.scrollTop).toBe(20) + it "never exceeds the computed scroll height minus the computed client height", -> didChangeScrollTopSpy = jasmine.createSpy() presenter = buildPresenter(scrollTop: 10, lineHeight: 10, explicitHeight: 50, horizontalScrollbarHeight: 10) From f6688b6d712142d17d6cd0c0504973e17408f703 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 3 Dec 2015 15:24:55 +0100 Subject: [PATCH 172/502] Don't use babel when not needed --- src/block-decorations-presenter.js | 25 ++++++++++++------------- src/linear-line-top-index.js | 5 +++-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/block-decorations-presenter.js b/src/block-decorations-presenter.js index f1a05da85..9964f3eb9 100644 --- a/src/block-decorations-presenter.js +++ b/src/block-decorations-presenter.js @@ -1,13 +1,13 @@ -/** @babel */ +'use strict' -const {CompositeDisposable, Emitter} = require('event-kit') +const EventKit = require('event-kit') module.exports = class BlockDecorationsPresenter { constructor (model, lineTopIndex) { this.model = model - this.disposables = new CompositeDisposable() - this.emitter = new Emitter() + this.disposables = new EventKit.CompositeDisposable() + this.emitter = new EventKit.Emitter() this.firstUpdate = true this.lineTopIndex = lineTopIndex this.blocksByDecoration = new Map() @@ -33,13 +33,12 @@ class BlockDecorationsPresenter { observeModel () { this.lineTopIndex.setMaxRow(this.model.getScreenLineCount()) this.lineTopIndex.setDefaultLineHeight(this.model.getLineHeightInPixels()) - this.disposables.add(this.model.onDidAddDecoration((decoration) => { - this.observeDecoration(decoration) - })) - this.disposables.add(this.model.onDidChange(({start, end, screenDelta}) => { - let oldExtent = end - start - let newExtent = Math.max(0, end - start + screenDelta) - this.lineTopIndex.splice(start, oldExtent, newExtent) + + this.disposables.add(this.model.onDidAddDecoration(this.observeDecoration.bind(this))) + this.disposables.add(this.model.onDidChange((changeEvent) => { + let oldExtent = changeEvent.end - changeEvent.start + let newExtent = Math.max(0, changeEvent.end - changeEvent.start + changeEvent.screenDelta) + this.lineTopIndex.splice(changeEvent.start, oldExtent, newExtent) })) } @@ -125,8 +124,8 @@ class BlockDecorationsPresenter { this.emitter.emit('did-update-state') } - didMoveDecoration (decoration, {textChanged}) { - if (textChanged) { + didMoveDecoration (decoration, markerEvent) { + if (markerEvent.textChanged) { // No need to move blocks because of a text change, because we already splice on buffer change. return } diff --git a/src/linear-line-top-index.js b/src/linear-line-top-index.js index a930b4a38..3fe501ec9 100644 --- a/src/linear-line-top-index.js +++ b/src/linear-line-top-index.js @@ -1,4 +1,4 @@ -/** @babel */ +'use strict' module.exports = class LineTopIndex { @@ -81,7 +81,8 @@ class LineTopIndex { return this.topPixelPositionForRow(row + 1) - this.defaultLineHeight } - rowForTopPixelPosition (top, roundingStrategy='floor') { + rowForTopPixelPosition (top, strategy) { + const roundingStrategy = strategy || 'floor' let blocksHeight = 0 let lastRow = 0 let lastTop = 0 From a3a77180de58bdceeb78c452b1598e7ac9ff57cb Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 3 Dec 2015 09:33:30 -0500 Subject: [PATCH 173/502] More helpful type annotations. --- src/git-repository-async.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 131b1b51c..705f98870 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -412,10 +412,10 @@ export default class GitRepositoryAsync { options.pathspec = _path return Git.Diff.treeToWorkdir(repo, tree, options) }) - .then(diff => diff.patches()) // :: Array - .then(patches => Promise.all(patches.map(p => p.hunks()))) // :: Array> - .then(hunks => Promise.all(_.flatten(hunks).map(h => h.lines()))) // :: Array> - .then(lines => { + .then(diff => diff.patches()) + .then(patches => Promise.all(patches.map(p => p.hunks()))) // patches :: Array + .then(hunks => Promise.all(_.flatten(hunks).map(h => h.lines()))) // hunks :: Array> + .then(lines => { // lines :: Array> const stats = {added: 0, deleted: 0} for (const line of _.flatten(lines)) { const origin = line.origin() From 555d77afa650d04fa2173c4ad55a05e9277ffba4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 3 Dec 2015 15:45:47 +0100 Subject: [PATCH 174/502] Do not remove invisible decorations on ::mouseWheelScreenRow --- spec/text-editor-presenter-spec.coffee | 19 +++++++++++++++++++ src/block-decorations-presenter.js | 5 +++-- src/text-editor-presenter.coffee | 2 +- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 1ae540005..fa2f1ea4f 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -2103,6 +2103,25 @@ describe "TextEditorPresenter", -> stateForBlockDecoration = (presenter, decoration) -> presenter.getState().content.blockDecorations[decoration.id] + it "contains state for measured block decorations that are not visible when they are on ::mouseWheelScreenRow", -> + blockDecoration1 = editor.addBlockDecorationForScreenRow(0, null) + presenter = buildPresenter(explicitHeight: 30, lineHeight: 10, tileSize: 2, scrollTop: 0, stoppedScrollingDelay: 200) + presenter.getState() # flush pending state + presenter.setBlockDecorationDimensions(blockDecoration1, 0, 0) + + presenter.setScrollTop(100) + presenter.setMouseWheelScreenRow(0) + + expectValues stateForBlockDecoration(presenter, blockDecoration1), { + decoration: blockDecoration1 + screenRow: 0 + isVisible: true + } + + advanceClock(presenter.stoppedScrollingDelay) + + expect(stateForBlockDecoration(presenter, blockDecoration1)).toBeUndefined() + it "contains state for block decorations, indicating the screen row they belong to both initially and when their markers move", -> item = {} blockDecoration1 = editor.addBlockDecorationForScreenRow(0, item) diff --git a/src/block-decorations-presenter.js b/src/block-decorations-presenter.js index 9964f3eb9..51944e584 100644 --- a/src/block-decorations-presenter.js +++ b/src/block-decorations-presenter.js @@ -75,13 +75,14 @@ class BlockDecorationsPresenter { return blocks.map((block) => this.decorationsByBlock.get(block.id)).filter((decoration) => decoration) } - decorationsForScreenRowRange (startRow, endRow) { + decorationsForScreenRowRange (startRow, endRow, mouseWheelScreenRow) { let blocks = this.lineTopIndex.allBlocks() let decorationsByScreenRow = new Map() for (let block of blocks) { let decoration = this.decorationsByBlock.get(block.id) let hasntMeasuredDecoration = !this.measuredDecorations.has(decoration) - let isVisible = startRow <= block.row && block.row < endRow + let isWithinVisibleRange = startRow <= block.row && block.row < endRow + let isVisible = isWithinVisibleRange || block.row === mouseWheelScreenRow if (decoration && (isVisible || hasntMeasuredDecoration)) { let decorations = decorationsByScreenRow.get(block.row) || [] decorations.push({decoration, isVisible}) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 2a869045f..78bc67a08 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -1208,7 +1208,7 @@ class TextEditorPresenter startRow = @getStartTileRow() endRow = @getEndTileRow() + @tileSize - decorations = @blockDecorationsPresenter.decorationsForScreenRowRange(startRow, endRow) + decorations = @blockDecorationsPresenter.decorationsForScreenRowRange(startRow, endRow, @mouseWheelScreenRow) decorations.forEach (decorations, screenRow) => for {decoration, isVisible} in decorations @state.content.blockDecorations[decoration.id] = {decoration, screenRow, isVisible} From 81dbc5c867cc88b7b01663656ce19890136cc16e Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 3 Dec 2015 09:48:38 -0500 Subject: [PATCH 175/502] Move the refreshingCount changes closer to where the work is done. --- src/git-repository-async.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 705f98870..614d18a26 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -350,7 +350,10 @@ export default class GitRepositoryAsync { return status }) - .then(_ => this._refreshingCount--) + .then(status => { + this._refreshingCount-- + return status + }) } // Returns a Promise that resolves to the status bit of a given path if it has @@ -491,6 +494,8 @@ export default class GitRepositoryAsync { } _refreshStatus () { + this._refreshingCount++ + return this.repoPromise .then(repo => repo.getStatus()) .then(statuses => { @@ -506,22 +511,21 @@ export default class GitRepositoryAsync { this.pathStatusCache = newPathStatusCache return newPathStatusCache }) + .then(_ => this._refreshingCount--) } // Refreshes the git status. // - // Returns :: Promise + // Returns :: Promise // Resolves when refresh has completed. refreshStatus () { - this._refreshingCount++ - // TODO add submodule tracking const status = this._refreshStatus() const branch = this._refreshBranch() const aheadBehind = branch.then(branchName => this._refreshAheadBehindCount(branchName)) - return Promise.all([status, branch, aheadBehind]).then(_ => this._refreshingCount--) + return Promise.all([status, branch, aheadBehind]).then(_ => null) } // Section: Private From 9bb8703978e9438bbc84bd15b3f76aeb45c06896 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 3 Dec 2015 09:49:10 -0500 Subject: [PATCH 176/502] Don't make the async repo duplicate the work if it's wrapped by a sync repo. --- src/git-repository-async.js | 4 ++-- src/git-repository.coffee | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 614d18a26..2c86c129d 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -41,9 +41,9 @@ export default class GitRepositoryAsync { this.subscriptions.add(new Disposable(() => window.removeEventListener('focus', onWindowFocus))) } - const {project} = options + const {project, subscribeToBuffers} = options this.project = project - if (this.project) { + if (this.project && subscribeToBuffers) { this.project.getBuffers().forEach(buffer => this.subscribeToBuffer(buffer)) this.subscriptions.add(this.project.onDidAddBuffer(buffer => this.subscribeToBuffer(buffer))) } diff --git a/src/git-repository.coffee b/src/git-repository.coffee index 30cce92d0..5a0df6743 100644 --- a/src/git-repository.coffee +++ b/src/git-repository.coffee @@ -76,7 +76,14 @@ class GitRepository unless @repo? throw new Error("No Git repository found searching path: #{path}") - @async = GitRepositoryAsync.open(path, options) + asyncOptions = {} + for key, val of options + asyncOptions[key] = val + # GitRepository itself will handle these cases by manually calling through + # to the async repo. + asyncOptions.refreshOnWindowFocus = false + asyncOptions.subscribeToBuffers = false + @async = GitRepositoryAsync.open(path, asyncOptions) @statuses = {} @upstream = {ahead: 0, behind: 0} From 14b126ace0a187d09f9dc57e9ecc0f0226daac3f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 3 Dec 2015 16:06:36 +0100 Subject: [PATCH 177/502] :art: --- src/lines-tile-component.coffee | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/lines-tile-component.coffee b/src/lines-tile-component.coffee index f447c3352..6c676dfc2 100644 --- a/src/lines-tile-component.coffee +++ b/src/lines-tile-component.coffee @@ -138,11 +138,18 @@ class LinesTileComponent @updateBlockDecorationInsertionPoint(id) updateBlockDecorationInsertionPoint: (id) -> - {blockDecorations, screenRow} = @newTileState.lines[id] - elementsIds = blockDecorations.map((d) -> "#atom--block-decoration-#{d.id}").join(',') - if insertionPoint = @insertionPointsByLineId[id] + oldLineState = @oldTileState.lines[id] + newLineState = @newTileState.lines[id] + insertionPoint = @insertionPointsByLineId[id] + return unless insertionPoint? + + if newLineState.screenRow isnt oldLineState.screenRow insertionPoint.dataset.screenRow = screenRow - insertionPoint.setAttribute("select", elementsIds) + + blockDecorationsSelector = newLineState.blockDecorations.map((d) -> "#atom--block-decoration-#{d.id}").join(',') + if blockDecorationsSelector isnt oldLineState.blockDecorationsSelector + insertionPoint.setAttribute("select", blockDecorationsSelector) + oldLineState.blockDecorationsSelector = blockDecorationsSelector findNodeNextTo: (node) -> for nextNode, index in @domNode.children @@ -369,16 +376,16 @@ class LinesTileComponent else if oldLineState.hasBlockDecorations and not newLineState.hasBlockDecorations @removeBlockDecorationInsertionPoint(id) - oldLineState.hasBlockDecorations = newLineState.hasBlockDecorations - if newLineState.screenRow isnt oldLineState.screenRow - @updateBlockDecorationInsertionPoint(id) - lineNode.dataset.screenRow = newLineState.screenRow - oldLineState.screenRow = newLineState.screenRow @lineIdsByScreenRow[newLineState.screenRow] = id @screenRowsByLineId[id] = newLineState.screenRow + @updateBlockDecorationInsertionPoint(id) + + oldLineState.screenRow = newLineState.screenRow + oldLineState.hasBlockDecorations = newLineState.hasBlockDecorations + lineNodeForScreenRow: (screenRow) -> @lineNodesByLineId[@lineIdsByScreenRow[screenRow]] From 36103a024ac2c89359ce57afc74bef40d6e78a43 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 3 Dec 2015 16:25:24 +0100 Subject: [PATCH 178/502] Make sure to add screen row to block decoration nodes --- spec/text-editor-component-spec.js | 31 ++++++++++++++++++++++++++ src/block-decorations-component.coffee | 13 ++++++----- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 3b8409469..f4afad9ca 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -3540,6 +3540,37 @@ describe('TextEditorComponent', function () { }) }) + describe('when the mousewheel event\'s target is a block decoration', function () { + it('keeps it on the DOM if it is scrolled off-screen', async function () { + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + wrapperNode.style.width = 20 * charWidth + 'px' + component.measureDimensions() + await nextViewUpdatePromise() + + let item = document.createElement("div") + item.style.width = "30px" + item.style.height = "30px" + item.className = "decoration-1" + editor.addBlockDecorationForScreenRow(0, item) + + await nextViewUpdatePromise() + + let wheelEvent = new WheelEvent('mousewheel', { + wheelDeltaX: 0, + wheelDeltaY: -500 + }) + Object.defineProperty(wheelEvent, 'target', { + get: function () { + return item + } + }) + componentNode.dispatchEvent(wheelEvent) + await nextAnimationFramePromise() + + expect(component.getTopmostDOMNode().contains(item)).toBe(true) + }) + }) + it('only prevents the default action of the mousewheel event if it actually lead to scrolling', async function () { spyOn(WheelEvent.prototype, 'preventDefault').andCallThrough() wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' diff --git a/src/block-decorations-component.coffee b/src/block-decorations-component.coffee index 2a8574ef6..c154b85c6 100644 --- a/src/block-decorations-component.coffee +++ b/src/block-decorations-component.coffee @@ -31,10 +31,9 @@ class BlockDecorationsComponent if @oldState.blockDecorations.hasOwnProperty(id) @updateBlockDecorationNode(id) else + @oldState.blockDecorations[id] = {} @createAndAppendBlockDecorationNode(id) - @oldState.blockDecorations[id] = cloneObject(blockDecorationState) - measureBlockDecorations: -> for decorationId, blockDecorationNode of @blockDecorationNodesById decoration = @newState.blockDecorations[decorationId].decoration @@ -48,18 +47,20 @@ class BlockDecorationsComponent blockDecorationState = @newState.blockDecorations[id] blockDecorationNode = @views.getView(blockDecorationState.decoration.getProperties().item) blockDecorationNode.id = "atom--block-decoration-#{id}" - unless blockDecorationState.isVisible - blockDecorationNode.classList.add("atom--invisible-block-decoration") - @container.appendChild(blockDecorationNode) - @blockDecorationNodesById[id] = blockDecorationNode + @updateBlockDecorationNode(id) updateBlockDecorationNode: (id) -> newBlockDecorationState = @newState.blockDecorations[id] + oldBlockDecorationState = @oldState.blockDecorations[id] blockDecorationNode = @blockDecorationNodesById[id] if newBlockDecorationState.isVisible blockDecorationNode.classList.remove("atom--invisible-block-decoration") else blockDecorationNode.classList.add("atom--invisible-block-decoration") + + if oldBlockDecorationState.screenRow isnt newBlockDecorationState.screenRow + blockDecorationNode.dataset.screenRow = newBlockDecorationState.screenRow + oldBlockDecorationState.screenRow = newBlockDecorationState.screenRow From cc4344735e42faddfd7db47af5f5c9e337f63085 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 3 Dec 2015 16:30:15 +0100 Subject: [PATCH 179/502] :green_heart: Fix specs --- spec/lines-yardstick-spec.coffee | 5 +++-- spec/text-editor-component-spec.js | 25 +++++++++---------------- src/block-decorations-presenter.js | 5 +++++ src/lines-tile-component.coffee | 2 +- src/text-editor-component.coffee | 20 ++++++++++---------- src/text-editor-presenter.coffee | 9 +++++++-- 6 files changed, 35 insertions(+), 31 deletions(-) diff --git a/spec/lines-yardstick-spec.coffee b/spec/lines-yardstick-spec.coffee index ae85a0e9d..fbf5c53bf 100644 --- a/spec/lines-yardstick-spec.coffee +++ b/spec/lines-yardstick-spec.coffee @@ -1,4 +1,5 @@ -LinesYardstick = require "../src/lines-yardstick" +LinesYardstick = require '../src/lines-yardstick' +LineTopIndex = require '../src/linear-line-top-index' {toArray} = require 'underscore-plus' describe "LinesYardstick", -> @@ -56,7 +57,7 @@ describe "LinesYardstick", -> textNodes editor.setLineHeightInPixels(14) - linesYardstick = new LinesYardstick(editor, mockPresenter, mockLineNodesProvider, atom.grammars) + linesYardstick = new LinesYardstick(editor, mockPresenter, mockLineNodesProvider, new LineTopIndex(), atom.grammars) afterEach -> lineNode.remove() for lineNode in createdLineNodes diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index f4afad9ca..d214201fa 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -1663,7 +1663,7 @@ describe('TextEditorComponent', function () { atom.themes.removeStylesheet('test') }) - it("renders all the editor's block decorations, inserting them in the appropriate spots between lines", async function () { + it("renders visible and yet-to-be-measured block decorations, inserting them in the appropriate spots between lines and refreshing them when needed", async function () { wrapperNode.style.height = 9 * lineHeightInPixels + 'px' component.measureDimensions() await nextViewUpdatePromise() @@ -1680,7 +1680,7 @@ describe('TextEditorComponent', function () { atom-text-editor .decoration-4 { width: 30px; height: 120px; } ` - await nextViewUpdatePromise() + await nextAnimationFramePromise() expect(component.getDomNode().querySelectorAll(".line").length).toBe(7) @@ -1694,19 +1694,17 @@ describe('TextEditorComponent', function () { expect(component.getTopmostDOMNode().querySelector(".decoration-1")).toBe(item1) expect(component.getTopmostDOMNode().querySelector(".decoration-2")).toBe(item2) expect(component.getTopmostDOMNode().querySelector(".decoration-3")).toBe(item3) - expect(component.getTopmostDOMNode().querySelector(".decoration-4")).toBe(item4) + expect(component.getTopmostDOMNode().querySelector(".decoration-4")).toBeNull() expect(item1.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 0) expect(item2.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 2 + 80) expect(item3.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 4 + 80 + 40) - expect(item4.getBoundingClientRect().top).toBe(0) // hidden - expect(item4.getBoundingClientRect().height).toBe(120) editor.setCursorScreenPosition([0, 0]) editor.insertNewline() blockDecoration1.destroy() - await nextViewUpdatePromise() + await nextAnimationFramePromise() expect(component.getDomNode().querySelectorAll(".line").length).toBe(7) @@ -1720,21 +1718,19 @@ describe('TextEditorComponent', function () { expect(component.getTopmostDOMNode().querySelector(".decoration-1")).toBeNull() expect(component.getTopmostDOMNode().querySelector(".decoration-2")).toBe(item2) expect(component.getTopmostDOMNode().querySelector(".decoration-3")).toBe(item3) - expect(component.getTopmostDOMNode().querySelector(".decoration-4")).toBe(item4) + expect(component.getTopmostDOMNode().querySelector(".decoration-4")).toBeNull() - expect(item1.getBoundingClientRect().height).toBe(0) // deleted expect(item2.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 3) expect(item3.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 5 + 40) - expect(item4.getBoundingClientRect().top).toBe(0) // hidden - expect(item4.getBoundingClientRect().height).toBe(120) - await nextViewUpdatePromise() + await nextAnimationFramePromise() atom.styles.addStyleSheet ` atom-text-editor .decoration-2 { height: 60px !important; } ` - await nextViewUpdatePromise() + await nextAnimationFramePromise() // causes the DOM to update and to retrieve new styles + await nextAnimationFramePromise() // applies the changes expect(component.getDomNode().querySelectorAll(".line").length).toBe(7) @@ -1748,13 +1744,10 @@ describe('TextEditorComponent', function () { expect(component.getTopmostDOMNode().querySelector(".decoration-1")).toBeNull() expect(component.getTopmostDOMNode().querySelector(".decoration-2")).toBe(item2) expect(component.getTopmostDOMNode().querySelector(".decoration-3")).toBe(item3) - expect(component.getTopmostDOMNode().querySelector(".decoration-4")).toBe(item4) + expect(component.getTopmostDOMNode().querySelector(".decoration-4")).toBeNull() - expect(item1.getBoundingClientRect().height).toBe(0) // deleted expect(item2.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 3) expect(item3.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 5 + 60) - expect(item4.getBoundingClientRect().top).toBe(0) // hidden - expect(item4.getBoundingClientRect().height).toBe(120) // hidden }) }) diff --git a/src/block-decorations-presenter.js b/src/block-decorations-presenter.js index 51944e584..bfe08dd63 100644 --- a/src/block-decorations-presenter.js +++ b/src/block-decorations-presenter.js @@ -70,6 +70,11 @@ class BlockDecorationsPresenter { this.emitter.emit('did-update-state') } + measurementsChanged () { + this.measuredDecorations.clear() + this.emitter.emit('did-update-state') + } + decorationsForScreenRow (screenRow) { let blocks = this.lineTopIndex.allBlocks().filter((block) => block.row === screenRow) return blocks.map((block) => this.decorationsByBlock.get(block.id)).filter((decoration) => decoration) diff --git a/src/lines-tile-component.coffee b/src/lines-tile-component.coffee index 6c676dfc2..73e88aa32 100644 --- a/src/lines-tile-component.coffee +++ b/src/lines-tile-component.coffee @@ -144,7 +144,7 @@ class LinesTileComponent return unless insertionPoint? if newLineState.screenRow isnt oldLineState.screenRow - insertionPoint.dataset.screenRow = screenRow + insertionPoint.dataset.screenRow = newLineState.screenRow blockDecorationsSelector = newLineState.blockDecorations.map((d) -> "#atom--block-decoration-#{d.id}").join(',') if blockDecorationsSelector isnt oldLineState.blockDecorationsSelector diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index afe46c705..c35139d7c 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -283,13 +283,13 @@ class TextEditorComponent observeConfig: -> @disposables.add @config.onDidChange 'editor.fontSize', => @sampleFontStyling() - @invalidateCharacterWidths() + @invalidateMeasurements() @disposables.add @config.onDidChange 'editor.fontFamily', => @sampleFontStyling() - @invalidateCharacterWidths() + @invalidateMeasurements() @disposables.add @config.onDidChange 'editor.lineHeight', => @sampleFontStyling() - @invalidateCharacterWidths() + @invalidateMeasurements() onGrammarChanged: => if @scopedConfigDisposables? @@ -576,7 +576,7 @@ class TextEditorComponent handleStylingChange: => @sampleFontStyling() @sampleBackgroundColors() - @invalidateCharacterWidths() + @invalidateMeasurements() handleDragUntilMouseUp: (dragHandler) -> dragging = false @@ -730,7 +730,7 @@ class TextEditorComponent if @fontSize isnt oldFontSize or @fontFamily isnt oldFontFamily or @lineHeight isnt oldLineHeight @clearPoolAfterUpdate = true @measureLineHeightAndDefaultCharWidth() - @invalidateCharacterWidths() + @invalidateMeasurements() sampleBackgroundColors: (suppressUpdate) -> {backgroundColor} = getComputedStyle(@hostElement) @@ -840,7 +840,7 @@ class TextEditorComponent setFontSize: (fontSize) -> @getTopmostDOMNode().style.fontSize = fontSize + 'px' @sampleFontStyling() - @invalidateCharacterWidths() + @invalidateMeasurements() getFontFamily: -> getComputedStyle(@getTopmostDOMNode()).fontFamily @@ -848,16 +848,16 @@ class TextEditorComponent setFontFamily: (fontFamily) -> @getTopmostDOMNode().style.fontFamily = fontFamily @sampleFontStyling() - @invalidateCharacterWidths() + @invalidateMeasurements() setLineHeight: (lineHeight) -> @getTopmostDOMNode().style.lineHeight = lineHeight @sampleFontStyling() - @invalidateCharacterWidths() + @invalidateMeasurements() - invalidateCharacterWidths: -> + invalidateMeasurements: -> @linesYardstick.invalidateCache() - @presenter.characterWidthsChanged() + @presenter.measurementsChanged() setShowIndentGuide: (showIndentGuide) -> @config.set("editor.showIndentGuide", showIndentGuide) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 78bc67a08..5b1f01b35 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -1152,9 +1152,12 @@ class TextEditorPresenter @koreanCharWidth = koreanCharWidth @model.setDefaultCharWidth(baseCharacterWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth) @restoreScrollLeftIfNeeded() - @characterWidthsChanged() + @measurementsChanged() - characterWidthsChanged: -> + measurementsChanged: -> + @blockDecorationsPresenter.measurementsChanged() + + @shouldUpdateHeightState = true @shouldUpdateHorizontalScrollState = true @shouldUpdateVerticalScrollState = true @shouldUpdateScrollbarsState = true @@ -1162,8 +1165,10 @@ class TextEditorPresenter @shouldUpdateContentState = true @shouldUpdateDecorations = true @shouldUpdateLinesState = true + @shouldUpdateLineNumbersState = true @shouldUpdateCursorsState = true @shouldUpdateOverlaysState = true + @shouldUpdateCustomGutterDecorationState = true @emitDidUpdateState() From 46d6e3b3c456ef0c98c3866be9805c55c5f9e7d9 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 3 Dec 2015 11:29:34 -0500 Subject: [PATCH 180/502] Added .hasBranch --- spec/git-repository-async-spec.js | 17 +++++++++++++++++ src/git-repository-async.js | 12 ++++++++---- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 6aa2e6f81..ba4559225 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -453,4 +453,21 @@ describe('GitRepositoryAsync', () => { expect(deleted).toBe(0) }) }) + + describe('.hasBranch(branch)', () => { + beforeEach(() => { + const workingDirectory = copyRepository() + repo = GitRepositoryAsync.open(workingDirectory) + }) + + it('resolves true when the branch exists', async () => { + const hasBranch = await repo.hasBranch('master') + expect(hasBranch).toBe(true) + }) + + it("resolves false when the branch doesn't exist", async () => { + const hasBranch = await repo.hasBranch('trolleybus') + expect(hasBranch).toBe(false) + }) + }) }) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 2c86c129d..c6d85943a 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -96,7 +96,7 @@ export default class GitRepositoryAsync { // Public: Returns a {Promise} which resolves to the {String} working // directory path of the repository. getWorkingDirectory () { - throw new Error('Unimplemented') + return this.repoPromise.then(repo => repo.workdir()) } // Public: Returns a {Promise} that resolves to true if at the root, false if @@ -104,7 +104,7 @@ export default class GitRepositoryAsync { isProjectAtRoot () { if (!this.projectAtRoot && this.project) { this.projectAtRoot = Promise.resolve(() => { - return this.repoPromise.then(repo => this.project.relativize(repo.workdir)) + return this.repoPromise.then(repo => this.project.relativize(repo.workdir())) }) } @@ -154,9 +154,13 @@ export default class GitRepositoryAsync { return _path } - // Public: Returns true if the given branch exists. + // Public: Returns a {Promise} which resolves to whether the given branch + // exists. hasBranch (branch) { - throw new Error('Unimplemented') + return this.repoPromise + .then(repo => repo.getBranch(branch)) + .then(branch => branch != null) + .catch(_ => false) } // Public: Retrieves a shortened version of the HEAD reference value. From b60056f960c6f33b1341d41eb0eeea5ea7aff441 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 3 Dec 2015 11:34:05 -0500 Subject: [PATCH 181/502] Added .getUpstreamBranch --- src/git-repository-async.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index c6d85943a..8bdb8ca22 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -251,9 +251,12 @@ export default class GitRepositoryAsync { // * `path` An optional {String} path in the repo to get this information for, // only needed if the repository contains submodules. // - // Returns a {String} branch name such as `refs/remotes/origin/master`. + // Returns a {Promise} which resolves to a {String} branch name such as + // `refs/remotes/origin/master`. getUpstreamBranch (_path) { - throw new Error('Unimplemented') + return this._getRepo(_path) + .then(repo => repo.getCurrentBranch()) + .then(branch => Git.Branch.upstream(branch)) } // Public: Gets all the local and remote references. From c1e511927ba97d0c2d283223a073fdd0918e9ede Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 3 Dec 2015 11:44:31 -0500 Subject: [PATCH 182/502] Added .getReferences --- spec/git-repository-async-spec.js | 15 +++++++++++++++ src/git-repository-async.js | 20 ++++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index ba4559225..27306d9d9 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -470,4 +470,19 @@ describe('GitRepositoryAsync', () => { expect(hasBranch).toBe(false) }) }) + + describe('.getReferences(path)', () => { + let workingDirectory + beforeEach(() => { + workingDirectory = copyRepository() + repo = GitRepositoryAsync.open(workingDirectory) + }) + + it('returns the heads, remotes, and tags', async () => { + const {heads, remotes, tags} = await repo.getReferences() + expect(heads.length).toBe(1) + expect(remotes.length).toBe(0) + expect(tags.length).toBe(0) + }) + }) }) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 8bdb8ca22..0dcc5b786 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -264,12 +264,28 @@ export default class GitRepositoryAsync { // * `path` An optional {String} path in the repository to get this information // for, only needed if the repository has submodules. // - // Returns an {Object} with the following keys: + // Returns a {Promise} which resolves to an {Object} with the following keys: // * `heads` An {Array} of head reference names. // * `remotes` An {Array} of remote reference names. // * `tags` An {Array} of tag reference names. getReferences (_path) { - throw new Error('Unimplemented') + return this._getRepo(_path) + .then(repo => repo.getReferences(Git.Reference.TYPE.LISTALL)) + .then(refs => { + const heads = [] + const remotes = [] + const tags = [] + for (const ref of refs) { + if (ref.isTag()) { + tags.push(ref.name()) + } else if (ref.isRemote()) { + remotes.push(ref.name()) + } else if (ref.isBranch()) { + heads.push(ref.name()) + } + } + return {heads, remotes, tags} + }) } // Public: Returns the current {String} SHA for the given reference. From 70601772d085aa90222ff874e226335c8a6889ca Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 3 Dec 2015 11:54:55 -0500 Subject: [PATCH 183/502] Added .getReferenceTarget --- spec/git-repository-async-spec.js | 13 +++++++++++++ src/git-repository-async.js | 9 +++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 27306d9d9..8d0c87bd6 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -485,4 +485,17 @@ describe('GitRepositoryAsync', () => { expect(tags.length).toBe(0) }) }) + + describe('.getReferenceTarget(reference, path)', () => { + let workingDirectory + beforeEach(() => { + workingDirectory = copyRepository() + repo = GitRepositoryAsync.open(workingDirectory) + }) + + it('returns the SHA target', async () => { + const SHA = await repo.getReferenceTarget('refs/heads/master') + expect(SHA).toBe('8a9c86f1cb1f14b8f436eb91f4b052c8802ca99e') + }) + }) }) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 0dcc5b786..1fd90362d 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -288,13 +288,18 @@ export default class GitRepositoryAsync { }) } - // Public: Returns the current {String} SHA for the given reference. + // Public: Get the SHA for the given reference. // // * `reference` The {String} reference to get the target of. // * `path` An optional {String} path in the repo to get the reference target // for. Only needed if the repository contains submodules. + // + // Returns a {Promise} which resolves to the current {String} SHA for the + // given reference. getReferenceTarget (reference, _path) { - throw new Error('Unimplemented') + return this._getRepo(_path) + .then(repo => repo.getReference(reference)) + .then(ref => ref.target().tostrS()) } // Reading Status From f2a02215199d9c41b6a10a45f228775d7957948c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 3 Dec 2015 17:42:21 +0100 Subject: [PATCH 184/502] Make sure block decorations are always in the right spot --- spec/text-editor-component-spec.js | 2 +- spec/text-editor-presenter-spec.coffee | 39 ++++++++++++++++++++++---- src/lines-tile-component.coffee | 4 +-- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index d214201fa..2c090cd4d 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -1663,7 +1663,7 @@ describe('TextEditorComponent', function () { atom.themes.removeStylesheet('test') }) - it("renders visible and yet-to-be-measured block decorations, inserting them in the appropriate spots between lines and refreshing them when needed", async function () { + it("renders visible and yet-to-be-measured block decorations, inserting them between the appropriate lines and refreshing them as needed", async function () { wrapperNode.style.height = 9 * lineHeightInPixels + 'px' component.measureDimensions() await nextViewUpdatePromise() diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index fa2f1ea4f..a07c3446b 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -1320,11 +1320,11 @@ describe "TextEditorPresenter", -> expect(lineStateForScreenRow(presenter, 12).blockDecorations).toEqual([]) waitsForStateToUpdate presenter, -> - blockDecoration1.destroy() blockDecoration3.destroy() + blockDecoration1.getMarker().setHeadBufferPosition([0, 0]) runs -> - expect(lineStateForScreenRow(presenter, 0).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 0).blockDecorations).toEqual([blockDecoration1]) expect(lineStateForScreenRow(presenter, 1).blockDecorations).toEqual([]) expect(lineStateForScreenRow(presenter, 2).blockDecorations).toEqual([]) expect(lineStateForScreenRow(presenter, 3).blockDecorations).toEqual([]) @@ -1338,6 +1338,25 @@ describe "TextEditorPresenter", -> expect(lineStateForScreenRow(presenter, 11).blockDecorations).toEqual([]) expect(lineStateForScreenRow(presenter, 12).blockDecorations).toEqual([]) + waitsForStateToUpdate presenter, -> + editor.setCursorBufferPosition([0, 0]) + editor.insertNewline() + + runs -> + expect(lineStateForScreenRow(presenter, 0).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 1).blockDecorations).toEqual([blockDecoration1]) + expect(lineStateForScreenRow(presenter, 2).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 3).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 4).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 5).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 6).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 7).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 8).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 9).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 10).blockDecorations).toEqual([blockDecoration2]) + expect(lineStateForScreenRow(presenter, 11).blockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 12).blockDecorations).toEqual([]) + describe ".decorationClasses", -> it "adds decoration classes to the relevant line state objects, both initially and when decorations change", -> marker1 = editor.addMarkerLayer(maintainHistory: true).markBufferRange([[4, 0], [6, 2]], invalidate: 'touch') @@ -1551,10 +1570,11 @@ describe "TextEditorPresenter", -> blockDecoration1 = editor.addBlockDecorationForScreenRow(0) blockDecoration2 = editor.addBlockDecorationForScreenRow(1) - presenter.setBlockDecorationDimensions(blockDecoration1, 0, 30) - presenter.setBlockDecorationDimensions(blockDecoration2, 0, 10) - waitsForStateToUpdate presenter + waitsForStateToUpdate presenter, -> + presenter.setBlockDecorationDimensions(blockDecoration1, 0, 30) + presenter.setBlockDecorationDimensions(blockDecoration2, 0, 10) + runs -> expect(stateForCursor(presenter, 0)).toEqual {top: 50, left: 2 * 10, width: 10, height: 10} expect(stateForCursor(presenter, 1)).toEqual {top: 60, left: 4 * 10, width: 10, height: 10} @@ -1562,6 +1582,15 @@ describe "TextEditorPresenter", -> expect(stateForCursor(presenter, 3)).toBeUndefined() expect(stateForCursor(presenter, 4)).toBeUndefined() + waitsForStateToUpdate presenter, -> + blockDecoration2.destroy() + editor.setCursorBufferPosition([0, 0]) + editor.insertNewline() + editor.setCursorBufferPosition([0, 0]) + + runs -> + expect(stateForCursor(presenter, 0)).toEqual {top: 0, left: 0, width: 10, height: 10} + it "updates when ::scrollTop changes", -> editor.setSelectedBufferRanges([ [[1, 2], [1, 2]], diff --git a/src/lines-tile-component.coffee b/src/lines-tile-component.coffee index 73e88aa32..24c05dc2e 100644 --- a/src/lines-tile-component.coffee +++ b/src/lines-tile-component.coffee @@ -127,14 +127,14 @@ class LinesTileComponent delete @insertionPointsByLineId[id] insertBlockDecorationInsertionPoint: (id) -> - {hasBlockDecorations} = @newTileState.lines[id] + {hasBlockDecorations, screenRow} = @newTileState.lines[id] if hasBlockDecorations lineNode = @lineNodesByLineId[id] insertionPoint = @domElementPool.buildElement("content") @domNode.insertBefore(insertionPoint, lineNode) @insertionPointsByLineId[id] = insertionPoint - + insertionPoint.dataset.screenRow = screenRow @updateBlockDecorationInsertionPoint(id) updateBlockDecorationInsertionPoint: (id) -> From 74a0528bef18fdd4d9013012cb59db891009b400 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 3 Dec 2015 12:18:00 -0500 Subject: [PATCH 185/502] Added .getConfigValue --- spec/git-repository-async-spec.js | 24 ++++++++++++++++++++---- src/git-repository-async.js | 8 +++++++- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 8d0c87bd6..f226743b2 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -472,9 +472,8 @@ describe('GitRepositoryAsync', () => { }) describe('.getReferences(path)', () => { - let workingDirectory beforeEach(() => { - workingDirectory = copyRepository() + const workingDirectory = copyRepository() repo = GitRepositoryAsync.open(workingDirectory) }) @@ -487,9 +486,8 @@ describe('GitRepositoryAsync', () => { }) describe('.getReferenceTarget(reference, path)', () => { - let workingDirectory beforeEach(() => { - workingDirectory = copyRepository() + const workingDirectory = copyRepository() repo = GitRepositoryAsync.open(workingDirectory) }) @@ -498,4 +496,22 @@ describe('GitRepositoryAsync', () => { expect(SHA).toBe('8a9c86f1cb1f14b8f436eb91f4b052c8802ca99e') }) }) + + describe('.getConfigValue(key, path)', () => { + beforeEach(() => { + const workingDirectory = copyRepository() + console.log(workingDirectory) + repo = GitRepositoryAsync.open(workingDirectory) + }) + + it('looks up the value for the key', async () => { + const bare = await repo.getConfigValue('core.bare') + expect(bare).toBe('false') + }) + + it("resolves to null if there's no value", async () => { + const value = await repo.getConfigValue('my.special.key') + expect(value).toBe(null) + }) + }) }) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 1fd90362d..432502d17 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -233,8 +233,14 @@ export default class GitRepositoryAsync { // // * `path` An optional {String} path in the repository to get this information // for, only needed if the repository has submodules. + // + // Returns a {Promise} which resolves to the {String} git configuration value + // specified by the key. getConfigValue (key, _path) { - throw new Error('Unimplemented') + return this._getRepo(_path) + .then(repo => repo.configSnapshot()) + .then(config => config.getStringBuf(key)) + .catch(_ => null) } // Public: Returns the origin url of the repository. From df839537a95264c0354669fae6966e2e9298b08f Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 3 Dec 2015 12:26:32 -0500 Subject: [PATCH 186/502] A lil more efficient .getReferenceTarget. --- src/git-repository-async.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 432502d17..e893bb9e5 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -304,8 +304,8 @@ export default class GitRepositoryAsync { // given reference. getReferenceTarget (reference, _path) { return this._getRepo(_path) - .then(repo => repo.getReference(reference)) - .then(ref => ref.target().tostrS()) + .then(repo => Git.Reference.nameToId(repo, reference)) + .then(oid => oid.tostrS()) } // Reading Status From d21a91a412ab24178c27eeff5eb234b897bc6bb8 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 3 Dec 2015 12:26:45 -0500 Subject: [PATCH 187/502] Added .getOriginalURL. --- src/git-repository-async.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index e893bb9e5..f65156856 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -243,12 +243,15 @@ export default class GitRepositoryAsync { .catch(_ => null) } - // Public: Returns the origin url of the repository. + // Public: Get the URL for the 'origin' remote. // // * `path` (optional) {String} path in the repository to get this information // for, only needed if the repository has submodules. + // + // Returns a {Promise} which resolves to the {String} origin url of the + // repository. getOriginURL (_path) { - throw new Error('Unimplemented') + return this.getConfigValue('remote.origin.url', _path) } // Public: Returns the upstream branch for the current HEAD, or null if there From c3adee6363f9dc6d17b9c5b70eb3080b57358d94 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 3 Dec 2015 12:41:22 -0500 Subject: [PATCH 188/502] Add some more stubs/TODOs. --- src/git-repository-async.js | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index f65156856..d1cd90cc4 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -189,6 +189,8 @@ export default class GitRepositoryAsync { return this.repoPromise .then(repo => repo.openIndex()) .then(index => { + // TODO: This'll probably be wrong if the submodule doesn't exist in the + // index yet? Is that a thing? const entry = index.getByPath(_path) const submoduleMode = 57344 // TODO compose this from libgit2 constants return entry.mode === submoduleMode @@ -469,6 +471,21 @@ export default class GitRepositoryAsync { }) } + // Public: Retrieves the line diffs comparing the `HEAD` version of the given + // path and the given text. + // + // * `path` The {String} path relative to the repository. + // * `text` The {String} to compare against the `HEAD` contents + // + // Returns an {Array} of hunk {Object}s with the following keys: + // * `oldStart` The line {Number} of the old hunk. + // * `newStart` The line {Number} of the new hunk. + // * `oldLines` The {Number} of lines in the old hunk. + // * `newLines` The {Number} of lines in the new hunk + getLineDiffs (_path, text) { + throw new Error('Unimplemented') + } + // Checking Out // ============ @@ -497,6 +514,21 @@ export default class GitRepositoryAsync { .then(() => this.refreshStatusForPath(_path)) } + // Public: Checks out a branch in your repository. + // + // * `reference` The {String} reference to checkout. + // * `create` A {Boolean} value which, if true creates the new reference if + // it doesn't exist. + // + // Returns a Boolean that's true if the method was successful. + checkoutReference (reference, create) { + throw new Error('Unimplemented') + // @getRepo().checkoutReference(reference, create) + } + + // Private + // ======= + checkoutHeadForEditor (editor) { return new Promise((resolve, reject) => { const filePath = editor.getPath() @@ -511,9 +543,6 @@ export default class GitRepositoryAsync { }).then(filePath => this.checkoutHead(filePath)) } - // Private - // ======= - // Get the current branch and update this.branch. // // Returns :: Promise From 53be0cf9655d5e948941a3f2df4079092d0b7f76 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 3 Dec 2015 14:07:12 -0500 Subject: [PATCH 189/502] Added .checkoutReference --- spec/git-repository-async-spec.js | 26 +++++++++++++++++++++++--- src/git-repository-async.js | 26 +++++++++++++++++++++++--- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index f226743b2..23acedff0 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -462,12 +462,12 @@ describe('GitRepositoryAsync', () => { it('resolves true when the branch exists', async () => { const hasBranch = await repo.hasBranch('master') - expect(hasBranch).toBe(true) + expect(hasBranch).toBeTruthy() }) it("resolves false when the branch doesn't exist", async () => { const hasBranch = await repo.hasBranch('trolleybus') - expect(hasBranch).toBe(false) + expect(hasBranch).toBeFalsy() }) }) @@ -511,7 +511,27 @@ describe('GitRepositoryAsync', () => { it("resolves to null if there's no value", async () => { const value = await repo.getConfigValue('my.special.key') - expect(value).toBe(null) + expect(value).toBeNull() + }) + }) + + describe('.checkoutReference(reference, create)', () => { + beforeEach(() => { + const workingDirectory = copyRepository() + console.log(workingDirectory) + repo = GitRepositoryAsync.open(workingDirectory) + }) + + it('can create new branches', () => { + let success = false + let threw = false + waitsForPromise(() => repo.checkoutReference('my-b', true) + .then(_ => success = true) + .catch(_ => threw = true)) + runs(() => { + expect(success).toBeTruthy() + expect(threw).toBeFalsy() + }) }) }) }) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index d1cd90cc4..275feb4c8 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -483,6 +483,11 @@ export default class GitRepositoryAsync { // * `oldLines` The {Number} of lines in the old hunk. // * `newLines` The {Number} of lines in the new hunk getLineDiffs (_path, text) { + // # Ignore eol of line differences on windows so that files checked in as + // # LF don't report every line modified when the text contains CRLF endings. + // options = ignoreEolWhitespace: process.platform is 'win32' + // repo = @getRepo(path) + // repo.getLineDiffs(repo.relativize(path), text, options) throw new Error('Unimplemented') } @@ -514,16 +519,31 @@ export default class GitRepositoryAsync { .then(() => this.refreshStatusForPath(_path)) } + _createBranch (name) { + return this.repoPromise + .then(repo => Promise.all([repo, repo.getHeadCommit()])) + .then(([repo, commit]) => repo.createBranch(name, commit)) + } + // Public: Checks out a branch in your repository. // // * `reference` The {String} reference to checkout. // * `create` A {Boolean} value which, if true creates the new reference if // it doesn't exist. // - // Returns a Boolean that's true if the method was successful. + // Returns a {Promise} that resolves if the method was successful. checkoutReference (reference, create) { - throw new Error('Unimplemented') - // @getRepo().checkoutReference(reference, create) + return this.repoPromise + .then(repo => repo.checkoutBranch(reference)) + .catch(error => { + if (create) { + return this._createBranch(reference) + .then(_ => this.checkoutReference(reference, false)) + } else { + throw error + } + }) + .then(_ => null) } // Private From 4fbbcac3bc5d524426b1dbb32fe7a0b3f3259eea Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 3 Dec 2015 15:34:58 -0500 Subject: [PATCH 190/502] Added .getLineDiffs --- spec/git-repository-async-spec.js | 17 +++++++-- src/git-repository-async.js | 60 +++++++++++++++++++++++++------ 2 files changed, 64 insertions(+), 13 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 23acedff0..9aca3356c 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -500,7 +500,6 @@ describe('GitRepositoryAsync', () => { describe('.getConfigValue(key, path)', () => { beforeEach(() => { const workingDirectory = copyRepository() - console.log(workingDirectory) repo = GitRepositoryAsync.open(workingDirectory) }) @@ -518,7 +517,6 @@ describe('GitRepositoryAsync', () => { describe('.checkoutReference(reference, create)', () => { beforeEach(() => { const workingDirectory = copyRepository() - console.log(workingDirectory) repo = GitRepositoryAsync.open(workingDirectory) }) @@ -534,4 +532,19 @@ describe('GitRepositoryAsync', () => { }) }) }) + + describe('.getLineDiffs(path, text)', () => { + beforeEach(() => { + const workingDirectory = copyRepository() + repo = GitRepositoryAsync.open(workingDirectory) + }) + + it('can get the line diff', async () => { + const {oldStart, newStart, oldLines, newLines} = await repo.getLineDiffs('a.txt', 'hi there') + expect(oldStart).toBe(0) + expect(oldLines).toBe(0) + expect(newStart).toBe(1) + expect(newLines).toBe(1) + }) + }) }) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 275feb4c8..f0c394a02 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -433,6 +433,18 @@ export default class GitRepositoryAsync { return (statusBit & deletedStatusFlags) > 0 } + _getDiffHunks (diff) { + return diff.patches() + .then(patches => Promise.all(patches.map(p => p.hunks()))) // patches :: Array + .then(hunks => _.flatten(hunks)) // hunks :: Array> + } + + _getDiffLines (diff) { + return this._getDiffHunks(diff) + .then(hunks => Promise.all(hunks.map(h => h.lines()))) + .then(lines => _.flatten(lines)) // lines :: Array> + } + // Retrieving Diffs // ================ // Public: Retrieves the number of lines added and removed to a path. @@ -454,12 +466,10 @@ export default class GitRepositoryAsync { options.pathspec = _path return Git.Diff.treeToWorkdir(repo, tree, options) }) - .then(diff => diff.patches()) - .then(patches => Promise.all(patches.map(p => p.hunks()))) // patches :: Array - .then(hunks => Promise.all(_.flatten(hunks).map(h => h.lines()))) // hunks :: Array> - .then(lines => { // lines :: Array> + .then(diff => this._getDiffLines(diff)) + .then(lines => { const stats = {added: 0, deleted: 0} - for (const line of _.flatten(lines)) { + for (const line of lines) { const origin = line.origin() if (origin === Git.Diff.LINE.ADDITION) { stats.added++ @@ -471,6 +481,14 @@ export default class GitRepositoryAsync { }) } + _diffBlobToBuffer (blob, buffer, options) { + const hunks = [] + const hunkCallback = (delta, hunk, payload) => { + hunks.push(hunk) + } + return Git.Diff.blobToBuffer(blob, null, buffer, null, null, options, null, null, hunkCallback, null, null).then(_ => hunks) + } + // Public: Retrieves the line diffs comparing the `HEAD` version of the given // path and the given text. // @@ -483,12 +501,32 @@ export default class GitRepositoryAsync { // * `oldLines` The {Number} of lines in the old hunk. // * `newLines` The {Number} of lines in the new hunk getLineDiffs (_path, text) { - // # Ignore eol of line differences on windows so that files checked in as - // # LF don't report every line modified when the text contains CRLF endings. - // options = ignoreEolWhitespace: process.platform is 'win32' - // repo = @getRepo(path) - // repo.getLineDiffs(repo.relativize(path), text, options) - throw new Error('Unimplemented') + return this.repoPromise + .then(repo => repo.getHeadCommit()) + .then(commit => commit.getEntry(_path)) + .then(entry => entry.getBlob()) + .then(blob => { + const options = new Git.DiffOptions() + if (process.platform === 'win32') { + // Ignore eol of line differences on windows so that files checked in + // as LF don't report every line modified when the text contains CRLF + // endings. + // TODO: set GIT_DIFF_IGNORE_WHITESPACE_EOL + } + return this._diffBlobToBuffer(blob, text, options) + }) + .then(hunks => { + // TODO: The old implementation just takes into account the last hunk. + // That's probably safe for most cases but maybe wrong in some edge + // cases? + const hunk = hunks[hunks.length - 1] + return { + oldStart: hunk.oldStart(), + newStart: hunk.newStart(), + oldLines: hunk.oldLines(), + newLines: hunk.newLines() + } + }) } // Checking Out From cea6926cf26647d5bad7dccadffc0769d2692e82 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 3 Dec 2015 15:46:36 -0500 Subject: [PATCH 191/502] Set IGNORE_WHITESPACE_EOL on Windows. --- src/git-repository-async.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index f0c394a02..116f52216 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -511,7 +511,7 @@ export default class GitRepositoryAsync { // Ignore eol of line differences on windows so that files checked in // as LF don't report every line modified when the text contains CRLF // endings. - // TODO: set GIT_DIFF_IGNORE_WHITESPACE_EOL + options.flags = Git.Diff.OPTION.IGNORE_WHITESPACE_EOL } return this._diffBlobToBuffer(blob, text, options) }) From 16525047f1f9f46a14c6a4197619fd916d6d6f7a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 4 Dec 2015 12:49:59 +0100 Subject: [PATCH 192/502] :green_heart: Fix component and presenter specs --- spec/text-editor-component-spec.js | 23 +++++++++++------------ spec/text-editor-presenter-spec.coffee | 12 ++++++------ 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 2c090cd4d..a645493e4 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -1673,13 +1673,13 @@ describe('TextEditorComponent', function () { let [item3, blockDecoration3] = createBlockDecorationForScreenRowWith(4, {className: "decoration-3"}) let [item4, blockDecoration4] = createBlockDecorationForScreenRowWith(7, {className: "decoration-4"}) - atom.styles.addStyleSheet ` - atom-text-editor .decoration-1 { width: 30px; height: 80px; } - atom-text-editor .decoration-2 { width: 30px; height: 40px; } - atom-text-editor .decoration-3 { width: 30px; height: 100px; } - atom-text-editor .decoration-4 { width: 30px; height: 120px; } - ` - + atom.styles.addStyleSheet( + `atom-text-editor .decoration-1 { width: 30px; height: 80px; } + atom-text-editor .decoration-2 { width: 30px; height: 40px; } + atom-text-editor .decoration-3 { width: 30px; height: 100px; } + atom-text-editor .decoration-4 { width: 30px; height: 120px; }`, + {context: 'atom-text-editor'} + ) await nextAnimationFramePromise() expect(component.getDomNode().querySelectorAll(".line").length).toBe(7) @@ -1723,11 +1723,10 @@ describe('TextEditorComponent', function () { expect(item2.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 3) expect(item3.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 5 + 40) - await nextAnimationFramePromise() - - atom.styles.addStyleSheet ` - atom-text-editor .decoration-2 { height: 60px !important; } - ` + atom.styles.addStyleSheet( + `atom-text-editor .decoration-2 { height: 60px !important; }`, + {context: 'atom-text-editor'} + ) await nextAnimationFramePromise() // causes the DOM to update and to retrieve new styles await nextAnimationFramePromise() // applies the changes diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index a07c3446b..885640e5b 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -403,7 +403,7 @@ describe "TextEditorPresenter", -> expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe 10 * maxLineLength + 1 expectStateUpdate presenter, -> presenter.getLinesYardstick().setScopedCharacterWidth(['source.js', 'support.function.js'], 'p', 20) - presenter.characterWidthsChanged() + presenter.measurementsChanged() expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe (10 * (maxLineLength - 2)) + (20 * 2) + 1 # 2 of the characters are 20px wide now instead of 10px wide it "updates when ::softWrapped changes on the editor", -> @@ -676,7 +676,7 @@ describe "TextEditorPresenter", -> expectStateUpdate presenter, -> presenter.getLinesYardstick().setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'r', 20) - presenter.characterWidthsChanged() + presenter.measurementsChanged() expect(presenter.getState().hiddenInput.width).toBe 20 it "is 2px at the end of lines", -> @@ -818,7 +818,7 @@ describe "TextEditorPresenter", -> expect(presenter.getState().content.scrollWidth).toBe 10 * maxLineLength + 1 expectStateUpdate presenter, -> presenter.getLinesYardstick().setScopedCharacterWidth(['source.js', 'support.function.js'], 'p', 20) - presenter.characterWidthsChanged() + presenter.measurementsChanged() expect(presenter.getState().content.scrollWidth).toBe (10 * (maxLineLength - 2)) + (20 * 2) + 1 # 2 of the characters are 20px wide now instead of 10px wide it "updates when ::softWrapped changes on the editor", -> @@ -1665,12 +1665,12 @@ describe "TextEditorPresenter", -> expectStateUpdate presenter, -> presenter.getLinesYardstick().setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'v', 20) - presenter.characterWidthsChanged() + presenter.measurementsChanged() expect(stateForCursor(presenter, 0)).toEqual {top: 1 * 10, left: (3 * 10) + 20, width: 10, height: 10} expectStateUpdate presenter, -> presenter.getLinesYardstick().setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'r', 20) - presenter.characterWidthsChanged() + presenter.measurementsChanged() expect(stateForCursor(presenter, 0)).toEqual {top: 1 * 10, left: (3 * 10) + 20, width: 20, height: 10} it "updates when cursors are added, moved, hidden, shown, or destroyed", -> @@ -2016,7 +2016,7 @@ describe "TextEditorPresenter", -> } expectStateUpdate presenter, -> presenter.getLinesYardstick().setScopedCharacterWidth(['source.js', 'keyword.control.js'], 'i', 20) - presenter.characterWidthsChanged() + presenter.measurementsChanged() expectValues stateForSelectionInTile(presenter, 0, 2), { regions: [{top: 0, left: 4 * 10, width: 20 + 10, height: 10}] } From 14d8ecefdde4d9c2dacb3cfdd9efa13751b24637 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 4 Dec 2015 12:54:20 +0100 Subject: [PATCH 193/502] :green_heart: Fix LinesYardstick specs --- spec/lines-yardstick-spec.coffee | 5 ++++- src/lines-yardstick.coffee | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/spec/lines-yardstick-spec.coffee b/spec/lines-yardstick-spec.coffee index fbf5c53bf..44267699e 100644 --- a/spec/lines-yardstick-spec.coffee +++ b/spec/lines-yardstick-spec.coffee @@ -57,7 +57,10 @@ describe "LinesYardstick", -> textNodes editor.setLineHeightInPixels(14) - linesYardstick = new LinesYardstick(editor, mockPresenter, mockLineNodesProvider, new LineTopIndex(), atom.grammars) + lineTopIndex = new LineTopIndex() + lineTopIndex.setDefaultLineHeight(editor.getLineHeightInPixels()) + lineTopIndex.setMaxRow(editor.getScreenLineCount()) + linesYardstick = new LinesYardstick(editor, mockPresenter, mockLineNodesProvider, lineTopIndex, atom.grammars) afterEach -> lineNode.remove() for lineNode in createdLineNodes diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index c1faa6399..847ebfd46 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -25,6 +25,7 @@ class LinesYardstick row = @lineTopIndex.rowForTopPixelPosition(targetTop, 'floor') targetLeft = 0 if targetTop < 0 targetLeft = Infinity if row > @model.getLastScreenRow() + row = Math.min(row, @model.getLastScreenRow()) @prepareScreenRowsForMeasurement([row]) unless measureVisibleLinesOnly From 7554f71f7451f6e2143ce6b1d61eaefb5d435717 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 4 Dec 2015 14:21:23 +0100 Subject: [PATCH 194/502] Make sure screen row is set correctly on --- spec/text-editor-component-spec.js | 52 +++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index a645493e4..1ae914d35 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -1724,7 +1724,7 @@ describe('TextEditorComponent', function () { expect(item3.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 5 + 40) atom.styles.addStyleSheet( - `atom-text-editor .decoration-2 { height: 60px !important; }`, + 'atom-text-editor .decoration-2 { height: 60px !important; }', {context: 'atom-text-editor'} ) @@ -1748,6 +1748,56 @@ describe('TextEditorComponent', function () { expect(item2.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 3) expect(item3.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 5 + 60) }) + + it("correctly sets screen rows on elements, both initially and when decorations move", async function () { + wrapperNode.style.height = 9 * lineHeightInPixels + 'px' + component.measureDimensions() + await nextViewUpdatePromise() + + let [item, blockDecoration] = createBlockDecorationForScreenRowWith(0, {className: "decoration-1"}) + atom.styles.addStyleSheet( + 'atom-text-editor .decoration-1 { width: 30px; height: 80px; }', + {context: 'atom-text-editor'} + ) + + await nextAnimationFramePromise() + + let tileNode, contentElements + + tileNode = component.tileNodesForLines()[0] + contentElements = tileNode.querySelectorAll("content") + + expect(contentElements.length).toBe(1) + expect(contentElements[0].dataset.screenRow).toBe("0") + expect(component.lineNodeForScreenRow(0).dataset.screenRow).toBe("0") + expect(component.lineNodeForScreenRow(1).dataset.screenRow).toBe("1") + expect(component.lineNodeForScreenRow(2).dataset.screenRow).toBe("2") + + editor.setCursorBufferPosition([0, 0]) + editor.insertNewline() + await nextAnimationFramePromise() + + tileNode = component.tileNodesForLines()[0] + contentElements = tileNode.querySelectorAll("content") + + expect(contentElements.length).toBe(1) + expect(contentElements[0].dataset.screenRow).toBe("1") + expect(component.lineNodeForScreenRow(0).dataset.screenRow).toBe("0") + expect(component.lineNodeForScreenRow(1).dataset.screenRow).toBe("1") + expect(component.lineNodeForScreenRow(2).dataset.screenRow).toBe("2") + + blockDecoration.getMarker().setHeadBufferPosition([2, 0]) + await nextAnimationFramePromise() + + tileNode = component.tileNodesForLines()[0] + contentElements = tileNode.querySelectorAll("content") + + expect(contentElements.length).toBe(1) + expect(contentElements[0].dataset.screenRow).toBe("2") + expect(component.lineNodeForScreenRow(0).dataset.screenRow).toBe("0") + expect(component.lineNodeForScreenRow(1).dataset.screenRow).toBe("1") + expect(component.lineNodeForScreenRow(2).dataset.screenRow).toBe("2") + }) }) describe('highlight decoration rendering', function () { From b6b2958e67055420e11a7dc09b327006cec7806a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 4 Dec 2015 15:33:18 +0100 Subject: [PATCH 195/502] :memo: TextEditor::addBlockDecorationForScreenRow --- src/text-editor.coffee | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 136756538..27fd75c22 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -1398,11 +1398,16 @@ class TextEditor extends Model Section: Decorations ### + # Experimental: Mark and add a block decoration to the specified screen row. + # + # * `screenRow` A {Number} representing the screen row where to add the block decoration. + # * `item` A {ViewRegistry::getView}-compatible object to render. + # + # Returns a {Decoration} object. addBlockDecorationForScreenRow: (screenRow, item) -> @decorateMarker( @markScreenPosition([screenRow, 0], invalidate: "never"), - type: "block", - item: item + type: "block", item: item ) # Essential: Add a decoration that tracks a {TextEditorMarker}. When the From cb4c27757a0914b31abad050bac6ba71ea6de4fa Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 4 Dec 2015 15:35:53 +0100 Subject: [PATCH 196/502] :memo: TextEditorElement::invalidateBlockDecorationDimensions --- src/text-editor-element.coffee | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index 6722f51df..380417163 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -347,6 +347,12 @@ class TextEditorElement extends HTMLElement getHeight: -> @offsetHeight + # Experimental: Invalidate the passed block {Decoration} dimensions, forcing + # them to be recalculated and the surrounding content to be adjusted on the + # next animation frame. + # + # * {blockDecoration} A {Decoration} representing the block decoration you + # want to update the dimensions of. invalidateBlockDecorationDimensions: -> @component.invalidateBlockDecorationDimensions(arguments...) From c578f221bf7be484d726f59c9f05b60edd337f8d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 4 Dec 2015 15:42:49 +0100 Subject: [PATCH 197/502] :white_check_mark: Test ::invalidateBlockDecorationDimensions --- spec/text-editor-component-spec.js | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index ab9583f31..86f924116 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -1724,7 +1724,7 @@ describe('TextEditorComponent', function () { expect(item3.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 5 + 40) atom.styles.addStyleSheet( - 'atom-text-editor .decoration-2 { height: 60px !important; }', + 'atom-text-editor .decoration-2 { height: 60px; }', {context: 'atom-text-editor'} ) @@ -1747,6 +1747,28 @@ describe('TextEditorComponent', function () { expect(item2.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 3) expect(item3.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 5 + 60) + + item2.style.height = "20px" + wrapperNode.invalidateBlockDecorationDimensions(blockDecoration2) + await nextAnimationFramePromise() + await nextAnimationFramePromise() + + expect(component.getDomNode().querySelectorAll(".line").length).toBe(7) + + expect(component.tileNodesForLines()[0].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + "px") + expect(component.tileNodesForLines()[0].style.webkitTransform).toBe("translate3d(0px, 0px, 0px)") + expect(component.tileNodesForLines()[1].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 100 + 20 + "px") + expect(component.tileNodesForLines()[1].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight}px, 0px)`) + expect(component.tileNodesForLines()[2].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 120 + "px") + expect(component.tileNodesForLines()[2].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight + component.tileNodesForLines()[1].offsetHeight}px, 0px)`) + + expect(component.getTopmostDOMNode().querySelector(".decoration-1")).toBeNull() + expect(component.getTopmostDOMNode().querySelector(".decoration-2")).toBe(item2) + expect(component.getTopmostDOMNode().querySelector(".decoration-3")).toBe(item3) + expect(component.getTopmostDOMNode().querySelector(".decoration-4")).toBeNull() + + expect(item2.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 3) + expect(item3.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 5 + 20) }) it("correctly sets screen rows on elements, both initially and when decorations move", async function () { From 0e3bb42b2cb830ab628453350391554c3f07a4e3 Mon Sep 17 00:00:00 2001 From: joshaber Date: Fri, 4 Dec 2015 12:01:51 -0500 Subject: [PATCH 198/502] Use some binaries, hopefully? --- .npmrc | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.npmrc b/.npmrc index c5ff09782..d71d2a4c4 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,4 @@ cache = ~/.atom/.npm +runtime = electron +target = 0.34.5 +disturl = https://atom.io/download/atom-shell diff --git a/package.json b/package.json index 77661a010..87873082a 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "key-path-helpers": "^0.4.0", "less-cache": "0.22", "marked": "^0.3.4", - "nodegit": "~0.5.0", + "nodegit": "0.0.10000002", "normalize-package-data": "^2.0.0", "nslog": "^3", "oniguruma": "^5", From 7691107e4ba468b14dc3a25d2d77405e752a9ea4 Mon Sep 17 00:00:00 2001 From: joshaber Date: Fri, 4 Dec 2015 12:27:52 -0500 Subject: [PATCH 199/502] Added license override for inherit. --- build/tasks/license-overrides.coffee | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/build/tasks/license-overrides.coffee b/build/tasks/license-overrides.coffee index 19f32fb84..aa136eb37 100644 --- a/build/tasks/license-overrides.coffee +++ b/build/tasks/license-overrides.coffee @@ -328,3 +328,16 @@ module.exports = modification. This license may not be modified without the express written permission of its copyright owner. """ + 'inherit@2.2.2': + license: 'MIT' + repository: 'https://github.com/dfilatov/inherit' + source: 'LICENSE.md' + sourceText: """ + Copyright (c) 2012 Dmitry Filatov + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + """ From f5f2cd372d4c872b39db3a792809292d44822b16 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 7 Dec 2015 11:09:04 -0500 Subject: [PATCH 200/502] Use nodegit-atom for now. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 87873082a..2ccf080dd 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "key-path-helpers": "^0.4.0", "less-cache": "0.22", "marked": "^0.3.4", - "nodegit": "0.0.10000002", + "nodegit-atom": "0.0.1", "normalize-package-data": "^2.0.0", "nslog": "^3", "oniguruma": "^5", From 09bd603c4b0bafd3113061fee041525af09d22c5 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 7 Dec 2015 11:10:20 -0500 Subject: [PATCH 201/502] Don't set these here. --- .npmrc | 3 --- 1 file changed, 3 deletions(-) diff --git a/.npmrc b/.npmrc index d71d2a4c4..c5ff09782 100644 --- a/.npmrc +++ b/.npmrc @@ -1,4 +1 @@ cache = ~/.atom/.npm -runtime = electron -target = 0.34.5 -disturl = https://atom.io/download/atom-shell From 8514cb18cdc34769552b13ac12b57a18c757e7ea Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 7 Dec 2015 11:10:28 -0500 Subject: [PATCH 202/502] Do the needful to get Electron binaries. --- script/cibuild | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/script/cibuild b/script/cibuild index 860e0a938..5f76a6eee 100755 --- a/script/cibuild +++ b/script/cibuild @@ -45,6 +45,13 @@ function setEnvironmentVariables() { process.env.BUILD_ATOM_RELEASES_S3_SECRET = process.env.BUILD_ATOM_WIN_RELEASES_S3_SECRET process.env.BUILD_ATOM_RELEASES_S3_BUCKET = process.env.BUILD_ATOM_WIN_RELEASES_S3_BUCKET } + + // `node-pre-gyp` will look for these when determining which binary to + // download or how to rebuild. + process.env.npm_config_runtime = 'electron'; + // TODO: get this from package.json + process.env.npm_config_target = '0.34.5'; + process.env.npm_config_disturl = 'https://atom.io/download/atom-shell'; } function removeNodeModules() { From 752b266712b51667bb1f1c26829f32f575f5c154 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 7 Dec 2015 11:19:47 -0500 Subject: [PATCH 203/502] Use the same version as The Real Nodegit. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2ccf080dd..6b15afb0d 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "key-path-helpers": "^0.4.0", "less-cache": "0.22", "marked": "^0.3.4", - "nodegit-atom": "0.0.1", + "nodegit-atom": "0.5.0", "normalize-package-data": "^2.0.0", "nslog": "^3", "oniguruma": "^5", From ed357b1bb67f6b50d95f46dc2ffbe2b51c5d757e Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 7 Dec 2015 11:52:28 -0500 Subject: [PATCH 204/502] ++nodegit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6b15afb0d..6aa81afbd 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "key-path-helpers": "^0.4.0", "less-cache": "0.22", "marked": "^0.3.4", - "nodegit-atom": "0.5.0", + "nodegit-atom": "0.5.1", "normalize-package-data": "^2.0.0", "nslog": "^3", "oniguruma": "^5", From 7da15beb71eb21d0dfe7e3ee0ea0a6f44f3a1180 Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 9 Dec 2015 15:39:23 -0500 Subject: [PATCH 205/502] ++nodegit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6aa81afbd..d766d8dd9 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "key-path-helpers": "^0.4.0", "less-cache": "0.22", "marked": "^0.3.4", - "nodegit-atom": "0.5.1", + "nodegit": "0.6.0", "normalize-package-data": "^2.0.0", "nslog": "^3", "oniguruma": "^5", From 0baf653b99094f29c287a9bec72fd8ebfc5cf37a Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 9 Dec 2015 15:44:38 -0500 Subject: [PATCH 206/502] Provide these from .npmrc. --- .npmrc | 3 +++ script/cibuild | 7 ------- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/.npmrc b/.npmrc index c5ff09782..d71d2a4c4 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,4 @@ cache = ~/.atom/.npm +runtime = electron +target = 0.34.5 +disturl = https://atom.io/download/atom-shell diff --git a/script/cibuild b/script/cibuild index 5f76a6eee..860e0a938 100755 --- a/script/cibuild +++ b/script/cibuild @@ -45,13 +45,6 @@ function setEnvironmentVariables() { process.env.BUILD_ATOM_RELEASES_S3_SECRET = process.env.BUILD_ATOM_WIN_RELEASES_S3_SECRET process.env.BUILD_ATOM_RELEASES_S3_BUCKET = process.env.BUILD_ATOM_WIN_RELEASES_S3_BUCKET } - - // `node-pre-gyp` will look for these when determining which binary to - // download or how to rebuild. - process.env.npm_config_runtime = 'electron'; - // TODO: get this from package.json - process.env.npm_config_target = '0.34.5'; - process.env.npm_config_disturl = 'https://atom.io/download/atom-shell'; } function removeNodeModules() { From b810a8b491c22f7e09db6d960fab31a90e0d8876 Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 9 Dec 2015 17:04:41 -0500 Subject: [PATCH 207/502] Get the Electron version from the package. --- .npmrc | 3 --- script/bootstrap | 6 ++++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.npmrc b/.npmrc index d71d2a4c4..c5ff09782 100644 --- a/.npmrc +++ b/.npmrc @@ -1,4 +1 @@ cache = ~/.atom/.npm -runtime = electron -target = 0.34.5 -disturl = https://atom.io/download/atom-shell diff --git a/script/bootstrap b/script/bootstrap index 8314b9cb0..d62b84375 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -64,6 +64,12 @@ function bootstrap() { process.env.ATOM_RESOURCE_PATH = path.resolve(__dirname, '..'); + // `node-pre-gyp` will look for these when determining which binary to + // download or how to rebuild. + process.env.npm_config_target = require('../package.json').electronVersion; + process.env.npm_config_runtime = 'electron'; + process.env.npm_config_disturl = 'https://atom.io/download/atom-shell'; + var buildInstallCommand = initialNpmCommand + npmFlags + 'install'; var buildInstallOptions = {cwd: path.resolve(__dirname, '..', 'build')}; var apmInstallCommand = npmPath + npmFlags + '--target=0.10.35 ' + 'install'; From ab94103497803673b59fd28a4b296000cf5ed148 Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 9 Dec 2015 23:19:09 -0500 Subject: [PATCH 208/502] Back to putting them in the npmrc. --- .npmrc | 3 +++ script/bootstrap | 6 ------ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.npmrc b/.npmrc index c5ff09782..6fc4bd7ae 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,4 @@ cache = ~/.atom/.npm +target = 0.34.5 +runtime = electron +disturl = https://atom.io/download/atom-shell diff --git a/script/bootstrap b/script/bootstrap index d62b84375..8314b9cb0 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -64,12 +64,6 @@ function bootstrap() { process.env.ATOM_RESOURCE_PATH = path.resolve(__dirname, '..'); - // `node-pre-gyp` will look for these when determining which binary to - // download or how to rebuild. - process.env.npm_config_target = require('../package.json').electronVersion; - process.env.npm_config_runtime = 'electron'; - process.env.npm_config_disturl = 'https://atom.io/download/atom-shell'; - var buildInstallCommand = initialNpmCommand + npmFlags + 'install'; var buildInstallOptions = {cwd: path.resolve(__dirname, '..', 'build')}; var apmInstallCommand = npmPath + npmFlags + '--target=0.10.35 ' + 'install'; From cdf1029698bd5d76822acf0b3ba64ea1264dee98 Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 9 Dec 2015 23:54:12 -0500 Subject: [PATCH 209/502] It's fine if there isn't an upstream. --- src/git-repository-async.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 116f52216..42e53f105 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -210,7 +210,10 @@ export default class GitRepositoryAsync { getAheadBehindCount (reference, _path) { return this._getRepo(_path) .then(repo => Promise.all([repo, repo.getBranch(reference)])) - .then(([repo, local]) => Promise.all([repo, local, Git.Branch.upstream(local)])) + .then(([repo, local]) => { + const upstream = Git.Branch.upstream(local).catch(_ => null) + return Promise.all([repo, local, upstream]) + }) .then(([repo, local, upstream]) => { if (!upstream) return {ahead: 0, behind: 0} From f0a9c5d9f09c3dd4e263322d0c4e567afb818518 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 10:33:56 -0500 Subject: [PATCH 210/502] Just use the repo declared above. --- spec/git-repository-async-spec.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 9aca3356c..0bc390814 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -55,8 +55,6 @@ describe('GitRepositoryAsync', () => { }) describe('.isPathIgnored(path)', () => { - let repo - beforeEach(() => { repo = openFixture('ignore.git') }) From 46d59fdda10f65b5647e935c571e3696ecbe5578 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 10:34:49 -0500 Subject: [PATCH 211/502] Fix usage when we're given no options. --- src/git-repository-async.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 42e53f105..c1d42bc41 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -34,7 +34,9 @@ export default class GitRepositoryAsync { this._refreshingCount = 0 - let {refreshOnWindowFocus} = options || true + options = options || {} + + let {refreshOnWindowFocus = true} = options if (refreshOnWindowFocus) { const onWindowFocus = () => this.refreshStatus() window.addEventListener('focus', onWindowFocus) From 4711d0367532ecb360548fea128556fa3833c52c Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 10:35:07 -0500 Subject: [PATCH 212/502] Tests for .checkoutHeadForEditor. --- spec/git-repository-async-spec.js | 35 +++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 0bc390814..d344fbb2f 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -203,6 +203,41 @@ describe('GitRepositoryAsync', () => { }) }) + describe('.checkoutHeadForEditor(editor)', () => { + let filePath + let editor + + beforeEach(() => { + spyOn(atom, "confirm") + + const workingDirPath = copyRepository() + repo = new GitRepositoryAsync(workingDirPath, {project: atom.project, config: atom.config, confirm: atom.confirm}) + filePath = path.join(workingDirPath, 'a.txt') + fs.writeFileSync(filePath, 'ch ch changes') + + waitsForPromise(() => atom.workspace.open(filePath)) + runs(() => editor = atom.workspace.getActiveTextEditor()) + }) + + it('displays a confirmation dialog by default', async () => { + atom.confirm.andCallFake(({buttons}) => buttons.OK()) + atom.config.set('editor.confirmCheckoutHeadRevision', true) + + await repo.checkoutHeadForEditor(editor) + + expect(fs.readFileSync(filePath, 'utf8')).toBe('') + }) + + it('does not display a dialog when confirmation is disabled', async () => { + atom.config.set('editor.confirmCheckoutHeadRevision', false) + + await repo.checkoutHeadForEditor(editor) + + expect(fs.readFileSync(filePath, 'utf8')).toBe('') + expect(atom.confirm).not.toHaveBeenCalled() + }) + }) + describe('.getDirectoryStatus(path)', () => { let directoryPath, filePath From 7f19cd4f17b100e9c8fb1be4227f0f6062dbab16 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 10:35:46 -0500 Subject: [PATCH 213/502] Throw if we try to use the repository after it's been destroyed. --- src/git-repository-async.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index c1d42bc41..8b3acef3c 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -61,6 +61,8 @@ export default class GitRepositoryAsync { this.subscriptions.dispose() this.subscriptions = null } + + this.repoPromise = null } // Event subscription @@ -213,14 +215,13 @@ export default class GitRepositoryAsync { return this._getRepo(_path) .then(repo => Promise.all([repo, repo.getBranch(reference)])) .then(([repo, local]) => { - const upstream = Git.Branch.upstream(local).catch(_ => null) + const upstream = Git.Branch.upstream(local) return Promise.all([repo, local, upstream]) }) .then(([repo, local, upstream]) => { - if (!upstream) return {ahead: 0, behind: 0} - return Git.Graph.aheadBehind(repo, local.target(), upstream.target()) }) + .catch(_ => ({ahead: 0, behind: 0})) } // Public: Get the cached ahead/behind commit counts for the current branch's @@ -664,7 +665,15 @@ export default class GitRepositoryAsync { return this._refreshingCount === 0 } + _destroyed() { + return this.repoPromise == null + } + _getRepo (_path) { + if (this._destroyed()) { + return Promise.reject(new Error('Repository has been destroyed')) + } + if (!_path) return this.repoPromise return this.isSubmodule(_path) From 86e39a22ffe17fdd49391a14599b5308db0dde8e Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 10:35:53 -0500 Subject: [PATCH 214/502] Test .destroy(). --- spec/git-repository-async-spec.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index d344fbb2f..1f8768593 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -238,6 +238,21 @@ describe('GitRepositoryAsync', () => { }) }) + describe('.destroy()', () => { + it('throws an exception when any method is called after it is called', async () => { + repo = new GitRepositoryAsync(require.resolve('./fixtures/git/master.git/HEAD')) + repo.destroy() + + let threw = false + try { + await repo.getShortHead() + } catch (e) { + threw = true + } + expect(threw).toBeTruthy() + }) + }) + describe('.getDirectoryStatus(path)', () => { let directoryPath, filePath From e867b68eab9b691f91cecb401f320cf811458c1d Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 10:38:15 -0500 Subject: [PATCH 215/502] Match the ordering of the GitRepository spec. --- spec/git-repository-async-spec.js | 52 +++++++++++++++---------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 1f8768593..9c49980f2 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -177,32 +177,6 @@ describe('GitRepositoryAsync', () => { }) }) - describe('.getPathStatus(path)', () => { - let filePath - - beforeEach(() => { - const workingDirectory = copyRepository() - repo = GitRepositoryAsync.open(workingDirectory) - filePath = path.join(workingDirectory, 'file.txt') - }) - - it('trigger a status-changed event when the new status differs from the last cached one', async () => { - const statusHandler = jasmine.createSpy('statusHandler') - repo.onDidChangeStatus(statusHandler) - fs.writeFileSync(filePath, '') - - await repo.getPathStatus(filePath) - - expect(statusHandler.callCount).toBe(1) - const status = Git.Status.STATUS.WT_MODIFIED - expect(statusHandler.argsForCall[0][0]).toEqual({path: filePath, pathStatus: status}) - fs.writeFileSync(filePath, 'abc') - - await repo.getPathStatus(filePath) - expect(statusHandler.callCount).toBe(1) - }) - }) - describe('.checkoutHeadForEditor(editor)', () => { let filePath let editor @@ -253,6 +227,32 @@ describe('GitRepositoryAsync', () => { }) }) + describe('.getPathStatus(path)', () => { + let filePath + + beforeEach(() => { + const workingDirectory = copyRepository() + repo = GitRepositoryAsync.open(workingDirectory) + filePath = path.join(workingDirectory, 'file.txt') + }) + + it('trigger a status-changed event when the new status differs from the last cached one', async () => { + const statusHandler = jasmine.createSpy('statusHandler') + repo.onDidChangeStatus(statusHandler) + fs.writeFileSync(filePath, '') + + await repo.getPathStatus(filePath) + + expect(statusHandler.callCount).toBe(1) + const status = Git.Status.STATUS.WT_MODIFIED + expect(statusHandler.argsForCall[0][0]).toEqual({path: filePath, pathStatus: status}) + fs.writeFileSync(filePath, 'abc') + + await repo.getPathStatus(filePath) + expect(statusHandler.callCount).toBe(1) + }) + }) + describe('.getDirectoryStatus(path)', () => { let directoryPath, filePath From 352f4064e9720b91b77ad477321492074899dc5b Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 11:05:55 -0500 Subject: [PATCH 216/502] Add a whole ton of missing documentation. --- src/git-repository-async.js | 237 ++++++++++++++++++++++++++++-------- 1 file changed, 187 insertions(+), 50 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 8b3acef3c..cb2c8ffae 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -51,6 +51,10 @@ export default class GitRepositoryAsync { } } + // Public: Destroy this {GitRepositoryAsync} object. + // + // This destroys any tasks and subscriptions and releases the underlying + // libgit2 repository handle. This method is idempotent. destroy () { if (this.emitter) { this.emitter.emit('did-destroy') @@ -68,14 +72,39 @@ export default class GitRepositoryAsync { // Event subscription // ================== + // Public: Invoke the given callback when this GitRepositoryAsync's destroy() + // method is invoked. + // + // * `callback` {Function} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidDestroy (callback) { return this.emitter.on('did-destroy', callback) } + // Public: Invoke the given callback when a specific file's status has + // changed. When a file is updated, reloaded, etc, and the status changes, this + // will be fired. + // + // * `callback` {Function} + // * `event` {Object} + // * `path` {String} the old parameters the decoration used to have + // * `pathStatus` {Number} representing the status. This value can be passed to + // {::isStatusModified} or {::isStatusNew} to get more information. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeStatus (callback) { return this.emitter.on('did-change-status', callback) } + // Public: Invoke the given callback when a multiple files' statuses have + // changed. For example, on window focus, the status of all the paths in the + // repo is checked. If any of them have changed, this will be fired. Call + // {::getPathStatus(path)} to get the status for your path of choice. + // + // * `callback` {Function} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeStatuses (callback) { return this.emitter.on('did-change-statuses', callback) } @@ -178,7 +207,7 @@ export default class GitRepositoryAsync { // // Returns a {Promise} which resolves to a {String}. getShortHead (_path) { - return this._getRepo(_path) + return this.getRepo(_path) .then(repo => repo.getCurrentBranch()) .then(branch => branch.shorthand()) } @@ -212,7 +241,7 @@ export default class GitRepositoryAsync { // * `ahead` The {Number} of commits ahead. // * `behind` The {Number} of commits behind. getAheadBehindCount (reference, _path) { - return this._getRepo(_path) + return this.getRepo(_path) .then(repo => Promise.all([repo, repo.getBranch(reference)])) .then(([repo, local]) => { const upstream = Git.Branch.upstream(local) @@ -245,7 +274,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to the {String} git configuration value // specified by the key. getConfigValue (key, _path) { - return this._getRepo(_path) + return this.getRepo(_path) .then(repo => repo.configSnapshot()) .then(config => config.getStringBuf(key)) .catch(_ => null) @@ -271,7 +300,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to a {String} branch name such as // `refs/remotes/origin/master`. getUpstreamBranch (_path) { - return this._getRepo(_path) + return this.getRepo(_path) .then(repo => repo.getCurrentBranch()) .then(branch => Git.Branch.upstream(branch)) } @@ -286,7 +315,7 @@ export default class GitRepositoryAsync { // * `remotes` An {Array} of remote reference names. // * `tags` An {Array} of tag reference names. getReferences (_path) { - return this._getRepo(_path) + return this.getRepo(_path) .then(repo => repo.getReferences(Git.Reference.TYPE.LISTALL)) .then(refs => { const heads = [] @@ -314,7 +343,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to the current {String} SHA for the // given reference. getReferenceTarget (reference, _path) { - return this._getRepo(_path) + return this.getRepo(_path) .then(repo => Git.Reference.nameToId(repo, reference)) .then(oid => oid.tostrS()) } @@ -322,18 +351,36 @@ export default class GitRepositoryAsync { // Reading Status // ============== + // Public: Resolves true if the given path is modified. + // + // * `path` The {String} path to check. + // + // Returns a {Promise} which resolves to a {Boolean} that's true if the `path` + // is modified. isPathModified (_path) { return this._filterStatusesByPath(_path).then(statuses => { return statuses.filter(status => status.isModified()).length > 0 }) } + // Public: Resolves true if the given path is new. + // + // * `path` The {String} path to check. + // + // Returns a {Promise} which resolves to a {Boolean} that's true if the `path` + // is new. isPathNew (_path) { return this._filterStatusesByPath(_path).then(statuses => { return statuses.filter(status => status.isNew()).length > 0 }) } + // Public: Is the given path ignored? + // + // * `path` The {String} path to check. + // + // Returns a {Promise} which resolves to a {Boolean} that's true if the `path` + // is ignored. isPathIgnored (_path) { return this.repoPromise.then(repo => Git.Ignore.pathIsIgnored(repo, _path)) } @@ -344,7 +391,6 @@ export default class GitRepositoryAsync { // // Returns a promise resolving to a {Number} representing the status. This value can be passed to // {::isStatusModified} or {::isStatusNew} to get more information. - getDirectoryStatus (directoryPath) { let relativePath // XXX _filterSBD already gets repoPromise @@ -419,38 +465,54 @@ export default class GitRepositoryAsync { .then(relativePath => this.pathStatusCache[relativePath]) } - isStatusNew (statusBit) { - return (statusBit & newStatusFlags) > 0 - } - + // Public: Returns true if the given status indicates modification. + // + // * `statusBit` A {Number} representing the status. + // + // Returns a {Boolean} that's true if the `statusBit` indicates modification. isStatusModified (statusBit) { return (statusBit & modifiedStatusFlags) > 0 } + // Public: Returns true if the given status indicates a new path. + // + // * `statusBit` A {Number} representing the status. + // + // Returns a {Boolean} that's true if the `statusBit` indicates a new path. + isStatusNew (statusBit) { + return (statusBit & newStatusFlags) > 0 + } + + // Public: Returns true if the given status indicates the path is staged. + // + // * `statusBit` A {Number} representing the status. + // + // Returns a {Boolean} that's true if the `statusBit` indicates the path is + // staged. isStatusStaged (statusBit) { return (statusBit & indexStatusFlags) > 0 } + // Public: Returns true if the given status indicates the path is ignored. + // + // * `statusBit` A {Number} representing the status. + // + // Returns a {Boolean} that's true if the `statusBit` indicates the path is + // ignored. isStatusIgnored (statusBit) { return (statusBit & (1 << 14)) > 0 } + // Public: Returns true if the given status indicates the path is deleted. + // + // * `statusBit` A {Number} representing the status. + // + // Returns a {Boolean} that's true if the `statusBit` indicates the path is + // deleted. isStatusDeleted (statusBit) { return (statusBit & deletedStatusFlags) > 0 } - _getDiffHunks (diff) { - return diff.patches() - .then(patches => Promise.all(patches.map(p => p.hunks()))) // patches :: Array - .then(hunks => _.flatten(hunks)) // hunks :: Array> - } - - _getDiffLines (diff) { - return this._getDiffHunks(diff) - .then(hunks => Promise.all(hunks.map(h => h.lines()))) - .then(lines => _.flatten(lines)) // lines :: Array> - } - // Retrieving Diffs // ================ // Public: Retrieves the number of lines added and removed to a path. @@ -487,14 +549,6 @@ export default class GitRepositoryAsync { }) } - _diffBlobToBuffer (blob, buffer, options) { - const hunks = [] - const hunkCallback = (delta, hunk, payload) => { - hunks.push(hunk) - } - return Git.Diff.blobToBuffer(blob, null, buffer, null, null, options, null, null, hunkCallback, null, null).then(_ => hunks) - } - // Public: Retrieves the line diffs comparing the `HEAD` version of the given // path and the given text. // @@ -563,12 +617,6 @@ export default class GitRepositoryAsync { .then(() => this.refreshStatusForPath(_path)) } - _createBranch (name) { - return this.repoPromise - .then(repo => Promise.all([repo, repo.getHeadCommit()])) - .then(([repo, commit]) => repo.createBranch(name, commit)) - } - // Public: Checks out a branch in your repository. // // * `reference` The {String} reference to checkout. @@ -607,6 +655,57 @@ export default class GitRepositoryAsync { }).then(filePath => this.checkoutHead(filePath)) } + // Create a new branch with the given name. + // + // name :: String + // The name of the new branch. + // + // Returns :: Promise + // A reference to the created branch. + _createBranch (name) { + return this.repoPromise + .then(repo => Promise.all([repo, repo.getHeadCommit()])) + .then(([repo, commit]) => repo.createBranch(name, commit)) + } + + // Get all the hunks in the diff. + // + // diff :: NodeGit.Diff + // + // Returns :: Promise> + _getDiffHunks (diff) { + return diff.patches() + .then(patches => Promise.all(patches.map(p => p.hunks()))) // patches :: Array + .then(hunks => _.flatten(hunks)) // hunks :: Array> + } + + // Get all the lines contained in the diff. + // + // diff :: NodeGit.Diff + // + // Returns :: Promise> + _getDiffLines (diff) { + return this._getDiffHunks(diff) + .then(hunks => Promise.all(hunks.map(h => h.lines()))) + .then(lines => _.flatten(lines)) // lines :: Array> + } + + // Diff the given blob and buffer with the provided options. + // + // blob :: NodeGit.Blob + // buffer :: String + // options :: NodeGit.DiffOptions + // + // Returns :: Promise> + _diffBlobToBuffer (blob, buffer, options) { + const hunks = [] + const hunkCallback = (delta, hunk, payload) => { + hunks.push(hunk) + } + return Git.Diff.blobToBuffer(blob, null, buffer, null, null, options, null, null, hunkCallback, null, null) + .then(_ => hunks) + } + // Get the current branch and update this.branch. // // Returns :: Promise @@ -618,11 +717,21 @@ export default class GitRepositoryAsync { .then(branchName => this.branch = branchName) } + // Refresh the cached ahead/behind count with the given branch. + // + // branchName :: String + // The name of the branch whose ahead/behind should be used for + // the refresh. + // + // Returns :: Promise _refreshAheadBehindCount (branchName) { return this.getAheadBehindCount(branchName) .then(counts => this.upstreamByPath['.'] = counts) } + // Refresh the cached status. + // + // Returns :: Promise _refreshStatus () { this._refreshingCount++ @@ -658,18 +767,14 @@ export default class GitRepositoryAsync { return Promise.all([status, branch, aheadBehind]).then(_ => null) } - // Section: Private - // ================ - - _isRefreshing () { - return this._refreshingCount === 0 - } - - _destroyed() { - return this.repoPromise == null - } - - _getRepo (_path) { + // Get the NodeGit repository for the given path. + // + // path :: Optional + // The path within the repository. This is only needed if you want + // to get the repository for that path if it is a submodule. + // + // Returns :: Promise + getRepo (_path) { if (this._destroyed()) { return Promise.reject(new Error('Repository has been destroyed')) } @@ -686,6 +791,24 @@ export default class GitRepositoryAsync { }) } + // Section: Private + // ================ + + // Is the repository currently refreshing its status? + // + // Returns :: Bool + _isRefreshing () { + return this._refreshingCount === 0 + } + + // Has the repository been destroyed? + // + // Returns :: Bool + _destroyed() { + return this.repoPromise == null + } + + // Subscribe to events on the given buffer. subscribeToBuffer (buffer) { const bufferSubscriptions = new CompositeDisposable() @@ -710,8 +833,15 @@ export default class GitRepositoryAsync { return } + // Get the status for the given path. + // + // path :: String + // The path whose status is wanted. + // + // Returns :: Promise + // The status for the path. _filterStatusesByPath (_path) { - // Surely I'm missing a built-in way to do this + // TODO: Is there a more efficient way to do this? let basePath = null return this.repoPromise .then(repo => { @@ -723,6 +853,13 @@ export default class GitRepositoryAsync { }) } + // Get the status for everything in the given directory. + // + // directoryPath :: String + // The directory whose status is wanted. + // + // Returns :: Promise> + // The status for every file in the directory. _filterStatusesByDirectory (directoryPath) { return this.repoPromise .then(repo => repo.getStatus()) From abf5697240900426e37c60cba45bc6d1fe552ae7 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 11:33:01 -0500 Subject: [PATCH 217/502] Delint --- src/git-repository-async.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index cb2c8ffae..dcb677bd4 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -804,7 +804,7 @@ export default class GitRepositoryAsync { // Has the repository been destroyed? // // Returns :: Bool - _destroyed() { + _destroyed () { return this.repoPromise == null } From 47606507f7712c3cae1cb978ff3d7924b5096297 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 16:31:36 -0500 Subject: [PATCH 218/502] We want the booleans. --- spec/git-repository-async-spec.js | 38 +++++++++++++++---------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 9c49980f2..2db1fa7da 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -41,7 +41,7 @@ describe('GitRepositoryAsync', () => { threw = true } - expect(threw).toBeTruthy() + expect(threw).toBe(true) expect(repo.repo).toBe(null) }) }) @@ -61,12 +61,12 @@ describe('GitRepositoryAsync', () => { it('resolves true for an ignored path', async () => { const ignored = await repo.isPathIgnored('a.txt') - expect(ignored).toBeTruthy() + expect(ignored).toBe(true) }) it('resolves false for a non-ignored path', async () => { const ignored = await repo.isPathIgnored('b.txt') - expect(ignored).toBeFalsy() + expect(ignored).toBe(false) }) }) @@ -85,23 +85,23 @@ describe('GitRepositoryAsync', () => { describe('when the path is unstaged', () => { it('resolves false if the path has not been modified', async () => { const modified = await repo.isPathModified(filePath) - expect(modified).toBeFalsy() + expect(modified).toBe(false) }) it('resolves true if the path is modified', async () => { fs.writeFileSync(filePath, 'change') const modified = await repo.isPathModified(filePath) - expect(modified).toBeTruthy() + expect(modified).toBe(true) }) it('resolves false if the path is new', async () => { const modified = await repo.isPathModified(newPath) - expect(modified).toBeFalsy() + expect(modified).toBe(false) }) it('resolves false if the path is invalid', async () => { const modified = await repo.isPathModified(emptyPath) - expect(modified).toBeFalsy() + expect(modified).toBe(false) }) }) }) @@ -119,12 +119,12 @@ describe('GitRepositoryAsync', () => { describe('when the path is unstaged', () => { it('returns true if the path is new', async () => { const isNew = await repo.isPathNew(newPath) - expect(isNew).toBeTruthy() + expect(isNew).toBe(true) }) it("returns false if the path isn't new", async () => { const modified = await repo.isPathModified(newPath) - expect(modified).toBeFalsy() + expect(modified).toBe(false) }) }) }) @@ -140,17 +140,17 @@ describe('GitRepositoryAsync', () => { it('no longer reports a path as modified after checkout', async () => { let modified = await repo.isPathModified(filePath) - expect(modified).toBeFalsy() + expect(modified).toBe(false) fs.writeFileSync(filePath, 'ch ch changes') modified = await repo.isPathModified(filePath) - expect(modified).toBeTruthy() + expect(modified).toBe(true) await repo.checkoutHead(filePath) modified = await repo.isPathModified(filePath) - expect(modified).toBeFalsy() + expect(modified).toBe(false) }) it('restores the contents of the path to the original text', async () => { @@ -223,7 +223,7 @@ describe('GitRepositoryAsync', () => { } catch (e) { threw = true } - expect(threw).toBeTruthy() + expect(threw).toBe(true) }) }) @@ -295,8 +295,8 @@ describe('GitRepositoryAsync', () => { await repo.refreshStatus() expect(await repo.getCachedPathStatus(cleanPath)).toBeUndefined() - expect(repo.isStatusNew(await repo.getCachedPathStatus(newPath))).toBeTruthy() - expect(repo.isStatusModified(await repo.getCachedPathStatus(modifiedPath))).toBeTruthy() + expect(repo.isStatusNew(await repo.getCachedPathStatus(newPath))).toBe(true) + expect(repo.isStatusModified(await repo.getCachedPathStatus(modifiedPath))).toBe(true) }) }) @@ -510,12 +510,12 @@ describe('GitRepositoryAsync', () => { it('resolves true when the branch exists', async () => { const hasBranch = await repo.hasBranch('master') - expect(hasBranch).toBeTruthy() + expect(hasBranch).toBe(true) }) it("resolves false when the branch doesn't exist", async () => { const hasBranch = await repo.hasBranch('trolleybus') - expect(hasBranch).toBeFalsy() + expect(hasBranch).toBe(false) }) }) @@ -575,8 +575,8 @@ describe('GitRepositoryAsync', () => { .then(_ => success = true) .catch(_ => threw = true)) runs(() => { - expect(success).toBeTruthy() - expect(threw).toBeFalsy() + expect(success).toBe(true) + expect(threw).toBe(false) }) }) }) From be964abf9ff96f62bbb0d0543e2f71cc5d4a62e5 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 16:31:46 -0500 Subject: [PATCH 219/502] Ensure we actually do give them a boolean. --- src/git-repository-async.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index dcb677bd4..dea3baec0 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -382,7 +382,9 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to a {Boolean} that's true if the `path` // is ignored. isPathIgnored (_path) { - return this.repoPromise.then(repo => Git.Ignore.pathIsIgnored(repo, _path)) + return this.repoPromise + .then(repo => Git.Ignore.pathIsIgnored(repo, _path)) + .then(ignored => Boolean(ignored)) } // Get the status of a directory in the repository's working directory. From 1a04c5a9c23c3c09b9bff389e1f17b3099130ed5 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 16:41:38 -0500 Subject: [PATCH 220/502] Be more specific in our error names. --- spec/git-repository-async-spec.js | 6 +++--- src/git-repository-async.js | 8 +++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 2db1fa7da..83d746ad2 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -217,13 +217,13 @@ describe('GitRepositoryAsync', () => { repo = new GitRepositoryAsync(require.resolve('./fixtures/git/master.git/HEAD')) repo.destroy() - let threw = false + let error = null try { await repo.getShortHead() } catch (e) { - threw = true + error = e } - expect(threw).toBe(true) + expect(error.name).toBe(GitRepositoryAsync.DestroyedErrorName) }) }) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index dea3baec0..5db475194 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -23,6 +23,10 @@ export default class GitRepositoryAsync { return Git } + static get DestroyedErrorName () { + return 'GitRepositoryAsync.destroyed' + } + constructor (_path, options) { this.repo = null this.emitter = new Emitter() @@ -778,7 +782,9 @@ export default class GitRepositoryAsync { // Returns :: Promise getRepo (_path) { if (this._destroyed()) { - return Promise.reject(new Error('Repository has been destroyed')) + const error = new Error('Repository has been destroyed') + error.name = GitRepositoryAsync.DestroyedErrorName + return Promise.reject(error) } if (!_path) return this.repoPromise From bc14c28f11617b96cb9bac6cd6d370a73f6a3824 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 16:42:11 -0500 Subject: [PATCH 221/502] Use the usual name. --- spec/git-repository-async-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 83d746ad2..82d4b6f24 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -301,7 +301,7 @@ describe('GitRepositoryAsync', () => { }) describe('buffer events', () => { - let repository + let repo beforeEach(() => { const workingDirectory = copyRepository() From 0794ea365d874fc8e97ef9c79641a48d6af4e0ab Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 16:43:25 -0500 Subject: [PATCH 222/502] Tinker tailor soldier spy. --- spec/git-repository-async-spec.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 82d4b6f24..74bf8ef16 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -319,12 +319,15 @@ describe('GitRepositoryAsync', () => { editor.insertNewline() - let called - repository.onDidChangeStatus(c => called = c) + const statusHandler = jasmine.createSpy('statusHandler') + repository.onDidChangeStatus(statusHandler) editor.save() - waitsFor(() => Boolean(called)) - runs(() => expect(called).toEqual({path: editor.getPath(), pathStatus: 256})) + waitsFor(() => statusHandler.callCount > 0) + runs(() => { + expect(statusHandler.callCount).toBeGreaterThan(0) + expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) + }) }) it('emits a status-changed event when a buffer is reloaded', async () => { From 5302fb84a944e3db98a484966f89934e2b7ff312 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 16:44:51 -0500 Subject: [PATCH 223/502] Better test name. --- spec/git-repository-async-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 74bf8ef16..8850bd9a7 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -590,7 +590,7 @@ describe('GitRepositoryAsync', () => { repo = GitRepositoryAsync.open(workingDirectory) }) - it('can get the line diff', async () => { + it('returns the old and new lines of the diff', async () => { const {oldStart, newStart, oldLines, newLines} = await repo.getLineDiffs('a.txt', 'hi there') expect(oldStart).toBe(0) expect(oldLines).toBe(0) From fcf8581255b50afe9245da468ed1a38d6984c084 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 16:45:05 -0500 Subject: [PATCH 224/502] Put this constant with the others. --- src/git-repository-async.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 5db475194..4b97323d2 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -9,6 +9,7 @@ const modifiedStatusFlags = Git.Status.STATUS.WT_MODIFIED | Git.Status.STATUS.IN const newStatusFlags = Git.Status.STATUS.WT_NEW | Git.Status.STATUS.INDEX_NEW const deletedStatusFlags = Git.Status.STATUS.WT_DELETED | Git.Status.STATUS.INDEX_DELETED const indexStatusFlags = Git.Status.STATUS.INDEX_NEW | Git.Status.STATUS.INDEX_MODIFIED | Git.Status.STATUS.INDEX_DELETED | Git.Status.STATUS.INDEX_RENAMED | Git.Status.STATUS.INDEX_TYPECHANGE +const submoduleMode = 57344 // TODO compose this from libgit2 constants // Just using this for _.isEqual and _.object, we should impl our own here import _ from 'underscore-plus' @@ -229,7 +230,6 @@ export default class GitRepositoryAsync { // TODO: This'll probably be wrong if the submodule doesn't exist in the // index yet? Is that a thing? const entry = index.getByPath(_path) - const submoduleMode = 57344 // TODO compose this from libgit2 constants return entry.mode === submoduleMode }) } From 3e7344394c01963a501b88732131ec93ebcff662 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 16:54:15 -0500 Subject: [PATCH 225/502] Fix the documentation style. --- src/git-repository-async.js | 76 +++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 41 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 4b97323d2..d96cb1b7c 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -395,8 +395,9 @@ export default class GitRepositoryAsync { // // * `directoryPath` The {String} path to check. // - // Returns a promise resolving to a {Number} representing the status. This value can be passed to - // {::isStatusModified} or {::isStatusNew} to get more information. + // Returns a {Promise} resolving to a {Number} representing the status. This + // value can be passed to {::isStatusModified} or {::isStatusNew} to get more + // information. getDirectoryStatus (directoryPath) { let relativePath // XXX _filterSBD already gets repoPromise @@ -423,11 +424,10 @@ export default class GitRepositoryAsync { // Note that if the status of the path has changed, this will emit a // 'did-change-status' event. // - // path :: String - // The path whose status should be refreshed. + // * `path` The {String} path whose status should be refreshed. // - // Returns :: Promise - // The refreshed status bit for the path. + // Returns a {Promise} which resolves to a {Number} which is the refreshed + // status bit for the path. refreshStatusForPath (_path) { this._refreshingCount++ @@ -663,11 +663,10 @@ export default class GitRepositoryAsync { // Create a new branch with the given name. // - // name :: String - // The name of the new branch. + // * `name` The {String} name of the new branch. // - // Returns :: Promise - // A reference to the created branch. + // Returns a {Promise} which resolves to a {NodeGit.Ref} reference to the + // created branch. _createBranch (name) { return this.repoPromise .then(repo => Promise.all([repo, repo.getHeadCommit()])) @@ -676,9 +675,9 @@ export default class GitRepositoryAsync { // Get all the hunks in the diff. // - // diff :: NodeGit.Diff + // * `diff` The {NodeGit.Diff} whose hunks should be retrieved. // - // Returns :: Promise> + // Returns a {Promise} which resolves to an {Array} of {NodeGit.Hunk}. _getDiffHunks (diff) { return diff.patches() .then(patches => Promise.all(patches.map(p => p.hunks()))) // patches :: Array @@ -687,9 +686,9 @@ export default class GitRepositoryAsync { // Get all the lines contained in the diff. // - // diff :: NodeGit.Diff + // * `diff` The {NodeGit.Diff} use lines should be retrieved. // - // Returns :: Promise> + // Returns a {Promise} which resolves to an {Array} of {NodeGit.Line}. _getDiffLines (diff) { return this._getDiffHunks(diff) .then(hunks => Promise.all(hunks.map(h => h.lines()))) @@ -698,11 +697,11 @@ export default class GitRepositoryAsync { // Diff the given blob and buffer with the provided options. // - // blob :: NodeGit.Blob - // buffer :: String - // options :: NodeGit.DiffOptions + // * `blob` The {NodeGit.Blob} + // * `buffer` The {String} buffer. + // * `options` The {NodeGit.DiffOptions} // - // Returns :: Promise> + // Returns a {Promise} which resolves to an {Array} of {NodeGit.Hunk}. _diffBlobToBuffer (blob, buffer, options) { const hunks = [] const hunkCallback = (delta, hunk, payload) => { @@ -714,8 +713,7 @@ export default class GitRepositoryAsync { // Get the current branch and update this.branch. // - // Returns :: Promise - // The branch name. + // Returns a {Promise} which resolves to the {String} branch name. _refreshBranch () { return this.repoPromise .then(repo => repo.getCurrentBranch()) @@ -725,11 +723,10 @@ export default class GitRepositoryAsync { // Refresh the cached ahead/behind count with the given branch. // - // branchName :: String - // The name of the branch whose ahead/behind should be used for - // the refresh. + // * `branchName` The {String} name of the branch whose ahead/behind should be + // used for the refresh. // - // Returns :: Promise + // Returns a {Promise} which will resolve to {null}. _refreshAheadBehindCount (branchName) { return this.getAheadBehindCount(branchName) .then(counts => this.upstreamByPath['.'] = counts) @@ -737,7 +734,7 @@ export default class GitRepositoryAsync { // Refresh the cached status. // - // Returns :: Promise + // Returns a {Promise} which will resolve to {null}. _refreshStatus () { this._refreshingCount++ @@ -761,8 +758,7 @@ export default class GitRepositoryAsync { // Refreshes the git status. // - // Returns :: Promise - // Resolves when refresh has completed. + // Returns a {Promise} which will resolve to {null} when refresh is complete. refreshStatus () { // TODO add submodule tracking @@ -775,11 +771,11 @@ export default class GitRepositoryAsync { // Get the NodeGit repository for the given path. // - // path :: Optional - // The path within the repository. This is only needed if you want - // to get the repository for that path if it is a submodule. + // * `path` The optional {String} path within the repository. This is only + // needed if you want to get the repository for that path if it is a + // submodule. // - // Returns :: Promise + // Returns a {Promise} which resolves to the {NodeGit.Repository}. getRepo (_path) { if (this._destroyed()) { const error = new Error('Repository has been destroyed') @@ -804,14 +800,14 @@ export default class GitRepositoryAsync { // Is the repository currently refreshing its status? // - // Returns :: Bool + // Returns a {Boolean}. _isRefreshing () { return this._refreshingCount === 0 } // Has the repository been destroyed? // - // Returns :: Bool + // Returns a {Boolean}. _destroyed () { return this.repoPromise == null } @@ -843,11 +839,10 @@ export default class GitRepositoryAsync { // Get the status for the given path. // - // path :: String - // The path whose status is wanted. + // * `path` The {String} path whose status is wanted. // - // Returns :: Promise - // The status for the path. + // Returns a {Promise} which resolves to the {NodeGit.StatusFile} status for + // the path. _filterStatusesByPath (_path) { // TODO: Is there a more efficient way to do this? let basePath = null @@ -863,11 +858,10 @@ export default class GitRepositoryAsync { // Get the status for everything in the given directory. // - // directoryPath :: String - // The directory whose status is wanted. + // * `directoryPath` The {String} directory whose status is wanted. // - // Returns :: Promise> - // The status for every file in the directory. + // Returns a {Promise} which resolves to an {Array} of {NodeGit.StatusFile} + // statuses for every file in the directory. _filterStatusesByDirectory (directoryPath) { return this.repoPromise .then(repo => repo.getStatus()) From 4916a2e7626f5089b8da2fbe2da89ba992c4d92e Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 16:55:08 -0500 Subject: [PATCH 226/502] :doughnut: need this. --- src/git-repository-async.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index d96cb1b7c..afc5795c9 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -834,7 +834,6 @@ export default class GitRepositoryAsync { ) this.subscriptions.add(bufferSubscriptions) - return } // Get the status for the given path. From 0c42747e8a62f8ce0c0bcd8b9f5335e60e87a07e Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 16:55:21 -0500 Subject: [PATCH 227/502] Just Use Clone. --- src/git-repository.coffee | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/git-repository.coffee b/src/git-repository.coffee index 5a0df6743..6ab6be6a5 100644 --- a/src/git-repository.coffee +++ b/src/git-repository.coffee @@ -76,9 +76,7 @@ class GitRepository unless @repo? throw new Error("No Git repository found searching path: #{path}") - asyncOptions = {} - for key, val of options - asyncOptions[key] = val + asyncOptions = _.clone(options) # GitRepository itself will handle these cases by manually calling through # to the async repo. asyncOptions.refreshOnWindowFocus = false From 3ccf1861506e1cd37797134502ff1d0a4758ba1b Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 17:57:13 -0500 Subject: [PATCH 228/502] This can be local. --- src/git-repository-async.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index afc5795c9..8d58a0066 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -399,11 +399,10 @@ export default class GitRepositoryAsync { // value can be passed to {::isStatusModified} or {::isStatusNew} to get more // information. getDirectoryStatus (directoryPath) { - let relativePath // XXX _filterSBD already gets repoPromise return this.repoPromise .then(repo => { - relativePath = this.relativize(directoryPath, repo.workdir()) + const relativePath = this.relativize(directoryPath, repo.workdir()) return this._filterStatusesByDirectory(relativePath) }) .then(statuses => { From dba698f672df8c1e2e1b69e1370d8b2212b53186 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 17:57:21 -0500 Subject: [PATCH 229/502] Define a constant for this. --- src/git-repository-async.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 8d58a0066..296c29adc 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -9,7 +9,8 @@ const modifiedStatusFlags = Git.Status.STATUS.WT_MODIFIED | Git.Status.STATUS.IN const newStatusFlags = Git.Status.STATUS.WT_NEW | Git.Status.STATUS.INDEX_NEW const deletedStatusFlags = Git.Status.STATUS.WT_DELETED | Git.Status.STATUS.INDEX_DELETED const indexStatusFlags = Git.Status.STATUS.INDEX_NEW | Git.Status.STATUS.INDEX_MODIFIED | Git.Status.STATUS.INDEX_DELETED | Git.Status.STATUS.INDEX_RENAMED | Git.Status.STATUS.INDEX_TYPECHANGE -const submoduleMode = 57344 // TODO compose this from libgit2 constants +const ignoredStatusFlags = 1 << 14 // TODO: compose this from libgit2 constants +const submoduleMode = 57344 // TODO: compose this from libgit2 constants // Just using this for _.isEqual and _.object, we should impl our own here import _ from 'underscore-plus' @@ -505,7 +506,7 @@ export default class GitRepositoryAsync { // Returns a {Boolean} that's true if the `statusBit` indicates the path is // ignored. isStatusIgnored (statusBit) { - return (statusBit & (1 << 14)) > 0 + return (statusBit & ignoredStatusFlags) > 0 } // Public: Returns true if the given status indicates the path is deleted. From c5383305d081d504d8e75da9c5dd0e1c00bf1d0c Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 17:57:35 -0500 Subject: [PATCH 230/502] If we don't have a project then we're not at the root. --- src/git-repository-async.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 296c29adc..a29b8f033 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -141,13 +141,15 @@ export default class GitRepositoryAsync { // Public: Returns a {Promise} that resolves to true if at the root, false if // in a subfolder of the repository. isProjectAtRoot () { - if (!this.projectAtRoot && this.project) { + if (!this.project) return Promise.resolve(false) + + if (this.projectAtRoot) { + return this.projectAtRoot + } else { this.projectAtRoot = Promise.resolve(() => { return this.repoPromise.then(repo => this.project.relativize(repo.workdir())) }) } - - return this.projectAtRoot } // Public: Makes a path relative to the repository's working directory. From 81d4f0680280fd20dc105c1daa575cb2017b84c7 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 20:00:37 -0500 Subject: [PATCH 231/502] And that's why we write tests. --- spec/git-repository-async-spec.js | 29 ++++++++++++++++++++++++----- src/git-repository-async.js | 12 ++++++------ 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 8850bd9a7..a9f1f9997 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -300,6 +300,25 @@ describe('GitRepositoryAsync', () => { }) }) + describe('.isProjectAtRoot()', () => { + it('returns true when the repository is at the root', async () => { + const workingDirectory = copyRepository() + atom.project.setPaths([workingDirectory]) + const repo = atom.project.getRepositories()[0].async + + const atRoot = await repo.isProjectAtRoot() + expect(atRoot).toBe(true) + }) + + it("returns false when the repository wasn't created with a project", async () => { + const workingDirectory = copyRepository() + const repo = GitRepositoryAsync.open(workingDirectory) + + const atRoot = await repo.isProjectAtRoot() + expect(atRoot).toBe(false) + }) + }) + describe('buffer events', () => { let repo @@ -310,8 +329,8 @@ describe('GitRepositoryAsync', () => { // When the path is added to the project, the repository is refreshed. We // need to wait for that to complete before the tests continue so that // we're in a known state. - repository = atom.project.getRepositories()[0].async - waitsFor(() => !repository._isRefreshing()) + repo = atom.project.getRepositories()[0].async + waitsFor(() => !repo._isRefreshing()) }) it('emits a status-changed event when a buffer is saved', async () => { @@ -320,7 +339,7 @@ describe('GitRepositoryAsync', () => { editor.insertNewline() const statusHandler = jasmine.createSpy('statusHandler') - repository.onDidChangeStatus(statusHandler) + repo.onDidChangeStatus(statusHandler) editor.save() waitsFor(() => statusHandler.callCount > 0) @@ -336,7 +355,7 @@ describe('GitRepositoryAsync', () => { fs.writeFileSync(editor.getPath(), 'changed') const statusHandler = jasmine.createSpy('statusHandler') - repository.onDidChangeStatus(statusHandler) + repo.onDidChangeStatus(statusHandler) editor.getBuffer().reload() waitsFor(() => statusHandler.callCount > 0) @@ -352,7 +371,7 @@ describe('GitRepositoryAsync', () => { fs.writeFileSync(editor.getPath(), 'changed') const statusHandler = jasmine.createSpy('statusHandler') - repository.onDidChangeStatus(statusHandler) + repo.onDidChangeStatus(statusHandler) editor.getBuffer().emitter.emit('did-change-path') waitsFor(() => statusHandler.callCount > 0) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index a29b8f033..a87c93973 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -143,13 +143,13 @@ export default class GitRepositoryAsync { isProjectAtRoot () { if (!this.project) return Promise.resolve(false) - if (this.projectAtRoot) { - return this.projectAtRoot - } else { - this.projectAtRoot = Promise.resolve(() => { - return this.repoPromise.then(repo => this.project.relativize(repo.workdir())) - }) + if (!this.projectAtRoot) { + this.projectAtRoot = this.repoPromise + .then(repo => this.project.relativize(repo.workdir())) + .then(relativePath => relativePath === '') } + + return this.projectAtRoot } // Public: Makes a path relative to the repository's working directory. From 976fe0f2cf6314d7a1c21b35d0c00dc92bc314fd Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 10 Dec 2015 20:19:58 -0500 Subject: [PATCH 232/502] Linter --- spec/git-repository-async-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index a9f1f9997..95c9fc150 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -182,7 +182,7 @@ describe('GitRepositoryAsync', () => { let editor beforeEach(() => { - spyOn(atom, "confirm") + spyOn(atom, 'confirm') const workingDirPath = copyRepository() repo = new GitRepositoryAsync(workingDirPath, {project: atom.project, config: atom.config, confirm: atom.confirm}) From b9f5853729a26424ae0edaf60f2721013e9065d1 Mon Sep 17 00:00:00 2001 From: joshaber Date: Fri, 11 Dec 2015 10:05:10 -0500 Subject: [PATCH 233/502] Const. --- spec/git-repository-async-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 95c9fc150..5d157dfc8 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -17,7 +17,7 @@ function openFixture (fixture) { } function copyRepository () { - let workingDirPath = temp.mkdirSync('atom-working-dir') + const workingDirPath = temp.mkdirSync('atom-working-dir') fs.copySync(path.join(__dirname, 'fixtures', 'git', 'working-dir'), workingDirPath) fs.renameSync(path.join(workingDirPath, 'git.git'), path.join(workingDirPath, '.git')) return fs.realpathSync(workingDirPath) From d14603c7786d155ccd0b4d8842ebb71a2c4f5e2c Mon Sep 17 00:00:00 2001 From: joshaber Date: Fri, 11 Dec 2015 10:09:52 -0500 Subject: [PATCH 234/502] Use `some` instead. --- src/git-repository-async.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index a87c93973..2b006a509 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -365,9 +365,8 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to a {Boolean} that's true if the `path` // is modified. isPathModified (_path) { - return this._filterStatusesByPath(_path).then(statuses => { - return statuses.filter(status => status.isModified()).length > 0 - }) + return this._filterStatusesByPath(_path) + .then(statuses => statuses.some(status => status.isModified())) } // Public: Resolves true if the given path is new. @@ -377,9 +376,8 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to a {Boolean} that's true if the `path` // is new. isPathNew (_path) { - return this._filterStatusesByPath(_path).then(statuses => { - return statuses.filter(status => status.isNew()).length > 0 - }) + return this._filterStatusesByPath(_path) + .then(statuses => statuses.some(status => status.isNew())) } // Public: Is the given path ignored? From 5d87002b3f07687f71186baec75b5acc98bc7cc1 Mon Sep 17 00:00:00 2001 From: joshaber Date: Fri, 11 Dec 2015 10:18:58 -0500 Subject: [PATCH 235/502] Use the functions. --- src/git-repository-async.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 2b006a509..1da9738a3 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -408,13 +408,9 @@ export default class GitRepositoryAsync { }) .then(statuses => { return Promise.all(statuses.map(s => s.statusBit())).then(bits => { - let directoryStatus = 0 - const filteredBits = bits.filter(b => b > 0) - if (filteredBits.length > 0) { - filteredBits.forEach(bit => directoryStatus |= bit) - } - - return directoryStatus + return bits + .filter(b => b > 0) + .reduce((status, bit) => status | bit, 0) }) }) } From c1cf5583b4d80fea4bf9a838be554a0f16107cff Mon Sep 17 00:00:00 2001 From: joshaber Date: Fri, 11 Dec 2015 14:48:11 -0500 Subject: [PATCH 236/502] If there isn't an index entry then it's not a submodule. --- src/git-repository-async.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 1da9738a3..4e900bc16 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -233,6 +233,8 @@ export default class GitRepositoryAsync { // TODO: This'll probably be wrong if the submodule doesn't exist in the // index yet? Is that a thing? const entry = index.getByPath(_path) + if (!entry) return false + return entry.mode === submoduleMode }) } From 8a91bfb20903b95341fdc05ce43b7aeb184a4f55 Mon Sep 17 00:00:00 2001 From: joshaber Date: Fri, 11 Dec 2015 15:34:42 -0500 Subject: [PATCH 237/502] Be consistent in our path relativization. --- src/git-repository-async.js | 38 +++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 4e900bc16..75c387b69 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -153,6 +153,21 @@ export default class GitRepositoryAsync { } // Public: Makes a path relative to the repository's working directory. + // + // * `path` The {String} path to relativize. + // + // Returns a {Promise} which resolves to the relative {String} path. + relativizeToWorkingDirectory (_path) { + return this.repoPromise + .then(repo => this.relativize(_path, repo.workdir())) + } + + // Public: Makes a path relative to the repository's working directory. + // + // * `path` The {String} path to relativize. + // * `workingDirectory` The {String} working directory path. + // + // Returns the relative {String} path. relativize (_path, workingDirectory) { // Cargo-culted from git-utils. The original implementation also handles // this.openedWorkingDirectory, which is set by git-utils when the @@ -229,10 +244,11 @@ export default class GitRepositoryAsync { isSubmodule (_path) { return this.repoPromise .then(repo => repo.openIndex()) - .then(index => { + .then(index => Promise.all([index, this.relativizeToWorkingDirectory(_path)])) + .then(([index, relativePath]) => { // TODO: This'll probably be wrong if the submodule doesn't exist in the // index yet? Is that a thing? - const entry = index.getByPath(_path) + const entry = index.getByPath(relativePath) if (!entry) return false return entry.mode === submoduleMode @@ -390,7 +406,10 @@ export default class GitRepositoryAsync { // is ignored. isPathIgnored (_path) { return this.repoPromise - .then(repo => Git.Ignore.pathIsIgnored(repo, _path)) + .then(repo => { + const relativePath = this.relativize(_path, repo.workdir()) + return Git.Ignore.pathIsIgnored(repo, relativePath) + }) .then(ignored => Boolean(ignored)) } @@ -464,8 +483,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to a status {Number} or null if the // path is not in the cache. getCachedPathStatus (_path) { - return this.repoPromise - .then(repo => this.relativize(_path, repo.workdir())) + return this.relativizeToWorkingDirectory(_path) .then(relativePath => this.pathStatusCache[relativePath]) } @@ -535,7 +553,7 @@ export default class GitRepositoryAsync { .then(([repo, headCommit]) => Promise.all([repo, headCommit.getTree()])) .then(([repo, tree]) => { const options = new Git.DiffOptions() - options.pathspec = _path + options.pathspec = this.relativize(_path, repo.workdir()) return Git.Diff.treeToWorkdir(repo, tree, options) }) .then(diff => this._getDiffLines(diff)) @@ -565,9 +583,13 @@ export default class GitRepositoryAsync { // * `oldLines` The {Number} of lines in the old hunk. // * `newLines` The {Number} of lines in the new hunk getLineDiffs (_path, text) { + let relativePath = null return this.repoPromise - .then(repo => repo.getHeadCommit()) - .then(commit => commit.getEntry(_path)) + .then(repo => { + relativePath = this.relativize(_path, repo.workdir()) + return repo.getHeadCommit() + }) + .then(commit => commit.getEntry(relativePath)) .then(entry => entry.getBlob()) .then(blob => { const options = new Git.DiffOptions() From cfb30c795dfaf9b35da0bdab76aeed08c5853cd5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 14 Dec 2015 15:04:54 +0100 Subject: [PATCH 238/502] Use an updated version of LineTopIndex --- spec/fake-lines-yardstick.coffee | 10 +- spec/line-top-index-spec.js | 189 ------------------------- spec/lines-yardstick-spec.coffee | 7 +- spec/text-editor-presenter-spec.coffee | 15 +- src/block-decorations-presenter.js | 19 +-- src/linear-line-top-index.js | 33 ++--- src/lines-yardstick.coffee | 12 +- src/text-editor-component.coffee | 5 +- src/text-editor-presenter.coffee | 24 ++-- 9 files changed, 56 insertions(+), 258 deletions(-) delete mode 100644 spec/line-top-index-spec.js diff --git a/spec/fake-lines-yardstick.coffee b/spec/fake-lines-yardstick.coffee index 3d2ebe7d5..a8a624970 100644 --- a/spec/fake-lines-yardstick.coffee +++ b/spec/fake-lines-yardstick.coffee @@ -31,7 +31,7 @@ class FakeLinesYardstick targetColumn = screenPosition.column baseCharacterWidth = @model.getDefaultCharWidth() - top = @lineTopIndex.bottomPixelPositionForRow(targetRow) + top = @lineTopIndex.pixelPositionForRow(targetRow) left = 0 column = 0 @@ -61,14 +61,14 @@ class FakeLinesYardstick {top, left} pixelRectForScreenRange: (screenRange) -> + top = @lineTopIndex.pixelPositionForRow(screenRange.start.row) if screenRange.end.row > screenRange.start.row - top = @pixelPositionForScreenPosition(screenRange.start).top left = 0 - height = @lineTopIndex.topPixelPositionForRow(screenRange.end.row + 1) - top + height = @lineTopIndex.pixelPositionForRow(screenRange.end.row) - top + @model.getLineHeightInPixels() width = @presenter.getScrollWidth() else - {top, left} = @pixelPositionForScreenPosition(screenRange.start, false) - height = @lineTopIndex.topPixelPositionForRow(screenRange.end.row + 1) - top + {left} = @pixelPositionForScreenPosition(screenRange.start, false) + height = @lineTopIndex.pixelPositionForRow(screenRange.end.row) - top + @model.getLineHeightInPixels() width = @pixelPositionForScreenPosition(screenRange.end, false).left - left {top, left, width, height} diff --git a/spec/line-top-index-spec.js b/spec/line-top-index-spec.js deleted file mode 100644 index 62d3e1839..000000000 --- a/spec/line-top-index-spec.js +++ /dev/null @@ -1,189 +0,0 @@ -/** @babel */ - -const LineTopIndex = require('../src/linear-line-top-index') - -describe("LineTopIndex", function () { - let lineTopIndex - - beforeEach(function () { - lineTopIndex = new LineTopIndex() - lineTopIndex.setDefaultLineHeight(10) - lineTopIndex.setMaxRow(12) - }) - - describe("::topPixelPositionForRow(row)", function () { - it("performs the simple math when there are no block decorations", function () { - expect(lineTopIndex.topPixelPositionForRow(0)).toBe(0) - expect(lineTopIndex.topPixelPositionForRow(4)).toBe(40) - expect(lineTopIndex.topPixelPositionForRow(5)).toBe(50) - expect(lineTopIndex.topPixelPositionForRow(12)).toBe(120) - expect(lineTopIndex.topPixelPositionForRow(13)).toBe(120) - expect(lineTopIndex.topPixelPositionForRow(14)).toBe(120) - - lineTopIndex.splice(0, 2, 3) - - expect(lineTopIndex.topPixelPositionForRow(0)).toBe(0) - expect(lineTopIndex.topPixelPositionForRow(4)).toBe(40) - expect(lineTopIndex.topPixelPositionForRow(5)).toBe(50) - expect(lineTopIndex.topPixelPositionForRow(12)).toBe(120) - expect(lineTopIndex.topPixelPositionForRow(13)).toBe(130) - expect(lineTopIndex.topPixelPositionForRow(14)).toBe(130) - }) - - it("takes into account inserted and removed blocks", function () { - let block1 = lineTopIndex.insertBlock(0, 10) - let block2 = lineTopIndex.insertBlock(3, 20) - let block3 = lineTopIndex.insertBlock(5, 20) - - expect(lineTopIndex.topPixelPositionForRow(0)).toBe(0) - expect(lineTopIndex.topPixelPositionForRow(1)).toBe(20) - expect(lineTopIndex.topPixelPositionForRow(2)).toBe(30) - expect(lineTopIndex.topPixelPositionForRow(3)).toBe(40) - expect(lineTopIndex.topPixelPositionForRow(4)).toBe(70) - expect(lineTopIndex.topPixelPositionForRow(5)).toBe(80) - expect(lineTopIndex.topPixelPositionForRow(6)).toBe(110) - - lineTopIndex.removeBlock(block1) - lineTopIndex.removeBlock(block3) - - expect(lineTopIndex.topPixelPositionForRow(0)).toBe(0) - expect(lineTopIndex.topPixelPositionForRow(1)).toBe(10) - expect(lineTopIndex.topPixelPositionForRow(2)).toBe(20) - expect(lineTopIndex.topPixelPositionForRow(3)).toBe(30) - expect(lineTopIndex.topPixelPositionForRow(4)).toBe(60) - expect(lineTopIndex.topPixelPositionForRow(5)).toBe(70) - expect(lineTopIndex.topPixelPositionForRow(6)).toBe(80) - }) - - it("moves blocks down/up when splicing regions", function () { - let block1 = lineTopIndex.insertBlock(3, 20) - let block2 = lineTopIndex.insertBlock(5, 30) - - lineTopIndex.splice(0, 0, 4) - - expect(lineTopIndex.topPixelPositionForRow(0)).toBe(0) - expect(lineTopIndex.topPixelPositionForRow(6)).toBe(60) - expect(lineTopIndex.topPixelPositionForRow(7)).toBe(70) - expect(lineTopIndex.topPixelPositionForRow(8)).toBe(100) - expect(lineTopIndex.topPixelPositionForRow(9)).toBe(110) - expect(lineTopIndex.topPixelPositionForRow(10)).toBe(150) - expect(lineTopIndex.topPixelPositionForRow(11)).toBe(160) - - lineTopIndex.splice(0, 6, 2) - - expect(lineTopIndex.topPixelPositionForRow(0)).toBe(0) - expect(lineTopIndex.topPixelPositionForRow(3)).toBe(30) - expect(lineTopIndex.topPixelPositionForRow(4)).toBe(60) - expect(lineTopIndex.topPixelPositionForRow(5)).toBe(70) - expect(lineTopIndex.topPixelPositionForRow(6)).toBe(110) - - lineTopIndex.splice(2, 4, 0) - - expect(lineTopIndex.topPixelPositionForRow(0)).toBe(0) - expect(lineTopIndex.topPixelPositionForRow(1)).toBe(10) - expect(lineTopIndex.topPixelPositionForRow(2)).toBe(20) - expect(lineTopIndex.topPixelPositionForRow(3)).toBe(80) - expect(lineTopIndex.topPixelPositionForRow(4)).toBe(90) - expect(lineTopIndex.topPixelPositionForRow(5)).toBe(100) - expect(lineTopIndex.topPixelPositionForRow(6)).toBe(110) - expect(lineTopIndex.topPixelPositionForRow(7)).toBe(120) - expect(lineTopIndex.topPixelPositionForRow(8)).toBe(130) - expect(lineTopIndex.topPixelPositionForRow(9)).toBe(130) - }) - }) - - describe("::rowForTopPixelPosition(top)", function () { - it("performs the simple math when there are no block decorations", function () { - expect(lineTopIndex.rowForTopPixelPosition(0)).toBe(0) - expect(lineTopIndex.rowForTopPixelPosition(40)).toBe(4) - expect(lineTopIndex.rowForTopPixelPosition(44)).toBe(4) - expect(lineTopIndex.rowForTopPixelPosition(46)).toBe(4) - expect(lineTopIndex.rowForTopPixelPosition(50)).toBe(5) - expect(lineTopIndex.rowForTopPixelPosition(120)).toBe(12) - expect(lineTopIndex.rowForTopPixelPosition(130)).toBe(12) - expect(lineTopIndex.rowForTopPixelPosition(140)).toBe(12) - expect(lineTopIndex.rowForTopPixelPosition(145)).toBe(12) - - lineTopIndex.splice(0, 2, 3) - - expect(lineTopIndex.rowForTopPixelPosition(0)).toBe(0) - expect(lineTopIndex.rowForTopPixelPosition(40)).toBe(4) - expect(lineTopIndex.rowForTopPixelPosition(50)).toBe(5) - expect(lineTopIndex.rowForTopPixelPosition(120)).toBe(12) - expect(lineTopIndex.rowForTopPixelPosition(130)).toBe(13) - expect(lineTopIndex.rowForTopPixelPosition(140)).toBe(13) - }) - - it("takes into account inserted and removed blocks", function () { - let block1 = lineTopIndex.insertBlock(0, 10) - let block2 = lineTopIndex.insertBlock(3, 20) - let block3 = lineTopIndex.insertBlock(5, 20) - - expect(lineTopIndex.rowForTopPixelPosition(0)).toBe(0) - expect(lineTopIndex.rowForTopPixelPosition(6)).toBe(0) - expect(lineTopIndex.rowForTopPixelPosition(10)).toBe(0) - expect(lineTopIndex.rowForTopPixelPosition(12)).toBe(0) - expect(lineTopIndex.rowForTopPixelPosition(17)).toBe(0) - expect(lineTopIndex.rowForTopPixelPosition(20)).toBe(1) - expect(lineTopIndex.rowForTopPixelPosition(30)).toBe(2) - expect(lineTopIndex.rowForTopPixelPosition(40)).toBe(3) - expect(lineTopIndex.rowForTopPixelPosition(70)).toBe(4) - expect(lineTopIndex.rowForTopPixelPosition(80)).toBe(5) - expect(lineTopIndex.rowForTopPixelPosition(90)).toBe(5) - expect(lineTopIndex.rowForTopPixelPosition(95)).toBe(5) - expect(lineTopIndex.rowForTopPixelPosition(106)).toBe(5) - expect(lineTopIndex.rowForTopPixelPosition(110)).toBe(6) - expect(lineTopIndex.rowForTopPixelPosition(160)).toBe(11) - expect(lineTopIndex.rowForTopPixelPosition(166)).toBe(11) - expect(lineTopIndex.rowForTopPixelPosition(170)).toBe(12) - expect(lineTopIndex.rowForTopPixelPosition(240)).toBe(12) - - lineTopIndex.removeBlock(block1) - lineTopIndex.removeBlock(block3) - - expect(lineTopIndex.rowForTopPixelPosition(0)).toBe(0) - expect(lineTopIndex.rowForTopPixelPosition(10)).toBe(1) - expect(lineTopIndex.rowForTopPixelPosition(20)).toBe(2) - expect(lineTopIndex.rowForTopPixelPosition(30)).toBe(3) - expect(lineTopIndex.rowForTopPixelPosition(60)).toBe(4) - expect(lineTopIndex.rowForTopPixelPosition(70)).toBe(5) - expect(lineTopIndex.rowForTopPixelPosition(80)).toBe(6) - }) - - it("moves blocks down/up when splicing regions", function () { - let block1 = lineTopIndex.insertBlock(3, 20) - let block2 = lineTopIndex.insertBlock(5, 30) - - lineTopIndex.splice(0, 0, 4) - - expect(lineTopIndex.rowForTopPixelPosition(0)).toBe(0) - expect(lineTopIndex.rowForTopPixelPosition(60)).toBe(6) - expect(lineTopIndex.rowForTopPixelPosition(70)).toBe(7) - expect(lineTopIndex.rowForTopPixelPosition(100)).toBe(8) - expect(lineTopIndex.rowForTopPixelPosition(110)).toBe(9) - expect(lineTopIndex.rowForTopPixelPosition(150)).toBe(10) - expect(lineTopIndex.rowForTopPixelPosition(160)).toBe(11) - - lineTopIndex.splice(0, 6, 2) - - expect(lineTopIndex.rowForTopPixelPosition(0)).toBe(0) - expect(lineTopIndex.rowForTopPixelPosition(30)).toBe(3) - expect(lineTopIndex.rowForTopPixelPosition(60)).toBe(4) - expect(lineTopIndex.rowForTopPixelPosition(70)).toBe(5) - expect(lineTopIndex.rowForTopPixelPosition(110)).toBe(6) - - lineTopIndex.splice(2, 4, 0) - - expect(lineTopIndex.rowForTopPixelPosition(0)).toBe(0) - expect(lineTopIndex.rowForTopPixelPosition(10)).toBe(1) - expect(lineTopIndex.rowForTopPixelPosition(20)).toBe(2) - expect(lineTopIndex.rowForTopPixelPosition(80)).toBe(3) - expect(lineTopIndex.rowForTopPixelPosition(90)).toBe(4) - expect(lineTopIndex.rowForTopPixelPosition(100)).toBe(5) - expect(lineTopIndex.rowForTopPixelPosition(110)).toBe(6) - expect(lineTopIndex.rowForTopPixelPosition(120)).toBe(7) - expect(lineTopIndex.rowForTopPixelPosition(130)).toBe(8) - expect(lineTopIndex.rowForTopPixelPosition(130)).toBe(8) - }) - }) -}) diff --git a/spec/lines-yardstick-spec.coffee b/spec/lines-yardstick-spec.coffee index 44267699e..791385d64 100644 --- a/spec/lines-yardstick-spec.coffee +++ b/spec/lines-yardstick-spec.coffee @@ -57,9 +57,10 @@ describe "LinesYardstick", -> textNodes editor.setLineHeightInPixels(14) - lineTopIndex = new LineTopIndex() - lineTopIndex.setDefaultLineHeight(editor.getLineHeightInPixels()) - lineTopIndex.setMaxRow(editor.getScreenLineCount()) + lineTopIndex = new LineTopIndex({ + maxRow: editor.getScreenLineCount(), + defaultLineHeight: editor.getLineHeightInPixels() + }) linesYardstick = new LinesYardstick(editor, mockPresenter, mockLineNodesProvider, lineTopIndex, atom.grammars) afterEach -> diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index fdcb8a418..87c32475d 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -27,7 +27,10 @@ describe "TextEditorPresenter", -> buffer.destroy() buildPresenterWithoutMeasurements = (params={}) -> - lineTopIndex = new LineTopIndex + lineTopIndex = new LineTopIndex({ + maxRow: editor.getScreenLineCount(), + defaultLineHeight: editor.getLineHeightInPixels() + }) _.defaults params, model: editor config: atom.config @@ -526,8 +529,6 @@ describe "TextEditorPresenter", -> presenter = buildPresenter(scrollTop: 0, lineHeight: 10) expect(presenter.getState().verticalScrollbar.scrollHeight).toBe editor.getScreenLineCount() * 10 - # Setting `null` as the DOM element, as it doesn't really matter here. - # Maybe a signal that we should separate models from views? blockDecoration1 = editor.addBlockDecorationForScreenRow(0, null) blockDecoration2 = editor.addBlockDecorationForScreenRow(3, null) blockDecoration3 = editor.addBlockDecorationForScreenRow(7, null) @@ -719,8 +720,6 @@ describe "TextEditorPresenter", -> presenter = buildPresenter(scrollTop: 0, lineHeight: 10) expect(presenter.getState().verticalScrollbar.scrollHeight).toBe editor.getScreenLineCount() * 10 - # Setting `null` as the DOM element, as it doesn't really matter here. - # Maybe a signal that we should separate models from views? blockDecoration1 = editor.addBlockDecorationForScreenRow(0, null) blockDecoration2 = editor.addBlockDecorationForScreenRow(3, null) blockDecoration3 = editor.addBlockDecorationForScreenRow(7, null) @@ -3041,8 +3040,8 @@ describe "TextEditorPresenter", -> presenter.setBlockDecorationDimensions(blockDecoration3, 0, 7) decorationState = getContentForGutterWithName(presenter, 'test-gutter') - expect(decorationState[decoration1.id].top).toBe lineHeight * marker1.getScreenRange().start.row - expect(decorationState[decoration1.id].height).toBe lineHeight * marker1.getScreenRange().getRowCount() + 3 + expect(decorationState[decoration1.id].top).toBe lineHeight * marker1.getScreenRange().start.row + 3 + expect(decorationState[decoration1.id].height).toBe lineHeight * marker1.getScreenRange().getRowCount() expect(decorationState[decoration1.id].item).toBe decorationItem expect(decorationState[decoration1.id].class).toBe 'test-class' expect(decorationState[decoration2.id].top).toBe lineHeight * marker2.getScreenRange().start.row + 3 + 5 @@ -3286,8 +3285,6 @@ describe "TextEditorPresenter", -> presenter = buildPresenter(scrollTop: 0, lineHeight: 10) expect(presenter.getState().verticalScrollbar.scrollHeight).toBe editor.getScreenLineCount() * 10 - # Setting `null` as the DOM element, as it doesn't really matter here. - # Maybe a signal that we should separate models from views? blockDecoration1 = editor.addBlockDecorationForScreenRow(0, null) blockDecoration2 = editor.addBlockDecorationForScreenRow(3, null) blockDecoration3 = editor.addBlockDecorationForScreenRow(7, null) diff --git a/src/block-decorations-presenter.js b/src/block-decorations-presenter.js index bfe08dd63..7f7258226 100644 --- a/src/block-decorations-presenter.js +++ b/src/block-decorations-presenter.js @@ -31,9 +31,6 @@ class BlockDecorationsPresenter { } observeModel () { - this.lineTopIndex.setMaxRow(this.model.getScreenLineCount()) - this.lineTopIndex.setDefaultLineHeight(this.model.getLineHeightInPixels()) - this.disposables.add(this.model.onDidAddDecoration(this.observeDecoration.bind(this))) this.disposables.add(this.model.onDidChange((changeEvent) => { let oldExtent = changeEvent.end - changeEvent.start @@ -54,11 +51,10 @@ class BlockDecorationsPresenter { setDimensionsForDecoration (decoration, width, height) { let block = this.blocksByDecoration.get(decoration) if (block) { - this.lineTopIndex.resizeBlock(block, height) + this.lineTopIndex.resizeBlock(decoration.getMarker().id, height) } else { this.observeDecoration(decoration) - block = this.blocksByDecoration.get(decoration) - this.lineTopIndex.resizeBlock(block, height) + this.lineTopIndex.resizeBlock(decoration.getMarker().id, height) } this.measuredDecorations.add(decoration) @@ -124,9 +120,9 @@ class BlockDecorationsPresenter { didAddDecoration (decoration) { let screenRow = decoration.getMarker().getHeadScreenPosition().row - let block = this.lineTopIndex.insertBlock(screenRow, 0) - this.decorationsByBlock.set(block, decoration) - this.blocksByDecoration.set(decoration, block) + this.lineTopIndex.insertBlock(decoration.getMarker().id, screenRow, 0) + this.decorationsByBlock.set(decoration.getMarker().id, decoration) + this.blocksByDecoration.set(decoration, decoration.getMarker().id) this.emitter.emit('did-update-state') } @@ -136,16 +132,15 @@ class BlockDecorationsPresenter { return } - let block = this.blocksByDecoration.get(decoration) let newScreenRow = decoration.getMarker().getHeadScreenPosition().row - this.lineTopIndex.moveBlock(block, newScreenRow) + this.lineTopIndex.moveBlock(decoration.getMarker().id, newScreenRow) this.emitter.emit('did-update-state') } didDestroyDecoration (decoration) { let block = this.blocksByDecoration.get(decoration) if (block) { - this.lineTopIndex.removeBlock(block) + this.lineTopIndex.removeBlock(decoration.getMarker().id) this.blocksByDecoration.delete(decoration) this.decorationsByBlock.delete(block) } diff --git a/src/linear-line-top-index.js b/src/linear-line-top-index.js index 3fe501ec9..aad947dd8 100644 --- a/src/linear-line-top-index.js +++ b/src/linear-line-top-index.js @@ -1,27 +1,23 @@ -'use strict' +"use babel" -module.exports = -class LineTopIndex { - constructor () { - this.idCounter = 1 +export default class LineTopIndex { + constructor (params = {}) { this.blocks = [] - this.maxRow = 0 - this.defaultLineHeight = 0 + this.maxRow = params.maxRow || 0 + this.setDefaultLineHeight(params.defaultLineHeight || 0) } setDefaultLineHeight (lineHeight) { this.defaultLineHeight = lineHeight } - setMaxRow (maxRow) { - this.maxRow = maxRow + getMaxRow () { + return this.maxRow } - insertBlock (row, height) { - let id = this.idCounter++ + insertBlock (id, row, height) { this.blocks.push({id, row, height}) this.blocks.sort((a, b) => a.row - b.row) - return id } resizeBlock (id, height) { @@ -62,26 +58,21 @@ class LineTopIndex { block.row += newExtent - oldExtent } else { block.row = startRow + newExtent - // invalidate marker? } } }) - this.setMaxRow(this.maxRow + newExtent - oldExtent) + this.maxRow = this.maxRow + newExtent - oldExtent } - topPixelPositionForRow (row) { + pixelPositionForRow (row) { row = Math.min(row, this.maxRow) let linesHeight = row * this.defaultLineHeight - let blocksHeight = this.blocks.filter((block) => block.row < row).reduce((a, b) => a + b.height, 0) + let blocksHeight = this.blocks.filter((block) => block.row <= row).reduce((a, b) => a + b.height, 0) return linesHeight + blocksHeight } - bottomPixelPositionForRow (row) { - return this.topPixelPositionForRow(row + 1) - this.defaultLineHeight - } - - rowForTopPixelPosition (top, strategy) { + rowForPixelPosition (top, strategy) { const roundingStrategy = strategy || 'floor' let blocksHeight = 0 let lastRow = 0 diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index 847ebfd46..e6454989a 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -22,7 +22,7 @@ class LinesYardstick targetTop = pixelPosition.top targetLeft = pixelPosition.left defaultCharWidth = @model.getDefaultCharWidth() - row = @lineTopIndex.rowForTopPixelPosition(targetTop, 'floor') + row = @lineTopIndex.rowForPixelPosition(targetTop, 'floor') targetLeft = 0 if targetTop < 0 targetLeft = Infinity if row > @model.getLastScreenRow() row = Math.min(row, @model.getLastScreenRow()) @@ -91,7 +91,7 @@ class LinesYardstick @prepareScreenRowsForMeasurement([targetRow]) unless measureVisibleLinesOnly - top = @lineTopIndex.bottomPixelPositionForRow(targetRow) + top = @lineTopIndex.pixelPositionForRow(targetRow) left = @leftPixelPositionForScreenPosition(targetRow, targetColumn) @clearScreenRowsForMeasurement() unless measureVisibleLinesOnly @@ -174,14 +174,14 @@ class LinesYardstick left + width - offset pixelRectForScreenRange: (screenRange, measureVisibleLinesOnly) -> + top = @lineTopIndex.pixelPositionForRow(screenRange.start.row) if screenRange.end.row > screenRange.start.row - top = @pixelPositionForScreenPosition(screenRange.start, true, measureVisibleLinesOnly).top left = 0 - height = @lineTopIndex.topPixelPositionForRow(screenRange.end.row + 1) - top + height = @lineTopIndex.pixelPositionForRow(screenRange.end.row) - top + @model.getLineHeightInPixels() width = @presenter.getScrollWidth() else - {top, left} = @pixelPositionForScreenPosition(screenRange.start, false, measureVisibleLinesOnly) - height = @lineTopIndex.topPixelPositionForRow(screenRange.end.row + 1) - top + {left} = @pixelPositionForScreenPosition(screenRange.start, false, measureVisibleLinesOnly) + height = @lineTopIndex.pixelPositionForRow(screenRange.end.row) - top + @model.getLineHeightInPixels() width = @pixelPositionForScreenPosition(screenRange.end, false, measureVisibleLinesOnly).left - left {top, left, width, height} diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index c35139d7c..3e644a084 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -50,7 +50,10 @@ class TextEditorComponent @observeConfig() @setScrollSensitivity(@config.get('editor.scrollSensitivity')) - lineTopIndex = new LineTopIndex(@editor) + lineTopIndex = new LineTopIndex({ + maxRow: @editor.getScreenLineCount(), + defaultLineHeight: @editor.getLineHeightInPixels() + }) @presenter = new TextEditorPresenter model: @editor tileSize: tileSize diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 5b1f01b35..ca05ea36a 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -442,8 +442,8 @@ class TextEditorPresenter continue if rowsWithinTile.length is 0 - top = Math.round(@lineTopIndex.topPixelPositionForRow(tileStartRow)) - bottom = Math.round(@lineTopIndex.topPixelPositionForRow(tileEndRow)) + top = Math.round(@lineTopIndex.pixelPositionForRow(tileStartRow - 1) + @lineHeight) + bottom = Math.round(@lineTopIndex.pixelPositionForRow(tileEndRow - 1) + @lineHeight) height = bottom - top tile = @state.content.tiles[tileStartRow] ?= {} @@ -659,8 +659,8 @@ class TextEditorPresenter continue unless @gutterIsVisible(gutter) for decorationId, {properties, screenRange} of @customGutterDecorationsByGutterName[gutterName] - top = @lineTopIndex.topPixelPositionForRow(screenRange.start.row) - bottom = @lineTopIndex.topPixelPositionForRow(screenRange.end.row + 1) + top = @lineTopIndex.pixelPositionForRow(screenRange.start.row) + bottom = @lineTopIndex.pixelPositionForRow(screenRange.end.row) + @lineHeight @customGutterDecorations[gutterName][decorationId] = top: top height: bottom - top @@ -724,12 +724,12 @@ class TextEditorPresenter updateStartRow: -> return unless @scrollTop? and @lineHeight? - @startRow = Math.max(0, @lineTopIndex.rowForTopPixelPosition(@scrollTop, "floor")) + @startRow = Math.max(0, @lineTopIndex.rowForPixelPosition(@scrollTop, "floor")) updateEndRow: -> return unless @scrollTop? and @lineHeight? and @height? - @endRow = @lineTopIndex.rowForTopPixelPosition(@scrollTop + @height + @lineHeight, 'ceil') + @endRow = @lineTopIndex.rowForPixelPosition(@scrollTop + @height + @lineHeight, 'ceil') updateRowsPerPage: -> rowsPerPage = Math.floor(@getClientHeight() / @lineHeight) @@ -761,7 +761,7 @@ class TextEditorPresenter updateVerticalDimensions: -> if @lineHeight? oldContentHeight = @contentHeight - @contentHeight = Math.round(@lineTopIndex.topPixelPositionForRow(Infinity)) + @contentHeight = Math.round(@lineTopIndex.pixelPositionForRow(Infinity)) if @contentHeight isnt oldContentHeight @updateHeight() @@ -1340,7 +1340,7 @@ class TextEditorPresenter screenRange.end.column = 0 repositionRegionWithinTile: (region, tileStartRow) -> - region.top += @scrollTop - @lineTopIndex.topPixelPositionForRow(tileStartRow) + region.top += @scrollTop - (@lineTopIndex.pixelPositionForRow(tileStartRow - 1) + @lineHeight) region.left += @scrollLeft buildHighlightRegions: (screenRange) -> @@ -1495,7 +1495,7 @@ class TextEditorPresenter @emitDidUpdateState() didChangeFirstVisibleScreenRow: (screenRow) -> - @updateScrollTop(@lineTopIndex.topPixelPositionForRow(screenRow)) + @updateScrollTop(@lineTopIndex.pixelPositionForRow(screenRow)) getVerticalScrollMarginInPixels: -> Math.round(@model.getVerticalScrollMargin() * @lineHeight) @@ -1516,8 +1516,8 @@ class TextEditorPresenter verticalScrollMarginInPixels = @getVerticalScrollMarginInPixels() - top = @lineTopIndex.bottomPixelPositionForRow(screenRange.start.row) - bottom = @lineTopIndex.bottomPixelPositionForRow(screenRange.end.row + 1) + top = @lineTopIndex.pixelPositionForRow(screenRange.start.row) + bottom = @lineTopIndex.pixelPositionForRow(screenRange.end.row) + @lineHeight if options?.center desiredScrollCenter = (top + bottom) / 2 @@ -1589,7 +1589,7 @@ class TextEditorPresenter restoreScrollTopIfNeeded: -> unless @scrollTop? - @updateScrollTop(@lineTopIndex.topPixelPositionForRow(@model.getFirstVisibleScreenRow())) + @updateScrollTop(@lineTopIndex.pixelPositionForRow(@model.getFirstVisibleScreenRow())) restoreScrollLeftIfNeeded: -> unless @scrollLeft? From 4b6a218bb91ff2338c14782714d88ad080b00e71 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 14 Dec 2015 15:53:01 +0100 Subject: [PATCH 239/502] Start to move stuff back into TextEditorPresenter --- spec/text-editor-presenter-spec.coffee | 24 ++++++--- src/block-decorations-presenter.js | 74 ++++++-------------------- src/text-editor-presenter.coffee | 28 +++++----- 3 files changed, 49 insertions(+), 77 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 87c32475d..1d1b214ec 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -1313,7 +1313,7 @@ describe "TextEditorPresenter", -> expect(lineStateForScreenRow(presenter, 6).blockDecorations).toEqual([]) expect(lineStateForScreenRow(presenter, 7).blockDecorations).toEqual([]) expect(lineStateForScreenRow(presenter, 8).blockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 9).blockDecorations).toEqual([blockDecoration3, blockDecoration2]) + expect(lineStateForScreenRow(presenter, 9).blockDecorations).toEqual([blockDecoration2, blockDecoration3]) expect(lineStateForScreenRow(presenter, 10).blockDecorations).toEqual([]) expect(lineStateForScreenRow(presenter, 11).blockDecorations).toEqual([]) expect(lineStateForScreenRow(presenter, 12).blockDecorations).toEqual([]) @@ -2166,12 +2166,12 @@ describe "TextEditorPresenter", -> expectValues stateForBlockDecoration(presenter, blockDecoration2), { decoration: blockDecoration2 screenRow: 4 - isVisible: true + isVisible: false } expectValues stateForBlockDecoration(presenter, blockDecoration3), { decoration: blockDecoration3 screenRow: 4 - isVisible: true + isVisible: false } expectValues stateForBlockDecoration(presenter, blockDecoration4), { decoration: blockDecoration4 @@ -2212,16 +2212,23 @@ describe "TextEditorPresenter", -> screenRow: 4 isVisible: false } - expectValues stateForBlockDecoration(presenter, blockDecoration4), { - decoration: blockDecoration4 - screenRow: 10 - isVisible: true - } + expect(stateForBlockDecoration(presenter, blockDecoration4)).toBeUndefined() presenter.invalidateBlockDecorationDimensions(blockDecoration1) presenter.setBlockDecorationDimensions(blockDecoration2, 0, 10) presenter.setBlockDecorationDimensions(blockDecoration3, 0, 10) + expectValues stateForBlockDecoration(presenter, blockDecoration1), { + decoration: blockDecoration1 + screenRow: 0 + isVisible: false + } + expect(stateForBlockDecoration(presenter, blockDecoration2)).toBeUndefined() + expect(stateForBlockDecoration(presenter, blockDecoration3)).toBeUndefined() + expect(stateForBlockDecoration(presenter, blockDecoration4)).toBeUndefined() + + presenter.setScrollTop(140) + expectValues stateForBlockDecoration(presenter, blockDecoration1), { decoration: blockDecoration1 screenRow: 0 @@ -2235,6 +2242,7 @@ describe "TextEditorPresenter", -> isVisible: true } + describe ".overlays", -> [item] = [] stateForOverlay = (presenter, decoration) -> diff --git a/src/block-decorations-presenter.js b/src/block-decorations-presenter.js index 7f7258226..076cfea12 100644 --- a/src/block-decorations-presenter.js +++ b/src/block-decorations-presenter.js @@ -8,10 +8,7 @@ class BlockDecorationsPresenter { this.model = model this.disposables = new EventKit.CompositeDisposable() this.emitter = new EventKit.Emitter() - this.firstUpdate = true this.lineTopIndex = lineTopIndex - this.blocksByDecoration = new Map() - this.decorationsByBlock = new Map() this.observedDecorations = new Set() this.measuredDecorations = new Set() @@ -31,30 +28,24 @@ class BlockDecorationsPresenter { } observeModel () { - this.disposables.add(this.model.onDidAddDecoration(this.observeDecoration.bind(this))) + this.disposables.add(this.model.onDidAddDecoration(this.didAddDecoration.bind(this))) this.disposables.add(this.model.onDidChange((changeEvent) => { let oldExtent = changeEvent.end - changeEvent.start let newExtent = Math.max(0, changeEvent.end - changeEvent.start + changeEvent.screenDelta) this.lineTopIndex.splice(changeEvent.start, oldExtent, newExtent) })) - } - update () { - if (this.firstUpdate) { - for (let decoration of this.model.getDecorations({type: 'block'})) { - this.observeDecoration(decoration) - } - this.firstUpdate = false + for (let decoration of this.model.getDecorations({type: 'block'})) { + this.didAddDecoration(decoration) } } setDimensionsForDecoration (decoration, width, height) { - let block = this.blocksByDecoration.get(decoration) - if (block) { - this.lineTopIndex.resizeBlock(decoration.getMarker().id, height) + if (this.observedDecorations.has(decoration)) { + this.lineTopIndex.resizeBlock(decoration.getId(), height) } else { - this.observeDecoration(decoration) - this.lineTopIndex.resizeBlock(decoration.getMarker().id, height) + this.didAddDecoration(decoration) + this.lineTopIndex.resizeBlock(decoration.getId(), height) } this.measuredDecorations.add(decoration) @@ -71,30 +62,7 @@ class BlockDecorationsPresenter { this.emitter.emit('did-update-state') } - decorationsForScreenRow (screenRow) { - let blocks = this.lineTopIndex.allBlocks().filter((block) => block.row === screenRow) - return blocks.map((block) => this.decorationsByBlock.get(block.id)).filter((decoration) => decoration) - } - - decorationsForScreenRowRange (startRow, endRow, mouseWheelScreenRow) { - let blocks = this.lineTopIndex.allBlocks() - let decorationsByScreenRow = new Map() - for (let block of blocks) { - let decoration = this.decorationsByBlock.get(block.id) - let hasntMeasuredDecoration = !this.measuredDecorations.has(decoration) - let isWithinVisibleRange = startRow <= block.row && block.row < endRow - let isVisible = isWithinVisibleRange || block.row === mouseWheelScreenRow - if (decoration && (isVisible || hasntMeasuredDecoration)) { - let decorations = decorationsByScreenRow.get(block.row) || [] - decorations.push({decoration, isVisible}) - decorationsByScreenRow.set(block.row, decorations) - } - } - - return decorationsByScreenRow - } - - observeDecoration (decoration) { + didAddDecoration (decoration) { if (!decoration.isType('block') || this.observedDecorations.has(decoration)) { return } @@ -108,21 +76,15 @@ class BlockDecorationsPresenter { this.disposables.remove(didDestroyDisposable) didMoveDisposable.dispose() didDestroyDisposable.dispose() - this.observedDecorations.delete(decoration) this.didDestroyDecoration(decoration) }) + let screenRow = decoration.getMarker().getHeadScreenPosition().row + this.lineTopIndex.insertBlock(decoration.getId(), screenRow, 0) + + this.observedDecorations.add(decoration) this.disposables.add(didMoveDisposable) this.disposables.add(didDestroyDisposable) - this.didAddDecoration(decoration) - this.observedDecorations.add(decoration) - } - - didAddDecoration (decoration) { - let screenRow = decoration.getMarker().getHeadScreenPosition().row - this.lineTopIndex.insertBlock(decoration.getMarker().id, screenRow, 0) - this.decorationsByBlock.set(decoration.getMarker().id, decoration) - this.blocksByDecoration.set(decoration, decoration.getMarker().id) this.emitter.emit('did-update-state') } @@ -133,17 +95,15 @@ class BlockDecorationsPresenter { } let newScreenRow = decoration.getMarker().getHeadScreenPosition().row - this.lineTopIndex.moveBlock(decoration.getMarker().id, newScreenRow) + this.lineTopIndex.moveBlock(decoration.getId(), newScreenRow) this.emitter.emit('did-update-state') } didDestroyDecoration (decoration) { - let block = this.blocksByDecoration.get(decoration) - if (block) { - this.lineTopIndex.removeBlock(decoration.getMarker().id) - this.blocksByDecoration.delete(decoration) - this.decorationsByBlock.delete(block) + if (this.observedDecorations.has(decoration)) { + this.lineTopIndex.removeBlock(decoration.getId()) + this.observedDecorations.delete(decoration) + this.emitter.emit('did-update-state') } - this.emitter.emit('did-update-state') } } diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index ca05ea36a..5a0630296 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -74,8 +74,6 @@ class TextEditorPresenter getPreMeasurementState: -> @updating = true - @blockDecorationsPresenter.update() - @updateVerticalDimensions() @updateScrollbarDimensions() @@ -87,11 +85,10 @@ class TextEditorPresenter @updateCommonGutterState() @updateReflowState() - @updateBlockDecorationsState() - if @shouldUpdateDecorations @fetchDecorations() @updateLineDecorations() + @updateBlockDecorations() if @shouldUpdateLinesState or @shouldUpdateLineNumbersState @updateTilesState() @@ -489,7 +486,7 @@ class TextEditorPresenter throw new Error("No line exists for row #{screenRow}. Last screen row: #{@model.getLastScreenRow()}") visibleLineIds[line.id] = true - blockDecorations = this.blockDecorationsPresenter.decorationsForScreenRow(screenRow) + blockDecorations = @blockDecorationsByScreenRow[screenRow] ? [] if tileState.lines.hasOwnProperty(line.id) lineState = tileState.lines[line.id] lineState.screenRow = screenRow @@ -943,6 +940,7 @@ class TextEditorPresenter @shouldUpdateLinesState = true @shouldUpdateLineNumbersState = true @shouldUpdateCustomGutterDecorationState = true + @shouldUpdateDecorations = true @emitDidUpdateState() @@ -1208,15 +1206,21 @@ class TextEditorPresenter return unless 0 <= @startRow <= @endRow <= Infinity @decorations = @model.decorationsStateForScreenRowRange(@startRow, @endRow - 1) - updateBlockDecorationsState: -> + updateBlockDecorations: -> @state.content.blockDecorations = {} + @blockDecorationsByScreenRow = {} - startRow = @getStartTileRow() - endRow = @getEndTileRow() + @tileSize - decorations = @blockDecorationsPresenter.decorationsForScreenRowRange(startRow, endRow, @mouseWheelScreenRow) - decorations.forEach (decorations, screenRow) => - for {decoration, isVisible} in decorations - @state.content.blockDecorations[decoration.id] = {decoration, screenRow, isVisible} + for decoration in @model.getDecorations({type: "block"}) + screenRow = decoration.getMarker().getHeadScreenPosition().row + @updateBlockDecorationState(decoration, screenRow) + @blockDecorationsByScreenRow[screenRow] ?= [] + @blockDecorationsByScreenRow[screenRow].push(decoration) + + updateBlockDecorationState: (decoration, screenRow) -> + hasntMeasuredDecoration = !@blockDecorationsPresenter.measuredDecorations.has(decoration) + isVisible = @startRow <= screenRow < @endRow || screenRow is @mouseWheelScreenRow + if isVisible or hasntMeasuredDecoration + @state.content.blockDecorations[decoration.id] = {decoration, screenRow, isVisible} updateLineDecorations: -> @lineDecorationsByScreenRow = {} From 27afc76455aeeb0900dc4e64d0cded44e5dc0203 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 14 Dec 2015 20:27:10 -0500 Subject: [PATCH 240/502] Fix hunk callback position. --- src/git-repository-async.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 75c387b69..807fadb1f 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -727,7 +727,7 @@ export default class GitRepositoryAsync { const hunkCallback = (delta, hunk, payload) => { hunks.push(hunk) } - return Git.Diff.blobToBuffer(blob, null, buffer, null, null, options, null, null, hunkCallback, null, null) + return Git.Diff.blobToBuffer(blob, null, buffer, null, options, null, null, hunkCallback, null) .then(_ => hunks) } From c4ba2132fa90704c6654a5d25d34aad30a459995 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 14 Dec 2015 23:29:01 -0500 Subject: [PATCH 241/502] Accumulate the raw data we want instead of keeping hunks around. --- src/git-repository-async.js | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 807fadb1f..5372c7a44 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -601,18 +601,6 @@ export default class GitRepositoryAsync { } return this._diffBlobToBuffer(blob, text, options) }) - .then(hunks => { - // TODO: The old implementation just takes into account the last hunk. - // That's probably safe for most cases but maybe wrong in some edge - // cases? - const hunk = hunks[hunks.length - 1] - return { - oldStart: hunk.oldStart(), - newStart: hunk.newStart(), - oldLines: hunk.oldLines(), - newLines: hunk.newLines() - } - }) } // Checking Out @@ -721,12 +709,23 @@ export default class GitRepositoryAsync { // * `buffer` The {String} buffer. // * `options` The {NodeGit.DiffOptions} // - // Returns a {Promise} which resolves to an {Array} of {NodeGit.Hunk}. + // Returns a {Promise} which resolves to an {Array} of {Object}s which have + // the following keys: + // * `oldStart` The {Number} of the old starting line. + // * `newStart` The {Number} of the new starting line. + // * `oldLines` The {Number} of old lines. + // * `newLines` The {Number} of new lines. _diffBlobToBuffer (blob, buffer, options) { const hunks = [] const hunkCallback = (delta, hunk, payload) => { - hunks.push(hunk) + hunks.push({ + oldStart: hunk.oldStart(), + newStart: hunk.newStart(), + oldLines: hunk.oldLines(), + newLines: hunk.newLines() + }) } + return Git.Diff.blobToBuffer(blob, null, buffer, null, options, null, null, hunkCallback, null) .then(_ => hunks) } From 36f13e603692d04db65db7422c17be1c448e8c03 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 14 Dec 2015 23:29:08 -0500 Subject: [PATCH 242/502] #nocontext --- src/git-repository-async.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 5372c7a44..b26e2f097 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -593,6 +593,7 @@ export default class GitRepositoryAsync { .then(entry => entry.getBlob()) .then(blob => { const options = new Git.DiffOptions() + options.contextLines = 0 if (process.platform === 'win32') { // Ignore eol of line differences on windows so that files checked in // as LF don't report every line modified when the text contains CRLF From 13f220d285f7e1cd04fd1cccf3a641643c9572f5 Mon Sep 17 00:00:00 2001 From: Lee Dohm Date: Mon, 14 Dec 2015 20:30:34 -0800 Subject: [PATCH 243/502] :memo: Linkify all docs mentions of Promises --- src/default-directory-provider.coffee | 2 +- src/project.coffee | 4 ++-- src/workspace.coffee | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/default-directory-provider.coffee b/src/default-directory-provider.coffee index da2d17593..6b05a582f 100644 --- a/src/default-directory-provider.coffee +++ b/src/default-directory-provider.coffee @@ -37,7 +37,7 @@ class DefaultDirectoryProvider # * `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: + # 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) -> diff --git a/src/project.coffee b/src/project.coffee index d59c041cb..eebab7079 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -329,7 +329,7 @@ class Project extends Model # # * `filePath` A {String} representing a path. If `null`, an "Untitled" buffer is created. # - # Returns a promise that resolves to the {TextBuffer}. + # Returns a {Promise} that resolves to the {TextBuffer}. bufferForPath: (absoluteFilePath) -> existingBuffer = @findBufferForPath(absoluteFilePath) if absoluteFilePath? if existingBuffer @@ -349,7 +349,7 @@ class Project extends Model # * `absoluteFilePath` A {String} representing a path. # * `text` The {String} text to use as a buffer. # - # Returns a promise that resolves to the {TextBuffer}. + # Returns a {Promise} that resolves to the {TextBuffer}. buildBuffer: (absoluteFilePath) -> buffer = new TextBuffer({filePath: absoluteFilePath}) @addBuffer(buffer) diff --git a/src/workspace.coffee b/src/workspace.coffee index f64f58ee0..41838d8bf 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -403,7 +403,7 @@ class Workspace extends Model # If `false`, only the active pane will be searched for # an existing item for the same URI. Defaults to `false`. # - # Returns a promise that resolves to the {TextEditor} for the file URI. + # Returns a {Promise} that resolves to the {TextEditor} for the file URI. open: (uri, options={}) -> searchAllPanes = options.searchAllPanes split = options.split @@ -544,7 +544,7 @@ class Workspace extends Model # Public: Asynchronously reopens the last-closed item's URI if it hasn't already been # reopened. # - # Returns a promise that is resolved when the item is opened + # Returns a {Promise} that is resolved when the item is opened reopenItem: -> if uri = @destroyedItemURIs.pop() @open(uri) @@ -881,7 +881,7 @@ class Workspace extends Model # with number of paths searched. # * `iterator` {Function} callback on each file found. # - # Returns a `Promise` with a `cancel()` method that will cancel all + # Returns a {Promise} with a `cancel()` method that will cancel all # of the underlying searches that were started as part of this scan. scan: (regex, options={}, iterator) -> if _.isFunction(options) @@ -984,7 +984,7 @@ class Workspace extends Model # * `iterator` A {Function} callback on each file with replacements: # * `options` {Object} with keys `filePath` and `replacements`. # - # Returns a `Promise`. + # Returns a {Promise}. replace: (regex, replacementText, filePaths, iterator) -> new Promise (resolve, reject) => openPaths = (buffer.getPath() for buffer in @project.getBuffers()) From 4024ea956c72f5239545a0485ab6d4d4fd039ebe Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 14 Dec 2015 23:58:04 -0500 Subject: [PATCH 244/502] Fix the test. --- spec/git-repository-async-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 5d157dfc8..e40c0a114 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -610,7 +610,7 @@ describe('GitRepositoryAsync', () => { }) it('returns the old and new lines of the diff', async () => { - const {oldStart, newStart, oldLines, newLines} = await repo.getLineDiffs('a.txt', 'hi there') + const [{oldStart, newStart, oldLines, newLines}] = await repo.getLineDiffs('a.txt', 'hi there') expect(oldStart).toBe(0) expect(oldLines).toBe(0) expect(newStart).toBe(1) From 1c01d05440e42c84c300a80066c126ae03f0effa Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 15 Dec 2015 09:00:52 -0500 Subject: [PATCH 245/502] Try running core specs on appveyor. --- appveyor.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/appveyor.yml b/appveyor.yml index 8e33549c7..e63b9eceb 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -18,6 +18,7 @@ environment: install: - SET PATH=C:\Program Files\Atom\resources\cli;%PATH% + - SET ATOM_SPECS_TASK=core - ps: Install-Product node $env:NODE_VERSION $env:PLATFORM build_script: From d98bbb1119fef552b182476e0e31019d047e031b Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 15 Dec 2015 10:09:20 -0500 Subject: [PATCH 246/502] Enable tests on windows. --- build/Gruntfile.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/Gruntfile.coffee b/build/Gruntfile.coffee index 26d9c2f42..62964a3bd 100644 --- a/build/Gruntfile.coffee +++ b/build/Gruntfile.coffee @@ -290,7 +290,7 @@ module.exports = (grunt) -> ciTasks.push('mkdeb') if process.platform is 'linux' ciTasks.push('codesign:exe') if process.platform is 'win32' and not process.env.CI ciTasks.push('create-windows-installer:installer') if process.platform is 'win32' - ciTasks.push('test') if process.platform is 'darwin' + ciTasks.push('test') if process.platform is 'darwin' or process.platform is 'win32' ciTasks.push('codesign:installer') if process.platform is 'win32' and not process.env.CI ciTasks.push('codesign:app') if process.platform is 'darwin' and not process.env.CI ciTasks.push('publish-build') unless process.env.CI From 013fcf16f2bb4cce2797e43971ecff76043faf69 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 15 Dec 2015 17:14:19 -0500 Subject: [PATCH 247/502] Revert "Enable tests on windows." This reverts commit d98bbb1119fef552b182476e0e31019d047e031b. --- build/Gruntfile.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/Gruntfile.coffee b/build/Gruntfile.coffee index 62964a3bd..26d9c2f42 100644 --- a/build/Gruntfile.coffee +++ b/build/Gruntfile.coffee @@ -290,7 +290,7 @@ module.exports = (grunt) -> ciTasks.push('mkdeb') if process.platform is 'linux' ciTasks.push('codesign:exe') if process.platform is 'win32' and not process.env.CI ciTasks.push('create-windows-installer:installer') if process.platform is 'win32' - ciTasks.push('test') if process.platform is 'darwin' or process.platform is 'win32' + ciTasks.push('test') if process.platform is 'darwin' ciTasks.push('codesign:installer') if process.platform is 'win32' and not process.env.CI ciTasks.push('codesign:app') if process.platform is 'darwin' and not process.env.CI ciTasks.push('publish-build') unless process.env.CI From 3f6447774082535a505844365522113f8d2ac205 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 15 Dec 2015 17:14:22 -0500 Subject: [PATCH 248/502] Revert "Try running core specs on appveyor." This reverts commit 1c01d05440e42c84c300a80066c126ae03f0effa. --- appveyor.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index e63b9eceb..8e33549c7 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -18,7 +18,6 @@ environment: install: - SET PATH=C:\Program Files\Atom\resources\cli;%PATH% - - SET ATOM_SPECS_TASK=core - ps: Install-Product node $env:NODE_VERSION $env:PLATFORM build_script: From 5c77effc207fbe383a95b4b01c28a491d5e67039 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 15 Dec 2015 17:23:23 -0500 Subject: [PATCH 249/502] Always set the architecture to ia32 on Windows. --- script/bootstrap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/bootstrap b/script/bootstrap index 8314b9cb0..7e5b534c7 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -82,7 +82,7 @@ function bootstrap() { // apm ships with 32-bit node so make sure its native modules are compiled // for a 32-bit target architecture - if (process.env.JANKY_SHA1 && process.platform === 'win32') + if (process.platform === 'win32') apmInstallCommand += ' --arch=ia32'; var commands = [ From 3256c8b5039302ca15ae55cbdde73cc1a2a6934f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Dec 2015 10:06:04 +0100 Subject: [PATCH 250/502] :green_heart: Adjust TextEditorComponent specs for block decorations --- spec/text-editor-component-spec.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 86f924116..488c6df3a 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -1664,7 +1664,7 @@ describe('TextEditorComponent', function () { }) it("renders visible and yet-to-be-measured block decorations, inserting them between the appropriate lines and refreshing them as needed", async function () { - wrapperNode.style.height = 9 * lineHeightInPixels + 'px' + wrapperNode.style.height = 13 * lineHeightInPixels + 'px' component.measureDimensions() await nextViewUpdatePromise() @@ -1706,7 +1706,7 @@ describe('TextEditorComponent', function () { await nextAnimationFramePromise() - expect(component.getDomNode().querySelectorAll(".line").length).toBe(7) + expect(component.getDomNode().querySelectorAll(".line").length).toBe(9) expect(component.tileNodesForLines()[0].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + "px") expect(component.tileNodesForLines()[0].style.webkitTransform).toBe("translate3d(0px, 0px, 0px)") @@ -1731,7 +1731,7 @@ describe('TextEditorComponent', function () { await nextAnimationFramePromise() // causes the DOM to update and to retrieve new styles await nextAnimationFramePromise() // applies the changes - expect(component.getDomNode().querySelectorAll(".line").length).toBe(7) + expect(component.getDomNode().querySelectorAll(".line").length).toBe(9) expect(component.tileNodesForLines()[0].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + "px") expect(component.tileNodesForLines()[0].style.webkitTransform).toBe("translate3d(0px, 0px, 0px)") @@ -1753,7 +1753,7 @@ describe('TextEditorComponent', function () { await nextAnimationFramePromise() await nextAnimationFramePromise() - expect(component.getDomNode().querySelectorAll(".line").length).toBe(7) + expect(component.getDomNode().querySelectorAll(".line").length).toBe(9) expect(component.tileNodesForLines()[0].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + "px") expect(component.tileNodesForLines()[0].style.webkitTransform).toBe("translate3d(0px, 0px, 0px)") From 6a403e441eb943454f2d1aba0a46dd965a168b28 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Dec 2015 10:41:21 +0100 Subject: [PATCH 251/502] Start integrating tree-based LineTopIndex --- package.json | 1 + spec/lines-yardstick-spec.coffee | 2 +- spec/text-editor-presenter-spec.coffee | 2 +- src/block-decorations-presenter.js | 14 ++-- src/linear-line-top-index.js | 107 ------------------------- src/text-editor-component.coffee | 2 +- src/text-editor-presenter.coffee | 12 ++- 7 files changed, 18 insertions(+), 122 deletions(-) delete mode 100644 src/linear-line-top-index.js diff --git a/package.json b/package.json index 21e960477..63953b1b6 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "jquery": "^2.1.1", "key-path-helpers": "^0.4.0", "less-cache": "0.22", + "line-top-index": "0.1.0", "marked": "^0.3.4", "normalize-package-data": "^2.0.0", "nslog": "^3", diff --git a/spec/lines-yardstick-spec.coffee b/spec/lines-yardstick-spec.coffee index 0120b4ef4..ceae2a847 100644 --- a/spec/lines-yardstick-spec.coffee +++ b/spec/lines-yardstick-spec.coffee @@ -1,5 +1,5 @@ LinesYardstick = require '../src/lines-yardstick' -LineTopIndex = require '../src/linear-line-top-index' +LineTopIndex = require 'line-top-index' {toArray} = require 'underscore-plus' describe "LinesYardstick", -> diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 0014d8cce..9bd85c7a9 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -5,7 +5,7 @@ TextBuffer = require 'text-buffer' TextEditor = require '../src/text-editor' TextEditorPresenter = require '../src/text-editor-presenter' FakeLinesYardstick = require './fake-lines-yardstick' -LineTopIndex = require '../src/linear-line-top-index' +LineTopIndex = require 'line-top-index' describe "TextEditorPresenter", -> # These `describe` and `it` blocks mirror the structure of the ::state object. diff --git a/src/block-decorations-presenter.js b/src/block-decorations-presenter.js index 076cfea12..870d49dcd 100644 --- a/src/block-decorations-presenter.js +++ b/src/block-decorations-presenter.js @@ -29,10 +29,10 @@ class BlockDecorationsPresenter { observeModel () { this.disposables.add(this.model.onDidAddDecoration(this.didAddDecoration.bind(this))) - this.disposables.add(this.model.onDidChange((changeEvent) => { - let oldExtent = changeEvent.end - changeEvent.start - let newExtent = Math.max(0, changeEvent.end - changeEvent.start + changeEvent.screenDelta) - this.lineTopIndex.splice(changeEvent.start, oldExtent, newExtent) + this.disposables.add(this.model.buffer.onDidChange((changeEvent) => { + let oldExtent = changeEvent.oldRange.getExtent() + let newExtent = changeEvent.newRange.getExtent() + this.lineTopIndex.splice(changeEvent.oldRange.start, oldExtent, newExtent) })) for (let decoration of this.model.getDecorations({type: 'block'})) { @@ -79,8 +79,7 @@ class BlockDecorationsPresenter { this.didDestroyDecoration(decoration) }) - let screenRow = decoration.getMarker().getHeadScreenPosition().row - this.lineTopIndex.insertBlock(decoration.getId(), screenRow, 0) + this.lineTopIndex.insertBlock(decoration.getId(), decoration.getMarker().getHeadBufferPosition(), true, 0) this.observedDecorations.add(decoration) this.disposables.add(didMoveDisposable) @@ -94,8 +93,7 @@ class BlockDecorationsPresenter { return } - let newScreenRow = decoration.getMarker().getHeadScreenPosition().row - this.lineTopIndex.moveBlock(decoration.getId(), newScreenRow) + this.lineTopIndex.moveBlock(decoration.getId(), decoration.getMarker().getHeadBufferPosition()) this.emitter.emit('did-update-state') } diff --git a/src/linear-line-top-index.js b/src/linear-line-top-index.js deleted file mode 100644 index aad947dd8..000000000 --- a/src/linear-line-top-index.js +++ /dev/null @@ -1,107 +0,0 @@ -"use babel" - -export default class LineTopIndex { - constructor (params = {}) { - this.blocks = [] - this.maxRow = params.maxRow || 0 - this.setDefaultLineHeight(params.defaultLineHeight || 0) - } - - setDefaultLineHeight (lineHeight) { - this.defaultLineHeight = lineHeight - } - - getMaxRow () { - return this.maxRow - } - - insertBlock (id, row, height) { - this.blocks.push({id, row, height}) - this.blocks.sort((a, b) => a.row - b.row) - } - - resizeBlock (id, height) { - let block = this.blocks.find((block) => block.id === id) - if (block) { - block.height = height - } - } - - moveBlock (id, newRow) { - let block = this.blocks.find((block) => block.id === id) - if (block) { - block.row = newRow - this.blocks.sort((a, b) => a.row - b.row) - } - } - - removeBlock (id) { - let index = this.blocks.findIndex((block) => block.id === id) - if (index !== -1) { - this.blocks.splice(index, 1) - } - } - - allBlocks () { - return this.blocks - } - - blocksHeightForRow (row) { - let blocksForRow = this.blocks.filter((block) => block.row === row) - return blocksForRow.reduce((a, b) => a + b.height, 0) - } - - splice (startRow, oldExtent, newExtent) { - this.blocks.forEach(function (block) { - if (block.row >= startRow) { - if (block.row >= startRow + oldExtent) { - block.row += newExtent - oldExtent - } else { - block.row = startRow + newExtent - } - } - }) - - this.maxRow = this.maxRow + newExtent - oldExtent - } - - pixelPositionForRow (row) { - row = Math.min(row, this.maxRow) - let linesHeight = row * this.defaultLineHeight - let blocksHeight = this.blocks.filter((block) => block.row <= row).reduce((a, b) => a + b.height, 0) - return linesHeight + blocksHeight - } - - rowForPixelPosition (top, strategy) { - const roundingStrategy = strategy || 'floor' - let blocksHeight = 0 - let lastRow = 0 - let lastTop = 0 - for (let block of this.blocks) { - let nextBlocksHeight = blocksHeight + block.height - let linesHeight = block.row * this.defaultLineHeight - if (nextBlocksHeight + linesHeight > top) { - while (lastRow < block.row && lastTop + this.defaultLineHeight <= top) { - lastTop += this.defaultLineHeight - lastRow++ - } - return lastRow - } else { - blocksHeight = nextBlocksHeight - lastRow = block.row - lastTop = blocksHeight + linesHeight - } - } - - let remainingHeight = Math.max(0, top - lastTop) - let remainingRows = Math.min(this.maxRow, lastRow + remainingHeight / this.defaultLineHeight) - switch (roundingStrategy) { - case 'floor': - return Math.floor(remainingRows) - case 'ceil': - return Math.ceil(remainingRows) - default: - throw new Error(`Cannot use '${roundingStrategy}' as a rounding strategy!`) - } - } -} diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 531269366..266102cdd 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -14,7 +14,7 @@ OverlayManager = require './overlay-manager' DOMElementPool = require './dom-element-pool' LinesYardstick = require './lines-yardstick' BlockDecorationsComponent = require './block-decorations-component' -LineTopIndex = require './linear-line-top-index' +LineTopIndex = require 'line-top-index' module.exports = class TextEditorComponent diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index de98aa038..6c90c4f27 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -617,7 +617,8 @@ class TextEditorPresenter line = @model.tokenizedLineForScreenRow(screenRow) decorationClasses = @lineNumberDecorationClassesForRow(screenRow) foldable = @model.isFoldableAtScreenRow(screenRow) - blockDecorationsHeight = @lineTopIndex.blocksHeightForRow(screenRow) + previousRowBottomPixelPosition = @lineTopIndex.pixelPositionForRow(screenRow - 1) + @lineHeight + blockDecorationsHeight = @lineTopIndex.pixelPositionForRow(screenRow) - previousRowBottomPixelPosition tileState.lineNumbers[line.id] = {screenRow, bufferRow, softWrapped, decorationClasses, foldable, blockDecorationsHeight} visibleLineNumberIds[line.id] = true @@ -630,12 +631,15 @@ class TextEditorPresenter updateStartRow: -> return unless @scrollTop? and @lineHeight? - @startRow = Math.max(0, @lineTopIndex.rowForPixelPosition(@scrollTop, "floor")) + @startRow = Math.max(0, @lineTopIndex.rowForPixelPosition(@scrollTop)) updateEndRow: -> return unless @scrollTop? and @lineHeight? and @height? - @endRow = @lineTopIndex.rowForPixelPosition(@scrollTop + @height + @lineHeight, 'ceil') + @endRow = Math.min( + @model.getScreenLineCount(), + @lineTopIndex.rowForPixelPosition(@scrollTop + @height + @lineHeight) + ) updateRowsPerPage: -> rowsPerPage = Math.floor(@getClientHeight() / @lineHeight) @@ -667,7 +671,7 @@ class TextEditorPresenter updateVerticalDimensions: -> if @lineHeight? oldContentHeight = @contentHeight - @contentHeight = Math.round(@lineTopIndex.pixelPositionForRow(Infinity)) + @contentHeight = Math.round(@lineTopIndex.pixelPositionForRow(@model.getScreenLineCount())) if @contentHeight isnt oldContentHeight @updateHeight() From 877eea3bd0d2779fa60ee756e2ff66145ed52c84 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Dec 2015 11:07:10 +0100 Subject: [PATCH 252/502] Finish integrating LineTopIndex --- spec/text-editor-component-spec.js | 9 +++++---- spec/text-editor-presenter-spec.coffee | 23 ++++++++--------------- src/text-editor-presenter.coffee | 6 ++++-- 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 488c6df3a..2a686cf0c 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -1664,7 +1664,7 @@ describe('TextEditorComponent', function () { }) it("renders visible and yet-to-be-measured block decorations, inserting them between the appropriate lines and refreshing them as needed", async function () { - wrapperNode.style.height = 13 * lineHeightInPixels + 'px' + wrapperNode.style.height = 9 * lineHeightInPixels + 'px' component.measureDimensions() await nextViewUpdatePromise() @@ -1706,7 +1706,7 @@ describe('TextEditorComponent', function () { await nextAnimationFramePromise() - expect(component.getDomNode().querySelectorAll(".line").length).toBe(9) + expect(component.getDomNode().querySelectorAll(".line").length).toBe(7) expect(component.tileNodesForLines()[0].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + "px") expect(component.tileNodesForLines()[0].style.webkitTransform).toBe("translate3d(0px, 0px, 0px)") @@ -1731,7 +1731,7 @@ describe('TextEditorComponent', function () { await nextAnimationFramePromise() // causes the DOM to update and to retrieve new styles await nextAnimationFramePromise() // applies the changes - expect(component.getDomNode().querySelectorAll(".line").length).toBe(9) + expect(component.getDomNode().querySelectorAll(".line").length).toBe(7) expect(component.tileNodesForLines()[0].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + "px") expect(component.tileNodesForLines()[0].style.webkitTransform).toBe("translate3d(0px, 0px, 0px)") @@ -1765,10 +1765,11 @@ describe('TextEditorComponent', function () { expect(component.getTopmostDOMNode().querySelector(".decoration-1")).toBeNull() expect(component.getTopmostDOMNode().querySelector(".decoration-2")).toBe(item2) expect(component.getTopmostDOMNode().querySelector(".decoration-3")).toBe(item3) - expect(component.getTopmostDOMNode().querySelector(".decoration-4")).toBeNull() + expect(component.getTopmostDOMNode().querySelector(".decoration-4")).toBe(item4) expect(item2.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 3) expect(item3.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 5 + 20) + expect(item4.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 8 + 20 + 100) }) it("correctly sets screen rows on elements, both initially and when decorations move", async function () { diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 9bd85c7a9..98285c7b2 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -2150,12 +2150,12 @@ describe "TextEditorPresenter", -> expectValues stateForBlockDecoration(presenter, blockDecoration2), { decoration: blockDecoration2 screenRow: 4 - isVisible: false + isVisible: true } expectValues stateForBlockDecoration(presenter, blockDecoration3), { decoration: blockDecoration3 screenRow: 4 - isVisible: false + isVisible: true } expectValues stateForBlockDecoration(presenter, blockDecoration4), { decoration: blockDecoration4 @@ -2196,23 +2196,16 @@ describe "TextEditorPresenter", -> screenRow: 4 isVisible: false } - expect(stateForBlockDecoration(presenter, blockDecoration4)).toBeUndefined() + expectValues stateForBlockDecoration(presenter, blockDecoration4), { + decoration: blockDecoration4 + screenRow: 10 + isVisible: true + } presenter.invalidateBlockDecorationDimensions(blockDecoration1) presenter.setBlockDecorationDimensions(blockDecoration2, 0, 10) presenter.setBlockDecorationDimensions(blockDecoration3, 0, 10) - expectValues stateForBlockDecoration(presenter, blockDecoration1), { - decoration: blockDecoration1 - screenRow: 0 - isVisible: false - } - expect(stateForBlockDecoration(presenter, blockDecoration2)).toBeUndefined() - expect(stateForBlockDecoration(presenter, blockDecoration3)).toBeUndefined() - expect(stateForBlockDecoration(presenter, blockDecoration4)).toBeUndefined() - - presenter.setScrollTop(140) - expectValues stateForBlockDecoration(presenter, blockDecoration1), { decoration: blockDecoration1 screenRow: 0 @@ -2226,7 +2219,6 @@ describe "TextEditorPresenter", -> isVisible: true } - describe ".overlays", -> [item] = [] stateForOverlay = (presenter, decoration) -> @@ -3091,6 +3083,7 @@ describe "TextEditorPresenter", -> expectStateUpdate presenter, -> presenter.setLineHeight(Math.ceil(1.0 * explicitHeight / marker3.getBufferRange().end.row)) decorationState = getContentForGutterWithName(presenter, 'test-gutter') + expect(decorationState[decoration1.id].top).toBeDefined() expect(decorationState[decoration2.id].top).toBeDefined() expect(decorationState[decoration3.id].top).toBeDefined() diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 6c90c4f27..0b8bfbbc9 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -638,7 +638,7 @@ class TextEditorPresenter @endRow = Math.min( @model.getScreenLineCount(), - @lineTopIndex.rowForPixelPosition(@scrollTop + @height + @lineHeight) + @lineTopIndex.rowForPixelPosition(@scrollTop + @height + (2 * @lineHeight - 1)) ) updateRowsPerPage: -> @@ -1059,8 +1059,10 @@ class TextEditorPresenter @blockDecorationsByScreenRow[screenRow].push(decoration) updateBlockDecorationState: (decoration, screenRow) -> + startRow = @getStartTileRow() + endRow = @getEndTileRow() + @tileSize hasntMeasuredDecoration = !@blockDecorationsPresenter.measuredDecorations.has(decoration) - isVisible = @startRow <= screenRow < @endRow || screenRow is @mouseWheelScreenRow + isVisible = startRow <= screenRow < endRow || screenRow is @mouseWheelScreenRow if isVisible or hasntMeasuredDecoration @state.content.blockDecorations[decoration.id] = {decoration, screenRow, isVisible} From da412e92f8832d6397c0210fd91a0192857a0857 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Dec 2015 11:54:10 +0100 Subject: [PATCH 253/502] :fire: Remove BlockDecorationsPresenter --- src/block-decorations-presenter.js | 107 ----------------------------- src/text-editor-presenter.coffee | 88 +++++++++++++++++------- 2 files changed, 65 insertions(+), 130 deletions(-) delete mode 100644 src/block-decorations-presenter.js diff --git a/src/block-decorations-presenter.js b/src/block-decorations-presenter.js deleted file mode 100644 index 870d49dcd..000000000 --- a/src/block-decorations-presenter.js +++ /dev/null @@ -1,107 +0,0 @@ -'use strict' - -const EventKit = require('event-kit') - -module.exports = -class BlockDecorationsPresenter { - constructor (model, lineTopIndex) { - this.model = model - this.disposables = new EventKit.CompositeDisposable() - this.emitter = new EventKit.Emitter() - this.lineTopIndex = lineTopIndex - this.observedDecorations = new Set() - this.measuredDecorations = new Set() - - this.observeModel() - } - - destroy () { - this.disposables.dispose() - } - - onDidUpdateState (callback) { - return this.emitter.on('did-update-state', callback) - } - - setLineHeight (lineHeight) { - this.lineTopIndex.setDefaultLineHeight(lineHeight) - } - - observeModel () { - this.disposables.add(this.model.onDidAddDecoration(this.didAddDecoration.bind(this))) - this.disposables.add(this.model.buffer.onDidChange((changeEvent) => { - let oldExtent = changeEvent.oldRange.getExtent() - let newExtent = changeEvent.newRange.getExtent() - this.lineTopIndex.splice(changeEvent.oldRange.start, oldExtent, newExtent) - })) - - for (let decoration of this.model.getDecorations({type: 'block'})) { - this.didAddDecoration(decoration) - } - } - - setDimensionsForDecoration (decoration, width, height) { - if (this.observedDecorations.has(decoration)) { - this.lineTopIndex.resizeBlock(decoration.getId(), height) - } else { - this.didAddDecoration(decoration) - this.lineTopIndex.resizeBlock(decoration.getId(), height) - } - - this.measuredDecorations.add(decoration) - this.emitter.emit('did-update-state') - } - - invalidateDimensionsForDecoration (decoration) { - this.measuredDecorations.delete(decoration) - this.emitter.emit('did-update-state') - } - - measurementsChanged () { - this.measuredDecorations.clear() - this.emitter.emit('did-update-state') - } - - didAddDecoration (decoration) { - if (!decoration.isType('block') || this.observedDecorations.has(decoration)) { - return - } - - let didMoveDisposable = decoration.getMarker().bufferMarker.onDidChange((markerEvent) => { - this.didMoveDecoration(decoration, markerEvent) - }) - - let didDestroyDisposable = decoration.onDidDestroy(() => { - this.disposables.remove(didMoveDisposable) - this.disposables.remove(didDestroyDisposable) - didMoveDisposable.dispose() - didDestroyDisposable.dispose() - this.didDestroyDecoration(decoration) - }) - - this.lineTopIndex.insertBlock(decoration.getId(), decoration.getMarker().getHeadBufferPosition(), true, 0) - - this.observedDecorations.add(decoration) - this.disposables.add(didMoveDisposable) - this.disposables.add(didDestroyDisposable) - this.emitter.emit('did-update-state') - } - - didMoveDecoration (decoration, markerEvent) { - if (markerEvent.textChanged) { - // No need to move blocks because of a text change, because we already splice on buffer change. - return - } - - this.lineTopIndex.moveBlock(decoration.getId(), decoration.getMarker().getHeadBufferPosition()) - this.emitter.emit('did-update-state') - } - - didDestroyDecoration (decoration) { - if (this.observedDecorations.has(decoration)) { - this.lineTopIndex.removeBlock(decoration.getId()) - this.observedDecorations.delete(decoration) - this.emitter.emit('did-update-state') - } - } -} diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 0b8bfbbc9..f7804f910 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -2,7 +2,6 @@ {Point, Range} = require 'text-buffer' _ = require 'underscore-plus' Decoration = require './decoration' -BlockDecorationsPresenter = require './block-decorations-presenter' module.exports = class TextEditorPresenter @@ -29,7 +28,8 @@ class TextEditorPresenter @lineDecorationsByScreenRow = {} @lineNumberDecorationsByScreenRow = {} @customGutterDecorationsByGutterName = {} - @blockDecorationsPresenter = new BlockDecorationsPresenter(@model, @lineTopIndex) + @observedBlockDecorations = new Set() + @measuredBlockDecorations = new Set() @screenRowsToMeasure = [] @transferMeasurementsToModel() @transferMeasurementsFromModel() @@ -46,7 +46,6 @@ class TextEditorPresenter getLinesYardstick: -> @linesYardstick destroy: -> - @blockDecorationsPresenter.destroy() @disposables.dispose() clearTimeout(@stoppedScrollingTimeoutId) if @stoppedScrollingTimeoutId? clearInterval(@reflowingInterval) if @reflowingInterval? @@ -138,20 +137,14 @@ class TextEditorPresenter @shouldUpdateDecorations = true @emitDidUpdateState() - @disposables.add @blockDecorationsPresenter.onDidUpdateState => - @shouldUpdateHeightState = true - @shouldUpdateVerticalScrollState = true - @shouldUpdateHorizontalScrollState = true - @shouldUpdateScrollbarsState = true - @shouldUpdateContentState = true - @shouldUpdateDecorations = true - @shouldUpdateCursorsState = true - @shouldUpdateLinesState = true - @shouldUpdateLineNumberGutterState = true - @shouldUpdateLineNumbersState = true - @shouldUpdateGutterOrderState = true - @shouldUpdateCustomGutterDecorationState = true - @emitDidUpdateState() + @disposables.add @model.onDidAddDecoration(@didAddBlockDecoration.bind(this)) + @disposables.add @model.buffer.onDidChange ({oldRange, newRange}) => + oldExtent = oldRange.getExtent() + newExtent = newRange.getExtent() + @lineTopIndex.splice(oldRange.start, oldExtent, newExtent) + + for decoration in @model.getDecorations({type: 'block'}) + this.didAddBlockDecoration(decoration) @disposables.add @model.onDidChangeGrammar(@didChangeGrammar.bind(this)) @disposables.add @model.onDidChangePlaceholderText(@emitDidUpdateState.bind(this)) @@ -995,7 +988,7 @@ class TextEditorPresenter @measurementsChanged() measurementsChanged: -> - @blockDecorationsPresenter.measurementsChanged() + @measuredBlockDecorations.clear() @shouldUpdateDecorations = true @emitDidUpdateState() @@ -1061,7 +1054,7 @@ class TextEditorPresenter updateBlockDecorationState: (decoration, screenRow) -> startRow = @getStartTileRow() endRow = @getEndTileRow() + @tileSize - hasntMeasuredDecoration = !@blockDecorationsPresenter.measuredDecorations.has(decoration) + hasntMeasuredDecoration = !@measuredBlockDecorations.has(decoration) isVisible = startRow <= screenRow < endRow || screenRow is @mouseWheelScreenRow if isVisible or hasntMeasuredDecoration @state.content.blockDecorations[decoration.id] = {decoration, screenRow, isVisible} @@ -1258,11 +1251,60 @@ class TextEditorPresenter @emitDidUpdateState() - setBlockDecorationDimensions: -> - @blockDecorationsPresenter.setDimensionsForDecoration(arguments...) + setBlockDecorationDimensions: (decoration, width, height) -> + @lineTopIndex.resizeBlock(decoration.getId(), height) - invalidateBlockDecorationDimensions: -> - @blockDecorationsPresenter.invalidateDimensionsForDecoration(arguments...) + @measuredBlockDecorations.add(decoration) + @shouldUpdateDecorations = true + @emitDidUpdateState() + + invalidateBlockDecorationDimensions: (decoration) -> + @measuredBlockDecorations.delete(decoration) + @shouldUpdateDecorations = true + @emitDidUpdateState() + + didAddBlockDecoration: (decoration) -> + return if not decoration.isType('block') or @observedBlockDecorations.has(decoration) + + didMoveDisposable = decoration.getMarker().bufferMarker.onDidChange (markerEvent) => + @didMoveBlockDecoration(decoration, markerEvent) + + didDestroyDisposable = decoration.onDidDestroy => + @disposables.remove(didMoveDisposable) + @disposables.remove(didDestroyDisposable) + didMoveDisposable.dispose() + didDestroyDisposable.dispose() + @didDestroyBlockDecoration(decoration) + + @lineTopIndex.insertBlock( + decoration.getId(), + decoration.getMarker().getHeadBufferPosition(), + true, + 0 + ) + + @observedBlockDecorations.add(decoration) + @disposables.add(didMoveDisposable) + @disposables.add(didDestroyDisposable) + @shouldUpdateDecorations = true + @emitDidUpdateState() + + didMoveBlockDecoration: (decoration, markerEvent) -> + # Don't move blocks after a text change, because we already splice on buffer + # change. + return if markerEvent.textChanged + + @lineTopIndex.moveBlock(decoration.getId(), decoration.getMarker().getHeadBufferPosition()) + @shouldUpdateDecorations = true + @emitDidUpdateState() + + didDestroyBlockDecoration: (decoration) -> + return unless @observedBlockDecorations.has(decoration) + + @lineTopIndex.removeBlock(decoration.getId()) + @observedBlockDecorations.delete(decoration) + @shouldUpdateDecorations = true + @emitDidUpdateState() observeCursor: (cursor) -> didChangePositionDisposable = cursor.onDidChangePosition => From 907dc661ec46256d02375655eb92f0738fcb4efc Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Dec 2015 12:43:27 +0100 Subject: [PATCH 254/502] :racehorse: Make updating block decorations incremental --- src/text-editor-presenter.coffee | 52 +++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index f7804f910..8409f33e0 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -29,7 +29,8 @@ class TextEditorPresenter @lineNumberDecorationsByScreenRow = {} @customGutterDecorationsByGutterName = {} @observedBlockDecorations = new Set() - @measuredBlockDecorations = new Set() + @invalidatedBlockDecorations = new Set() + @invalidateAllBlockDecorations = false @screenRowsToMeasure = [] @transferMeasurementsToModel() @transferMeasurementsFromModel() @@ -204,6 +205,7 @@ class TextEditorPresenter highlights: {} overlays: {} cursors: {} + blockDecorations: {} gutters: [] # Shared state that is copied into ``@state.gutters`. @sharedGutterStyles = {} @@ -988,7 +990,7 @@ class TextEditorPresenter @measurementsChanged() measurementsChanged: -> - @measuredBlockDecorations.clear() + @invalidateAllBlockDecorations = true @shouldUpdateDecorations = true @emitDidUpdateState() @@ -1042,22 +1044,36 @@ class TextEditorPresenter @decorations = @model.decorationsStateForScreenRowRange(@startRow, @endRow - 1) updateBlockDecorations: -> - @state.content.blockDecorations = {} + @blockDecorationsToRenderById = {} @blockDecorationsByScreenRow = {} + visibleDecorationsByMarkerId = @model.decorationsForScreenRowRange(@getStartTileRow(), @getEndTileRow() + @tileSize - 1) - for decoration in @model.getDecorations({type: "block"}) - screenRow = decoration.getMarker().getHeadScreenPosition().row - @updateBlockDecorationState(decoration, screenRow) - @blockDecorationsByScreenRow[screenRow] ?= [] - @blockDecorationsByScreenRow[screenRow].push(decoration) + if @invalidateAllBlockDecorations + for decoration in @model.getDecorations(type: 'block') + @invalidatedBlockDecorations.add(decoration) + @invalidateAllBlockDecorations = false - updateBlockDecorationState: (decoration, screenRow) -> - startRow = @getStartTileRow() - endRow = @getEndTileRow() + @tileSize - hasntMeasuredDecoration = !@measuredBlockDecorations.has(decoration) - isVisible = startRow <= screenRow < endRow || screenRow is @mouseWheelScreenRow - if isVisible or hasntMeasuredDecoration - @state.content.blockDecorations[decoration.id] = {decoration, screenRow, isVisible} + for markerId, decorations of visibleDecorationsByMarkerId + for decoration in decorations when decoration.isType('block') + @updateBlockDecorationState(decoration, true) + + @invalidatedBlockDecorations.forEach (decoration) => + @updateBlockDecorationState(decoration, false) + + for decorationId, decorationState of @state.content.blockDecorations + continue if @blockDecorationsToRenderById[decorationId] + continue if decorationState.screenRow is @mouseWheelScreenRow + + delete @state.content.blockDecorations[decorationId] + + updateBlockDecorationState: (decoration, isVisible) -> + return if @blockDecorationsToRenderById[decoration.getId()] + + screenRow = decoration.getMarker().getHeadScreenPosition().row + @blockDecorationsByScreenRow[screenRow] ?= [] + @blockDecorationsByScreenRow[screenRow].push(decoration) + @state.content.blockDecorations[decoration.getId()] = {decoration, screenRow, isVisible} + @blockDecorationsToRenderById[decoration.getId()] = true updateLineDecorations: -> @lineDecorationsByScreenRow = {} @@ -1254,12 +1270,12 @@ class TextEditorPresenter setBlockDecorationDimensions: (decoration, width, height) -> @lineTopIndex.resizeBlock(decoration.getId(), height) - @measuredBlockDecorations.add(decoration) + @invalidatedBlockDecorations.delete(decoration) @shouldUpdateDecorations = true @emitDidUpdateState() invalidateBlockDecorationDimensions: (decoration) -> - @measuredBlockDecorations.delete(decoration) + @invalidatedBlockDecorations.add(decoration) @shouldUpdateDecorations = true @emitDidUpdateState() @@ -1284,6 +1300,7 @@ class TextEditorPresenter ) @observedBlockDecorations.add(decoration) + @invalidateBlockDecorationDimensions(decoration) @disposables.add(didMoveDisposable) @disposables.add(didDestroyDisposable) @shouldUpdateDecorations = true @@ -1303,6 +1320,7 @@ class TextEditorPresenter @lineTopIndex.removeBlock(decoration.getId()) @observedBlockDecorations.delete(decoration) + @invalidatedBlockDecorations.delete(decoration) @shouldUpdateDecorations = true @emitDidUpdateState() From 087dc3b4fd6348675e136e06a36db70751dbe7bc Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Dec 2015 15:18:04 +0100 Subject: [PATCH 255/502] Back to green specs :metal: --- spec/text-editor-presenter-spec.coffee | 2 +- src/lines-yardstick.coffee | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 98285c7b2..36395feed 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -893,7 +893,7 @@ describe "TextEditorPresenter", -> expect(getState(presenter).content.scrollTop).toBe 13 it "scrolls down automatically when the model is changed", -> - presenter = buildPresenter(scrollTop: 0, lineHeight: 10, explicitHeight: 10) + presenter = buildPresenter(scrollTop: 0, lineHeight: 10, explicitHeight: 20) editor.setText("") editor.insertNewline() diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index 6bc691639..3be276412 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -20,10 +20,11 @@ class LinesYardstick targetTop = pixelPosition.top targetLeft = pixelPosition.left defaultCharWidth = @model.getDefaultCharWidth() - row = @lineTopIndex.rowForPixelPosition(targetTop, 'floor') + row = @lineTopIndex.rowForPixelPosition(targetTop) targetLeft = 0 if targetTop < 0 targetLeft = Infinity if row > @model.getLastScreenRow() row = Math.min(row, @model.getLastScreenRow()) + row = Math.max(0, row) line = @model.tokenizedLineForScreenRow(row) lineNode = @lineNodesProvider.lineNodeForLineIdAndScreenRow(line?.id, row) From db9f67b9a515e5779bd62263e022eb9b610b6349 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Dec 2015 15:41:46 +0100 Subject: [PATCH 256/502] :fire: Remove maxRow parameter --- spec/lines-yardstick-spec.coffee | 1 - spec/text-editor-presenter-spec.coffee | 1 - src/text-editor-component.coffee | 1 - 3 files changed, 3 deletions(-) diff --git a/spec/lines-yardstick-spec.coffee b/spec/lines-yardstick-spec.coffee index ceae2a847..c6b8b2d76 100644 --- a/spec/lines-yardstick-spec.coffee +++ b/spec/lines-yardstick-spec.coffee @@ -46,7 +46,6 @@ describe "LinesYardstick", -> editor.setLineHeightInPixels(14) lineTopIndex = new LineTopIndex({ - maxRow: editor.getScreenLineCount(), defaultLineHeight: editor.getLineHeightInPixels() }) linesYardstick = new LinesYardstick(editor, mockLineNodesProvider, lineTopIndex, atom.grammars) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 36395feed..6cfc99d36 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -32,7 +32,6 @@ describe "TextEditorPresenter", -> buildPresenterWithoutMeasurements = (params={}) -> lineTopIndex = new LineTopIndex({ - maxRow: editor.getScreenLineCount(), defaultLineHeight: editor.getLineHeightInPixels() }) _.defaults params, diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 266102cdd..0edc267db 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -51,7 +51,6 @@ class TextEditorComponent @setScrollSensitivity(@config.get('editor.scrollSensitivity')) lineTopIndex = new LineTopIndex({ - maxRow: @editor.getScreenLineCount(), defaultLineHeight: @editor.getLineHeightInPixels() }) @presenter = new TextEditorPresenter From 8710089cb733290bd94ef631e58fca61a7091487 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Dec 2015 15:43:23 +0100 Subject: [PATCH 257/502] Support only Shadow DOM enabled editors --- src/text-editor-component.coffee | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 0edc267db..1d2c0bcdc 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -77,7 +77,6 @@ class TextEditorComponent else @domNode.classList.add('editor-contents') @overlayManager = new OverlayManager(@presenter, @domNode, @views) - @blockDecorationsComponent = new BlockDecorationsComponent(@domNode, @views, @presenter, @domElementPool) @scrollViewNode = document.createElement('div') @scrollViewNode.classList.add('scroll-view') @@ -89,7 +88,8 @@ class TextEditorComponent @linesComponent = new LinesComponent({@presenter, @hostElement, @useShadowDOM, @domElementPool, @assert, @grammars}) @scrollViewNode.appendChild(@linesComponent.getDomNode()) - @scrollViewNode.appendChild(@blockDecorationsComponent.getDomNode()) + if @blockDecorationsComponent? + @scrollViewNode.appendChild(@blockDecorationsComponent.getDomNode()) @linesYardstick = new LinesYardstick(@editor, @linesComponent, lineTopIndex, @grammars) @presenter.setLinesYardstick(@linesYardstick) @@ -167,7 +167,7 @@ class TextEditorComponent @hiddenInputComponent.updateSync(@newState) @linesComponent.updateSync(@newState) - @blockDecorationsComponent.updateSync(@newState) + @blockDecorationsComponent?.updateSync(@newState) @horizontalScrollbarComponent.updateSync(@newState) @verticalScrollbarComponent.updateSync(@newState) @scrollbarCornerComponent.updateSync(@newState) @@ -187,7 +187,7 @@ class TextEditorComponent readAfterUpdateSync: => @overlayManager?.measureOverlays() - @blockDecorationsComponent.measureBlockDecorations() + @blockDecorationsComponent?.measureBlockDecorations() mountGutterContainerComponent: -> @gutterContainerComponent = new GutterContainerComponent({@editor, @onLineNumberGutterMouseDown, @domElementPool, @views}) From 5fa9d3bc40a841904caa1f67d5adbdafef902bcb Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 19 Dec 2015 11:21:45 +0100 Subject: [PATCH 258/502] Splice LineTopIndex when DisplayBuffer changes We invalidate whole screen lines accordingly to `DisplayBuffer`, so that we can catch if there was any screen-only transformation and move block decorations accordingly. --- spec/text-editor-presenter-spec.coffee | 105 ++++++++++++++++++------- src/text-editor-presenter.coffee | 46 +++++------ 2 files changed, 101 insertions(+), 50 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 6cfc99d36..4022ea46b 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -169,38 +169,87 @@ describe "TextEditorPresenter", -> expect(stateFn(presenter).tiles[12]).toBeUndefined() - it "computes each tile's height and scrollTop based on block decorations' height", -> - presenter = buildPresenter(explicitHeight: 120, scrollTop: 0, lineHeight: 10, tileSize: 2) + describe "when there are block decorations", -> + it "computes each tile's height and scrollTop based on block decorations' height", -> + presenter = buildPresenter(explicitHeight: 120, scrollTop: 0, lineHeight: 10, tileSize: 2) - blockDecoration1 = editor.addBlockDecorationForScreenRow(0) - blockDecoration2 = editor.addBlockDecorationForScreenRow(3) - blockDecoration3 = editor.addBlockDecorationForScreenRow(5) - presenter.setBlockDecorationDimensions(blockDecoration1, 0, 1) - presenter.setBlockDecorationDimensions(blockDecoration2, 0, 30) - presenter.setBlockDecorationDimensions(blockDecoration3, 0, 40) + blockDecoration1 = editor.addBlockDecorationForScreenRow(0) + blockDecoration2 = editor.addBlockDecorationForScreenRow(3) + blockDecoration3 = editor.addBlockDecorationForScreenRow(5) + presenter.setBlockDecorationDimensions(blockDecoration1, 0, 1) + presenter.setBlockDecorationDimensions(blockDecoration2, 0, 30) + presenter.setBlockDecorationDimensions(blockDecoration3, 0, 40) - expect(stateFn(presenter).tiles[0].height).toBe(20 + 1) - expect(stateFn(presenter).tiles[0].top).toBe(0) - expect(stateFn(presenter).tiles[2].height).toBe(20 + 30) - expect(stateFn(presenter).tiles[2].top).toBe(20 + 1) - expect(stateFn(presenter).tiles[4].height).toBe(20 + 40) - expect(stateFn(presenter).tiles[4].top).toBe(20 + 30 + 20 + 1) - expect(stateFn(presenter).tiles[6].height).toBe(20) - expect(stateFn(presenter).tiles[6].top).toBe((20 + 40) + (20 + 30) + 20 + 1) - expect(stateFn(presenter).tiles[8]).toBeUndefined() + expect(stateFn(presenter).tiles[0].height).toBe(20 + 1) + expect(stateFn(presenter).tiles[0].top).toBe(0) + expect(stateFn(presenter).tiles[2].height).toBe(20 + 30) + expect(stateFn(presenter).tiles[2].top).toBe(20 + 1) + expect(stateFn(presenter).tiles[4].height).toBe(20 + 40) + expect(stateFn(presenter).tiles[4].top).toBe(20 + 30 + 20 + 1) + expect(stateFn(presenter).tiles[6].height).toBe(20) + expect(stateFn(presenter).tiles[6].top).toBe((20 + 40) + (20 + 30) + 20 + 1) + expect(stateFn(presenter).tiles[8]).toBeUndefined() - presenter.setScrollTop(21) + presenter.setScrollTop(21) - expect(stateFn(presenter).tiles[0]).toBeUndefined() - expect(stateFn(presenter).tiles[2].height).toBe(20 + 30) - expect(stateFn(presenter).tiles[2].top).toBe(0) - expect(stateFn(presenter).tiles[4].height).toBe(20 + 40) - expect(stateFn(presenter).tiles[4].top).toBe(30 + 20) - expect(stateFn(presenter).tiles[6].height).toBe(20) - expect(stateFn(presenter).tiles[6].top).toBe((20 + 40) + (20 + 30)) - expect(stateFn(presenter).tiles[8].height).toBe(20) - expect(stateFn(presenter).tiles[8].top).toBe((20 + 40) + (20 + 30) + 20) - expect(stateFn(presenter).tiles[10]).toBeUndefined() + expect(stateFn(presenter).tiles[0]).toBeUndefined() + expect(stateFn(presenter).tiles[2].height).toBe(20 + 30) + expect(stateFn(presenter).tiles[2].top).toBe(0) + expect(stateFn(presenter).tiles[4].height).toBe(20 + 40) + expect(stateFn(presenter).tiles[4].top).toBe(30 + 20) + expect(stateFn(presenter).tiles[6].height).toBe(20) + expect(stateFn(presenter).tiles[6].top).toBe((20 + 40) + (20 + 30)) + expect(stateFn(presenter).tiles[8].height).toBe(20) + expect(stateFn(presenter).tiles[8].top).toBe((20 + 40) + (20 + 30) + 20) + expect(stateFn(presenter).tiles[10]).toBeUndefined() + + it "works correctly when soft wrapping is enabled", -> + blockDecoration1 = editor.addBlockDecorationForScreenRow(0, null) + blockDecoration2 = editor.addBlockDecorationForScreenRow(4, null) + blockDecoration3 = editor.addBlockDecorationForScreenRow(8, null) + + presenter = buildPresenter(explicitHeight: 330, lineHeight: 10, tileSize: 2, baseCharacterWidth: 5) + + presenter.setBlockDecorationDimensions(blockDecoration1, 0, 10) + presenter.setBlockDecorationDimensions(blockDecoration2, 0, 20) + presenter.setBlockDecorationDimensions(blockDecoration3, 0, 30) + + expect(stateFn(presenter).tiles[0].top).toBe(0 * 10) + expect(stateFn(presenter).tiles[2].top).toBe(2 * 10 + 10) + expect(stateFn(presenter).tiles[4].top).toBe(4 * 10 + 10) + expect(stateFn(presenter).tiles[6].top).toBe(6 * 10 + 10 + 20) + expect(stateFn(presenter).tiles[8].top).toBe(8 * 10 + 10 + 20) + expect(stateFn(presenter).tiles[10].top).toBe(10 * 10 + 10 + 20 + 30) + expect(stateFn(presenter).tiles[12].top).toBe(12 * 10 + 10 + 20 + 30) + + editor.setSoftWrapped(true) + presenter.setContentFrameWidth(5 * 25) + + expect(stateFn(presenter).tiles[0].top).toBe(0 * 10) + expect(stateFn(presenter).tiles[2].top).toBe(2 * 10 + 10) + expect(stateFn(presenter).tiles[4].top).toBe(4 * 10 + 10) + expect(stateFn(presenter).tiles[6].top).toBe(6 * 10 + 10) + expect(stateFn(presenter).tiles[8].top).toBe(8 * 10 + 10) + expect(stateFn(presenter).tiles[10].top).toBe(10 * 10 + 10) + expect(stateFn(presenter).tiles[12].top).toBe(12 * 10 + 10 + 20) + expect(stateFn(presenter).tiles[14].top).toBe(14 * 10 + 10 + 20) + expect(stateFn(presenter).tiles[16].top).toBe(16 * 10 + 10 + 20) + expect(stateFn(presenter).tiles[18].top).toBe(18 * 10 + 10 + 20) + expect(stateFn(presenter).tiles[20].top).toBe(20 * 10 + 10 + 20) + expect(stateFn(presenter).tiles[22].top).toBe(22 * 10 + 10 + 20 + 30) + expect(stateFn(presenter).tiles[24].top).toBe(24 * 10 + 10 + 20 + 30) + expect(stateFn(presenter).tiles[26].top).toBe(26 * 10 + 10 + 20 + 30) + expect(stateFn(presenter).tiles[28].top).toBe(28 * 10 + 10 + 20 + 30) + + editor.setSoftWrapped(false) + + expect(stateFn(presenter).tiles[0].top).toBe(0 * 10) + expect(stateFn(presenter).tiles[2].top).toBe(2 * 10 + 10) + expect(stateFn(presenter).tiles[4].top).toBe(4 * 10 + 10) + expect(stateFn(presenter).tiles[6].top).toBe(6 * 10 + 10 + 20) + expect(stateFn(presenter).tiles[8].top).toBe(8 * 10 + 10 + 20) + expect(stateFn(presenter).tiles[10].top).toBe(10 * 10 + 10 + 20 + 30) + expect(stateFn(presenter).tiles[12].top).toBe(12 * 10 + 10 + 20 + 30) it "includes state for all tiles if no external ::explicitHeight is assigned", -> presenter = buildPresenter(explicitHeight: null, tileSize: 2) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 8409f33e0..46a575e55 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -29,8 +29,8 @@ class TextEditorPresenter @lineNumberDecorationsByScreenRow = {} @customGutterDecorationsByGutterName = {} @observedBlockDecorations = new Set() - @invalidatedBlockDecorations = new Set() - @invalidateAllBlockDecorations = false + @invalidatedDimensionsByBlockDecoration = new Set() + @invalidateAllBlockDecorationsDimensions = false @screenRowsToMeasure = [] @transferMeasurementsToModel() @transferMeasurementsFromModel() @@ -130,7 +130,8 @@ class TextEditorPresenter @shouldUpdateDecorations = true observeModel: -> - @disposables.add @model.onDidChange => + @disposables.add @model.onDidChange ({start, end, screenDelta}) => + @spliceBlockDecorationsInRange(start, end, screenDelta) @shouldUpdateDecorations = true @emitDidUpdateState() @@ -139,10 +140,6 @@ class TextEditorPresenter @emitDidUpdateState() @disposables.add @model.onDidAddDecoration(@didAddBlockDecoration.bind(this)) - @disposables.add @model.buffer.onDidChange ({oldRange, newRange}) => - oldExtent = oldRange.getExtent() - newExtent = newRange.getExtent() - @lineTopIndex.splice(oldRange.start, oldExtent, newExtent) for decoration in @model.getDecorations({type: 'block'}) this.didAddBlockDecoration(decoration) @@ -990,7 +987,7 @@ class TextEditorPresenter @measurementsChanged() measurementsChanged: -> - @invalidateAllBlockDecorations = true + @invalidateAllBlockDecorationsDimensions = true @shouldUpdateDecorations = true @emitDidUpdateState() @@ -1048,16 +1045,16 @@ class TextEditorPresenter @blockDecorationsByScreenRow = {} visibleDecorationsByMarkerId = @model.decorationsForScreenRowRange(@getStartTileRow(), @getEndTileRow() + @tileSize - 1) - if @invalidateAllBlockDecorations + if @invalidateAllBlockDecorationsDimensions for decoration in @model.getDecorations(type: 'block') - @invalidatedBlockDecorations.add(decoration) - @invalidateAllBlockDecorations = false + @invalidatedDimensionsByBlockDecoration.add(decoration) + @invalidateAllBlockDecorationsDimensions = false for markerId, decorations of visibleDecorationsByMarkerId for decoration in decorations when decoration.isType('block') @updateBlockDecorationState(decoration, true) - @invalidatedBlockDecorations.forEach (decoration) => + @invalidatedDimensionsByBlockDecoration.forEach (decoration) => @updateBlockDecorationState(decoration, false) for decorationId, decorationState of @state.content.blockDecorations @@ -1270,15 +1267,25 @@ class TextEditorPresenter setBlockDecorationDimensions: (decoration, width, height) -> @lineTopIndex.resizeBlock(decoration.getId(), height) - @invalidatedBlockDecorations.delete(decoration) + @invalidatedDimensionsByBlockDecoration.delete(decoration) @shouldUpdateDecorations = true @emitDidUpdateState() invalidateBlockDecorationDimensions: (decoration) -> - @invalidatedBlockDecorations.add(decoration) + @invalidatedDimensionsByBlockDecoration.add(decoration) @shouldUpdateDecorations = true @emitDidUpdateState() + spliceBlockDecorationsInRange: (start, end, screenDelta) -> + return if screenDelta is 0 + + oldExtent = Point(end - start, Infinity) + newExtent = Point(end - start + screenDelta, 0) + invalidatedBlockDecorationIds = @lineTopIndex.splice(Point(start, 0), oldExtent, newExtent, true) + invalidatedBlockDecorationIds?.forEach (blockDecorationId) => + decoration = @model.decorationForId(blockDecorationId) + @lineTopIndex.moveBlock(decoration.getId(), decoration.getMarker().getHeadScreenPosition()) + didAddBlockDecoration: (decoration) -> return if not decoration.isType('block') or @observedBlockDecorations.has(decoration) @@ -1292,12 +1299,7 @@ class TextEditorPresenter didDestroyDisposable.dispose() @didDestroyBlockDecoration(decoration) - @lineTopIndex.insertBlock( - decoration.getId(), - decoration.getMarker().getHeadBufferPosition(), - true, - 0 - ) + @lineTopIndex.insertBlock(decoration.getId(), decoration.getMarker().getHeadScreenPosition(), true, 0) @observedBlockDecorations.add(decoration) @invalidateBlockDecorationDimensions(decoration) @@ -1311,7 +1313,7 @@ class TextEditorPresenter # change. return if markerEvent.textChanged - @lineTopIndex.moveBlock(decoration.getId(), decoration.getMarker().getHeadBufferPosition()) + @lineTopIndex.moveBlock(decoration.getId(), decoration.getMarker().getHeadScreenPosition()) @shouldUpdateDecorations = true @emitDidUpdateState() @@ -1320,7 +1322,7 @@ class TextEditorPresenter @lineTopIndex.removeBlock(decoration.getId()) @observedBlockDecorations.delete(decoration) - @invalidatedBlockDecorations.delete(decoration) + @invalidatedDimensionsByBlockDecoration.delete(decoration) @shouldUpdateDecorations = true @emitDidUpdateState() From 5e0863c119d9f22f2621807c33edc625dd858cb5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 19 Dec 2015 12:02:05 +0100 Subject: [PATCH 259/502] :white_check_mark: Write specs for moving markers manually --- spec/text-editor-presenter-spec.coffee | 48 +++++++++++++++++--------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 4022ea46b..199075197 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -180,27 +180,40 @@ describe "TextEditorPresenter", -> presenter.setBlockDecorationDimensions(blockDecoration2, 0, 30) presenter.setBlockDecorationDimensions(blockDecoration3, 0, 40) - expect(stateFn(presenter).tiles[0].height).toBe(20 + 1) - expect(stateFn(presenter).tiles[0].top).toBe(0) - expect(stateFn(presenter).tiles[2].height).toBe(20 + 30) - expect(stateFn(presenter).tiles[2].top).toBe(20 + 1) - expect(stateFn(presenter).tiles[4].height).toBe(20 + 40) - expect(stateFn(presenter).tiles[4].top).toBe(20 + 30 + 20 + 1) - expect(stateFn(presenter).tiles[6].height).toBe(20) - expect(stateFn(presenter).tiles[6].top).toBe((20 + 40) + (20 + 30) + 20 + 1) + expect(stateFn(presenter).tiles[0].height).toBe(2 * 10 + 1) + expect(stateFn(presenter).tiles[0].top).toBe(0 * 10) + expect(stateFn(presenter).tiles[2].height).toBe(2 * 10 + 30) + expect(stateFn(presenter).tiles[2].top).toBe(2 * 10 + 1) + expect(stateFn(presenter).tiles[4].height).toBe(2 * 10 + 40) + expect(stateFn(presenter).tiles[4].top).toBe(4 * 10 + 1 + 30) + expect(stateFn(presenter).tiles[6].height).toBe(2 * 10) + expect(stateFn(presenter).tiles[6].top).toBe(6 * 10 + 1 + 30 + 40) expect(stateFn(presenter).tiles[8]).toBeUndefined() presenter.setScrollTop(21) expect(stateFn(presenter).tiles[0]).toBeUndefined() - expect(stateFn(presenter).tiles[2].height).toBe(20 + 30) - expect(stateFn(presenter).tiles[2].top).toBe(0) - expect(stateFn(presenter).tiles[4].height).toBe(20 + 40) - expect(stateFn(presenter).tiles[4].top).toBe(30 + 20) - expect(stateFn(presenter).tiles[6].height).toBe(20) - expect(stateFn(presenter).tiles[6].top).toBe((20 + 40) + (20 + 30)) - expect(stateFn(presenter).tiles[8].height).toBe(20) - expect(stateFn(presenter).tiles[8].top).toBe((20 + 40) + (20 + 30) + 20) + expect(stateFn(presenter).tiles[2].height).toBe(2 * 10 + 30) + expect(stateFn(presenter).tiles[2].top).toBe(2 * 10 + 1 - 21) + expect(stateFn(presenter).tiles[4].height).toBe(2 * 10 + 40) + expect(stateFn(presenter).tiles[4].top).toBe(4 * 10 + 1 + 30 - 21) + expect(stateFn(presenter).tiles[6].height).toBe(2 * 10) + expect(stateFn(presenter).tiles[6].top).toBe(6 * 10 + 1 + 30 + 40 - 21) + expect(stateFn(presenter).tiles[8].height).toBe(2 * 10) + expect(stateFn(presenter).tiles[8].top).toBe(8 * 10 + 1 + 30 + 40 - 21) + expect(stateFn(presenter).tiles[10]).toBeUndefined() + + blockDecoration3.getMarker().setHeadScreenPosition([6, 0]) + + expect(stateFn(presenter).tiles[0]).toBeUndefined() + expect(stateFn(presenter).tiles[2].height).toBe(2 * 10 + 30) + expect(stateFn(presenter).tiles[2].top).toBe(2 * 10 + 1 - 21) + expect(stateFn(presenter).tiles[4].height).toBe(2 * 10) + expect(stateFn(presenter).tiles[4].top).toBe(4 * 10 + 1 + 30 - 21) + expect(stateFn(presenter).tiles[6].height).toBe(2 * 10 + 40) + expect(stateFn(presenter).tiles[6].top).toBe(6 * 10 + 1 + 30 - 21) + expect(stateFn(presenter).tiles[8].height).toBe(2 * 10) + expect(stateFn(presenter).tiles[8].top).toBe(8 * 10 + 1 + 30 + 40 - 21) expect(stateFn(presenter).tiles[10]).toBeUndefined() it "works correctly when soft wrapping is enabled", -> @@ -2231,6 +2244,7 @@ describe "TextEditorPresenter", -> } expect(stateForBlockDecoration(presenter, blockDecoration4)).toBeUndefined() + blockDecoration3.getMarker().setHeadScreenPosition([5, 0]) presenter.setScrollTop(90) expect(stateForBlockDecoration(presenter, blockDecoration1)).toBeUndefined() @@ -2241,7 +2255,7 @@ describe "TextEditorPresenter", -> } expectValues stateForBlockDecoration(presenter, blockDecoration3), { decoration: blockDecoration3 - screenRow: 4 + screenRow: 5 isVisible: false } expectValues stateForBlockDecoration(presenter, blockDecoration4), { From 07234c510935dd2e92a5a9a4e1f16625cfe5298d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 19 Dec 2015 12:50:58 +0100 Subject: [PATCH 260/502] Resize all block decorations when width changes --- spec/text-editor-presenter-spec.coffee | 43 ++++++++++++++++++++++++++ src/block-decorations-component.coffee | 7 +++-- src/text-editor-component.coffee | 2 +- src/text-editor-presenter.coffee | 5 +++ 4 files changed, 54 insertions(+), 3 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 199075197..035cddcc9 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -2195,6 +2195,49 @@ describe "TextEditorPresenter", -> expect(stateForBlockDecoration(presenter, blockDecoration1)).toBeUndefined() + it "invalidates all block decorations when content frame width, window size or bounding client rect change", -> + blockDecoration1 = editor.addBlockDecorationForScreenRow(11, null) + presenter = buildPresenter(explicitHeight: 30, lineHeight: 10, tileSize: 2, scrollTop: 0) + + expectValues stateForBlockDecoration(presenter, blockDecoration1), { + decoration: blockDecoration1 + screenRow: 11 + isVisible: false + } + + presenter.setBlockDecorationDimensions(blockDecoration1, 0, 10) + expect(stateForBlockDecoration(presenter, blockDecoration1)).toBeUndefined() + + presenter.setBoundingClientRect({top: 0, left: 0, width: 50, height: 30}) + expectValues stateForBlockDecoration(presenter, blockDecoration1), { + decoration: blockDecoration1 + screenRow: 11 + isVisible: false + } + + presenter.setBlockDecorationDimensions(blockDecoration1, 0, 20) + expect(stateForBlockDecoration(presenter, blockDecoration1)).toBeUndefined() + + presenter.setContentFrameWidth(100) + expectValues stateForBlockDecoration(presenter, blockDecoration1), { + decoration: blockDecoration1 + screenRow: 11 + isVisible: false + } + + presenter.setBlockDecorationDimensions(blockDecoration1, 0, 20) + expect(stateForBlockDecoration(presenter, blockDecoration1)).toBeUndefined() + + presenter.setWindowSize(100, 200) + expectValues stateForBlockDecoration(presenter, blockDecoration1), { + decoration: blockDecoration1 + screenRow: 11 + isVisible: false + } + + presenter.setBlockDecorationDimensions(blockDecoration1, 0, 20) + expect(stateForBlockDecoration(presenter, blockDecoration1)).toBeUndefined() + it "contains state for block decorations, indicating the screen row they belong to both initially and when their markers move", -> item = {} blockDecoration1 = editor.addBlockDecorationForScreenRow(0, item) diff --git a/src/block-decorations-component.coffee b/src/block-decorations-component.coffee index c154b85c6..e9d80f2aa 100644 --- a/src/block-decorations-component.coffee +++ b/src/block-decorations-component.coffee @@ -12,14 +12,17 @@ class BlockDecorationsComponent @domNode = @domElementPool.buildElement("content") @domNode.setAttribute("select", ".atom--invisible-block-decoration") @domNode.style.visibility = "hidden" - @domNode.style.position = "absolute" getDomNode: -> @domNode updateSync: (state) -> @newState = state.content - @oldState ?= {blockDecorations: {}} + @oldState ?= {blockDecorations: {}, width: 0} + + if @newState.width isnt @oldState.width + @domNode.style.width = @newState.width + "px" + @oldState.width = @newState.width for id, blockDecorationState of @oldState.blockDecorations unless @newState.blockDecorations.hasOwnProperty(id) diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 1d2c0bcdc..1fcb21123 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -89,7 +89,7 @@ class TextEditorComponent @scrollViewNode.appendChild(@linesComponent.getDomNode()) if @blockDecorationsComponent? - @scrollViewNode.appendChild(@blockDecorationsComponent.getDomNode()) + @linesComponent.getDomNode().appendChild(@blockDecorationsComponent.getDomNode()) @linesYardstick = new LinesYardstick(@editor, @linesComponent, lineTopIndex, @grammars) @presenter.setLinesYardstick(@linesYardstick) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 46a575e55..f11027b22 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -921,12 +921,15 @@ class TextEditorPresenter @editorWidthInChars = null @updateScrollbarDimensions() @updateClientWidth() + @invalidateAllBlockDecorationsDimensions = true @shouldUpdateDecorations = true @emitDidUpdateState() setBoundingClientRect: (boundingClientRect) -> unless @clientRectsEqual(@boundingClientRect, boundingClientRect) @boundingClientRect = boundingClientRect + @invalidateAllBlockDecorationsDimensions = true + @shouldUpdateDecorations = true @emitDidUpdateState() clientRectsEqual: (clientRectA, clientRectB) -> @@ -940,6 +943,8 @@ class TextEditorPresenter if @windowWidth isnt width or @windowHeight isnt height @windowWidth = width @windowHeight = height + @invalidateAllBlockDecorationsDimensions = true + @shouldUpdateDecorations = true @emitDidUpdateState() From dfb095b75471d5f555c0b7b13c745f1bb6d93423 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 19 Dec 2015 13:00:47 +0100 Subject: [PATCH 261/502] :fire: Remove TextEditor.prototype.addBlockDecorationForScreenRow --- spec/text-editor-component-spec.js | 10 +++- spec/text-editor-presenter-spec.coffee | 72 ++++++++++++++------------ src/text-editor.coffee | 18 ++----- 3 files changed, 51 insertions(+), 49 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 2a686cf0c..fb874a7aa 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -1655,7 +1655,10 @@ describe('TextEditorComponent', function () { function createBlockDecorationForScreenRowWith(screenRow, {className}) { let item = document.createElement("div") item.className = className || "" - let blockDecoration = editor.addBlockDecorationForScreenRow(screenRow, item) + let blockDecoration = editor.decorateMarker( + editor.markScreenPosition([screenRow, 0], invalidate: "never"), + type: "block", item: item + ) return [item, blockDecoration] } @@ -3616,7 +3619,10 @@ describe('TextEditorComponent', function () { item.style.width = "30px" item.style.height = "30px" item.className = "decoration-1" - editor.addBlockDecorationForScreenRow(0, item) + editor.decorateMarker( + editor.markScreenPosition([0, 0], invalidate: "never"), + type: "block", item: item + ) await nextViewUpdatePromise() diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 035cddcc9..5a4e8c916 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -30,6 +30,12 @@ describe "TextEditorPresenter", -> presenter.getPreMeasurementState() presenter.getPostMeasurementState() + addBlockDecorationForScreenRow = (screenRow, item) -> + editor.decorateMarker( + editor.markScreenPosition([screenRow, 0], invalidate: "never"), + type: "block", item: item + ) + buildPresenterWithoutMeasurements = (params={}) -> lineTopIndex = new LineTopIndex({ defaultLineHeight: editor.getLineHeightInPixels() @@ -173,9 +179,9 @@ describe "TextEditorPresenter", -> it "computes each tile's height and scrollTop based on block decorations' height", -> presenter = buildPresenter(explicitHeight: 120, scrollTop: 0, lineHeight: 10, tileSize: 2) - blockDecoration1 = editor.addBlockDecorationForScreenRow(0) - blockDecoration2 = editor.addBlockDecorationForScreenRow(3) - blockDecoration3 = editor.addBlockDecorationForScreenRow(5) + blockDecoration1 = addBlockDecorationForScreenRow(0) + blockDecoration2 = addBlockDecorationForScreenRow(3) + blockDecoration3 = addBlockDecorationForScreenRow(5) presenter.setBlockDecorationDimensions(blockDecoration1, 0, 1) presenter.setBlockDecorationDimensions(blockDecoration2, 0, 30) presenter.setBlockDecorationDimensions(blockDecoration3, 0, 40) @@ -217,9 +223,9 @@ describe "TextEditorPresenter", -> expect(stateFn(presenter).tiles[10]).toBeUndefined() it "works correctly when soft wrapping is enabled", -> - blockDecoration1 = editor.addBlockDecorationForScreenRow(0, null) - blockDecoration2 = editor.addBlockDecorationForScreenRow(4, null) - blockDecoration3 = editor.addBlockDecorationForScreenRow(8, null) + blockDecoration1 = addBlockDecorationForScreenRow(0) + blockDecoration2 = addBlockDecorationForScreenRow(4) + blockDecoration3 = addBlockDecorationForScreenRow(8) presenter = buildPresenter(explicitHeight: 330, lineHeight: 10, tileSize: 2, baseCharacterWidth: 5) @@ -586,9 +592,9 @@ describe "TextEditorPresenter", -> presenter = buildPresenter(scrollTop: 0, lineHeight: 10) expect(getState(presenter).verticalScrollbar.scrollHeight).toBe editor.getScreenLineCount() * 10 - blockDecoration1 = editor.addBlockDecorationForScreenRow(0, null) - blockDecoration2 = editor.addBlockDecorationForScreenRow(3, null) - blockDecoration3 = editor.addBlockDecorationForScreenRow(7, null) + blockDecoration1 = addBlockDecorationForScreenRow(0) + blockDecoration2 = addBlockDecorationForScreenRow(3) + blockDecoration3 = addBlockDecorationForScreenRow(7) presenter.setBlockDecorationDimensions(blockDecoration1, 0, 35.8) presenter.setBlockDecorationDimensions(blockDecoration2, 0, 50.3) @@ -760,9 +766,9 @@ describe "TextEditorPresenter", -> presenter = buildPresenter(scrollTop: 0, lineHeight: 10) expect(getState(presenter).verticalScrollbar.scrollHeight).toBe editor.getScreenLineCount() * 10 - blockDecoration1 = editor.addBlockDecorationForScreenRow(0, null) - blockDecoration2 = editor.addBlockDecorationForScreenRow(3, null) - blockDecoration3 = editor.addBlockDecorationForScreenRow(7, null) + blockDecoration1 = addBlockDecorationForScreenRow(0) + blockDecoration2 = addBlockDecorationForScreenRow(3) + blockDecoration3 = addBlockDecorationForScreenRow(7) presenter.setBlockDecorationDimensions(blockDecoration1, 0, 35.8) presenter.setBlockDecorationDimensions(blockDecoration2, 0, 50.3) @@ -1322,10 +1328,10 @@ describe "TextEditorPresenter", -> describe ".blockDecorations", -> it "contains all block decorations that are present before a line, both initially and when decorations change", -> - blockDecoration1 = editor.addBlockDecorationForScreenRow(0) + blockDecoration1 = addBlockDecorationForScreenRow(0) presenter = buildPresenter() - blockDecoration2 = editor.addBlockDecorationForScreenRow(3) - blockDecoration3 = editor.addBlockDecorationForScreenRow(7) + blockDecoration2 = addBlockDecorationForScreenRow(3) + blockDecoration3 = addBlockDecorationForScreenRow(7) waitsForStateToUpdate presenter runs -> @@ -1612,8 +1618,8 @@ describe "TextEditorPresenter", -> expect(stateForCursor(presenter, 3)).toEqual {top: 5 * 10, left: 12 * 10, width: 10, height: 10} expect(stateForCursor(presenter, 4)).toEqual {top: 8 * 10, left: 4 * 10, width: 10, height: 10} - blockDecoration1 = editor.addBlockDecorationForScreenRow(0) - blockDecoration2 = editor.addBlockDecorationForScreenRow(1) + blockDecoration1 = addBlockDecorationForScreenRow(0) + blockDecoration2 = addBlockDecorationForScreenRow(1) waitsForStateToUpdate presenter, -> presenter.setBlockDecorationDimensions(blockDecoration1, 0, 30) @@ -2177,7 +2183,7 @@ describe "TextEditorPresenter", -> getState(presenter).content.blockDecorations[decoration.id] it "contains state for measured block decorations that are not visible when they are on ::mouseWheelScreenRow", -> - blockDecoration1 = editor.addBlockDecorationForScreenRow(0, null) + blockDecoration1 = addBlockDecorationForScreenRow(0) presenter = buildPresenter(explicitHeight: 30, lineHeight: 10, tileSize: 2, scrollTop: 0, stoppedScrollingDelay: 200) getState(presenter) # flush pending state presenter.setBlockDecorationDimensions(blockDecoration1, 0, 0) @@ -2196,7 +2202,7 @@ describe "TextEditorPresenter", -> expect(stateForBlockDecoration(presenter, blockDecoration1)).toBeUndefined() it "invalidates all block decorations when content frame width, window size or bounding client rect change", -> - blockDecoration1 = editor.addBlockDecorationForScreenRow(11, null) + blockDecoration1 = addBlockDecorationForScreenRow(11) presenter = buildPresenter(explicitHeight: 30, lineHeight: 10, tileSize: 2, scrollTop: 0) expectValues stateForBlockDecoration(presenter, blockDecoration1), { @@ -2240,10 +2246,10 @@ describe "TextEditorPresenter", -> it "contains state for block decorations, indicating the screen row they belong to both initially and when their markers move", -> item = {} - blockDecoration1 = editor.addBlockDecorationForScreenRow(0, item) - blockDecoration2 = editor.addBlockDecorationForScreenRow(4, item) - blockDecoration3 = editor.addBlockDecorationForScreenRow(4, item) - blockDecoration4 = editor.addBlockDecorationForScreenRow(10, item) + blockDecoration1 = addBlockDecorationForScreenRow(0, item) + blockDecoration2 = addBlockDecorationForScreenRow(4, item) + blockDecoration3 = addBlockDecorationForScreenRow(4, item) + blockDecoration4 = addBlockDecorationForScreenRow(10, item) presenter = buildPresenter(explicitHeight: 30, lineHeight: 10, tileSize: 2, scrollTop: 0) expectValues stateForBlockDecoration(presenter, blockDecoration1), { @@ -2825,11 +2831,11 @@ describe "TextEditorPresenter", -> describe ".blockDecorationsHeight", -> it "adds the sum of all block decorations' heights to the relevant line number state objects, both initially and when decorations change", -> - blockDecoration1 = editor.addBlockDecorationForScreenRow(0) + blockDecoration1 = addBlockDecorationForScreenRow(0) presenter = buildPresenter() - blockDecoration2 = editor.addBlockDecorationForScreenRow(3) - blockDecoration3 = editor.addBlockDecorationForScreenRow(3) - blockDecoration4 = editor.addBlockDecorationForScreenRow(7) + blockDecoration2 = addBlockDecorationForScreenRow(3) + blockDecoration3 = addBlockDecorationForScreenRow(3) + blockDecoration4 = addBlockDecorationForScreenRow(7) presenter.setBlockDecorationDimensions(blockDecoration1, 0, 10) presenter.setBlockDecorationDimensions(blockDecoration2, 0, 20) @@ -3119,13 +3125,13 @@ describe "TextEditorPresenter", -> it "updates when block decorations are added, changed or removed", -> # block decoration before decoration1 - blockDecoration1 = editor.addBlockDecorationForScreenRow(0) + blockDecoration1 = addBlockDecorationForScreenRow(0) presenter.setBlockDecorationDimensions(blockDecoration1, 0, 3) # block decoration between decoration1 and decoration2 - blockDecoration2 = editor.addBlockDecorationForScreenRow(3) + blockDecoration2 = addBlockDecorationForScreenRow(3) presenter.setBlockDecorationDimensions(blockDecoration2, 0, 5) # block decoration between decoration2 and decoration3 - blockDecoration3 = editor.addBlockDecorationForScreenRow(10) + blockDecoration3 = addBlockDecorationForScreenRow(10) presenter.setBlockDecorationDimensions(blockDecoration3, 0, 7) decorationState = getContentForGutterWithName(presenter, 'test-gutter') @@ -3375,9 +3381,9 @@ describe "TextEditorPresenter", -> presenter = buildPresenter(scrollTop: 0, lineHeight: 10) expect(getState(presenter).verticalScrollbar.scrollHeight).toBe editor.getScreenLineCount() * 10 - blockDecoration1 = editor.addBlockDecorationForScreenRow(0, null) - blockDecoration2 = editor.addBlockDecorationForScreenRow(3, null) - blockDecoration3 = editor.addBlockDecorationForScreenRow(7, null) + blockDecoration1 = addBlockDecorationForScreenRow(0) + blockDecoration2 = addBlockDecorationForScreenRow(3) + blockDecoration3 = addBlockDecorationForScreenRow(7) presenter.setBlockDecorationDimensions(blockDecoration1, 0, 35.8) presenter.setBlockDecorationDimensions(blockDecoration2, 0, 50.3) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index ed50f9e79..325e4a304 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -1398,18 +1398,6 @@ class TextEditor extends Model Section: Decorations ### - # Experimental: Mark and add a block decoration to the specified screen row. - # - # * `screenRow` A {Number} representing the screen row where to add the block decoration. - # * `item` A {ViewRegistry::getView}-compatible object to render. - # - # Returns a {Decoration} object. - addBlockDecorationForScreenRow: (screenRow, item) -> - @decorateMarker( - @markScreenPosition([screenRow, 0], invalidate: "never"), - type: "block", item: item - ) - # Essential: Add a decoration that tracks a {TextEditorMarker}. When the # marker moves, is invalidated, or is destroyed, the decoration will be # updated to reflect the marker's state. @@ -1435,6 +1423,8 @@ class TextEditor extends Model # * __gutter__: A decoration that tracks a {TextEditorMarker} in a {Gutter}. Gutter # decorations are created by calling {Gutter::decorateMarker} on the # desired `Gutter` instance. + # * __block__: A decoration that lies between the {TextEditorMarker} row and + # the previous one. # # ## Arguments # @@ -1457,8 +1447,8 @@ class TextEditor extends Model # * `class` This CSS class will be applied to the decorated line number, # line, highlight, or overlay. # * `item` (optional) An {HTMLElement} or a model {Object} with a - # corresponding view registered. Only applicable to the `gutter` and - # `overlay` types. + # corresponding view registered. Only applicable to the `gutter`, + # `overlay` and `block` types. # * `onlyHead` (optional) If `true`, the decoration will only be applied to # the head of the `TextEditorMarker`. Only applicable to the `line` and # `line-number` types. From 5dfecf39ab5eb3f28f763d4b3755a74c01e33dd0 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 19 Dec 2015 13:18:36 +0100 Subject: [PATCH 262/502] Take margin top and margin bottom into account --- spec/text-editor-component-spec.js | 30 ++++++++++++++++++++++---- src/block-decorations-component.coffee | 5 ++++- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index fb874a7aa..041eca4cb 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -1656,8 +1656,8 @@ describe('TextEditorComponent', function () { let item = document.createElement("div") item.className = className || "" let blockDecoration = editor.decorateMarker( - editor.markScreenPosition([screenRow, 0], invalidate: "never"), - type: "block", item: item + editor.markScreenPosition([screenRow, 0], {invalidate: "never"}), + {type: "block", item: item} ) return [item, blockDecoration] } @@ -1824,6 +1824,28 @@ describe('TextEditorComponent', function () { expect(component.lineNodeForScreenRow(1).dataset.screenRow).toBe("1") expect(component.lineNodeForScreenRow(2).dataset.screenRow).toBe("2") }) + + it('does not render highlights for off-screen lines until they come on-screen', async function () { + wrapperNode.style.height = 9 * lineHeightInPixels + 'px' + component.measureDimensions() + await nextViewUpdatePromise() + + let [item, blockDecoration] = createBlockDecorationForScreenRowWith(0, {className: "decoration-1"}) + atom.styles.addStyleSheet( + 'atom-text-editor .decoration-1 { width: 30px; height: 30px; margin-top: 10px; margin-bottom: 5px; }', + {context: 'atom-text-editor'} + ) + + await nextAnimationFramePromise() // causes the DOM to update and to retrieve new styles + await nextAnimationFramePromise() // applies the changes + + expect(component.tileNodesForLines()[0].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 30 + 10 + 5 + "px") + expect(component.tileNodesForLines()[0].style.webkitTransform).toBe("translate3d(0px, 0px, 0px)") + expect(component.tileNodesForLines()[1].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + "px") + expect(component.tileNodesForLines()[1].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight}px, 0px)`) + expect(component.tileNodesForLines()[2].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + "px") + expect(component.tileNodesForLines()[2].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight + component.tileNodesForLines()[1].offsetHeight}px, 0px)`) + }) }) describe('highlight decoration rendering', function () { @@ -3620,8 +3642,8 @@ describe('TextEditorComponent', function () { item.style.height = "30px" item.className = "decoration-1" editor.decorateMarker( - editor.markScreenPosition([0, 0], invalidate: "never"), - type: "block", item: item + editor.markScreenPosition([0, 0], {invalidate: "never"}), + {type: "block", item: item} ) await nextViewUpdatePromise() diff --git a/src/block-decorations-component.coffee b/src/block-decorations-component.coffee index e9d80f2aa..0cfa7974f 100644 --- a/src/block-decorations-component.coffee +++ b/src/block-decorations-component.coffee @@ -39,11 +39,14 @@ class BlockDecorationsComponent measureBlockDecorations: -> for decorationId, blockDecorationNode of @blockDecorationNodesById + style = getComputedStyle(blockDecorationNode) decoration = @newState.blockDecorations[decorationId].decoration + marginBottom = parseInt(style.marginBottom) ? 0 + marginTop = parseInt(style.marginTop) ? 0 @presenter.setBlockDecorationDimensions( decoration, blockDecorationNode.offsetWidth, - blockDecorationNode.offsetHeight + blockDecorationNode.offsetHeight + marginTop + marginBottom ) createAndAppendBlockDecorationNode: (id) -> From 1534aad414fb14f07bd9589a868512b7c897aac1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 19 Dec 2015 14:38:30 +0100 Subject: [PATCH 263/502] :arrow_up: line-top-index --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 63953b1b6..99b62519e 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "jquery": "^2.1.1", "key-path-helpers": "^0.4.0", "less-cache": "0.22", - "line-top-index": "0.1.0", + "line-top-index": "0.1.1", "marked": "^0.3.4", "normalize-package-data": "^2.0.0", "nslog": "^3", From 7543bcbdc17c42a863ad1e8ca544975ddee91c05 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 19 Dec 2015 14:59:25 +0100 Subject: [PATCH 264/502] :art: Rearrange code a bit --- src/text-editor-presenter.coffee | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index f11027b22..9a29ec922 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -1286,10 +1286,10 @@ class TextEditorPresenter oldExtent = Point(end - start, Infinity) newExtent = Point(end - start + screenDelta, 0) - invalidatedBlockDecorationIds = @lineTopIndex.splice(Point(start, 0), oldExtent, newExtent, true) - invalidatedBlockDecorationIds?.forEach (blockDecorationId) => - decoration = @model.decorationForId(blockDecorationId) - @lineTopIndex.moveBlock(decoration.getId(), decoration.getMarker().getHeadScreenPosition()) + invalidatedBlockDecorationIds = @lineTopIndex.splice(Point(start, 0), oldExtent, newExtent) + invalidatedBlockDecorationIds.forEach (id) => + newScreenPosition = @model.decorationForId(id).getMarker().getHeadScreenPosition() + @lineTopIndex.moveBlock(id, newScreenPosition) didAddBlockDecoration: (decoration) -> return if not decoration.isType('block') or @observedBlockDecorations.has(decoration) From eab70d9a9537fd1939792c6b20ddf618b155de50 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 19 Dec 2015 15:38:34 +0100 Subject: [PATCH 265/502] Invalidate spliced block decorations' dimensions --- spec/text-editor-presenter-spec.coffee | 45 ++++++++++++++++++++++++++ src/text-editor-presenter.coffee | 4 ++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 5a4e8c916..b604a7022 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -2201,6 +2201,51 @@ describe "TextEditorPresenter", -> expect(stateForBlockDecoration(presenter, blockDecoration1)).toBeUndefined() + it "invalidates block decorations that intersect a change in the buffer", -> + blockDecoration1 = addBlockDecorationForScreenRow(9) + blockDecoration2 = addBlockDecorationForScreenRow(10) + blockDecoration3 = addBlockDecorationForScreenRow(11) + presenter = buildPresenter(explicitHeight: 30, lineHeight: 10, tileSize: 2, scrollTop: 0) + + expectValues stateForBlockDecoration(presenter, blockDecoration1), { + decoration: blockDecoration1 + screenRow: 9 + isVisible: false + } + expectValues stateForBlockDecoration(presenter, blockDecoration2), { + decoration: blockDecoration2 + screenRow: 10 + isVisible: false + } + expectValues stateForBlockDecoration(presenter, blockDecoration3), { + decoration: blockDecoration3 + screenRow: 11 + isVisible: false + } + + presenter.setBlockDecorationDimensions(blockDecoration1, 0, 10) + presenter.setBlockDecorationDimensions(blockDecoration2, 0, 10) + presenter.setBlockDecorationDimensions(blockDecoration3, 0, 10) + expect(stateForBlockDecoration(presenter, blockDecoration1)).toBeUndefined() + expect(stateForBlockDecoration(presenter, blockDecoration2)).toBeUndefined() + expect(stateForBlockDecoration(presenter, blockDecoration3)).toBeUndefined() + + editor.setSelectedScreenRange([[10, 0], [12, 0]]) + editor.delete() + presenter.setScrollTop(0) # deleting the buffer causes the editor to autoscroll + + expect(stateForBlockDecoration(presenter, blockDecoration1)).toBeUndefined() + expectValues stateForBlockDecoration(presenter, blockDecoration2), { + decoration: blockDecoration2 + screenRow: 10 + isVisible: false + } + expectValues stateForBlockDecoration(presenter, blockDecoration3), { + decoration: blockDecoration3 + screenRow: 10 + isVisible: false + } + it "invalidates all block decorations when content frame width, window size or bounding client rect change", -> blockDecoration1 = addBlockDecorationForScreenRow(11) presenter = buildPresenter(explicitHeight: 30, lineHeight: 10, tileSize: 2, scrollTop: 0) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 9a29ec922..73848bdb3 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -1288,8 +1288,10 @@ class TextEditorPresenter newExtent = Point(end - start + screenDelta, 0) invalidatedBlockDecorationIds = @lineTopIndex.splice(Point(start, 0), oldExtent, newExtent) invalidatedBlockDecorationIds.forEach (id) => - newScreenPosition = @model.decorationForId(id).getMarker().getHeadScreenPosition() + decoration = @model.decorationForId(id) + newScreenPosition = decoration.getMarker().getHeadScreenPosition() @lineTopIndex.moveBlock(id, newScreenPosition) + @invalidatedDimensionsByBlockDecoration.add(decoration) didAddBlockDecoration: (decoration) -> return if not decoration.isType('block') or @observedBlockDecorations.has(decoration) From ed92db1f43b573c25669b3d68247e7fd837bf26f Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 28 Dec 2015 13:05:29 -0500 Subject: [PATCH 266/502] Provide messages for all these steps. --- script/bootstrap | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/script/bootstrap b/script/bootstrap index 7e5b534c7..220c0b4f3 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -96,9 +96,18 @@ function bootstrap() { message: 'Installing apm...', options: apmInstallOptions }, - apmPath + ' clean' + apmFlags, - moduleInstallCommand, - dedupeApmCommand + ' ' + packagesToDedupe.join(' '), + { + command: apmPath + ' clean' + apmFlags, + message: 'Deleting old packages...' + }, + { + command: moduleInstallCommand, + message: 'Installing modules...', + }, + { + command: dedupeApmCommand + ' ' + packagesToDedupe.join(' '), + message: 'Deduping modules...' + } ]; process.chdir(path.dirname(__dirname)); From 8ac72db0b2d8191854f698a05c1c88100fd6220e Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 28 Dec 2015 13:09:38 -0500 Subject: [PATCH 267/502] Get the Electron version from our package.json. --- .npmrc | 3 --- script/bootstrap | 11 +++++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.npmrc b/.npmrc index 6fc4bd7ae..c5ff09782 100644 --- a/.npmrc +++ b/.npmrc @@ -1,4 +1 @@ cache = ~/.atom/.npm -target = 0.34.5 -runtime = electron -disturl = https://atom.io/download/atom-shell diff --git a/script/bootstrap b/script/bootstrap index 220c0b4f3..ed4411272 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -4,6 +4,7 @@ var fs = require('fs'); var verifyRequirements = require('./utils/verify-requirements'); var safeExec = require('./utils/child-process-wrapper.js').safeExec; var path = require('path'); +var _ = require('underscore-plus'); // Executes an array of commands one by one. function executeCommands(commands, done, index) { @@ -71,6 +72,15 @@ function bootstrap() { var moduleInstallCommand = apmPath + ' install' + apmFlags; var dedupeApmCommand = apmPath + ' dedupe' + apmFlags; + var electronVersion = require('../package.json').electronVersion; + var moduleInstallOptions = {env: _.extend({}, process.env, + // `node-pre-gyp` will look for these when determining which binary to + // download or how to rebuild. + npm_config_target: electronVersion, + npm_config_runtime: 'electron', + npm_config_disturl: 'https://atom.io/download/atom-shell' + )} + if (process.argv.indexOf('--no-quiet') === -1) { buildInstallCommand += ' --loglevel error'; apmInstallCommand += ' --loglevel error'; @@ -103,6 +113,7 @@ function bootstrap() { { command: moduleInstallCommand, message: 'Installing modules...', + options: moduleInstallOptions }, { command: dedupeApmCommand + ' ' + packagesToDedupe.join(' '), From 11180cea1c77544f4a975a1bf573a95ccce237db Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 28 Dec 2015 13:11:33 -0500 Subject: [PATCH 268/502] Syntax --- script/bootstrap | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/bootstrap b/script/bootstrap index ed4411272..930c5bcd0 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -73,13 +73,13 @@ function bootstrap() { var dedupeApmCommand = apmPath + ' dedupe' + apmFlags; var electronVersion = require('../package.json').electronVersion; - var moduleInstallOptions = {env: _.extend({}, process.env, + var moduleInstallOptions = {env: _.extend({}, process.env, { // `node-pre-gyp` will look for these when determining which binary to // download or how to rebuild. npm_config_target: electronVersion, npm_config_runtime: 'electron', npm_config_disturl: 'https://atom.io/download/atom-shell' - )} + })} if (process.argv.indexOf('--no-quiet') === -1) { buildInstallCommand += ' --loglevel error'; From 71bf3a7546cd439b14da3db435ed699ab633ea4e Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 28 Dec 2015 13:16:22 -0500 Subject: [PATCH 269/502] How about this. --- script/bootstrap | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/script/bootstrap b/script/bootstrap index 930c5bcd0..e6111db5c 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -4,7 +4,6 @@ var fs = require('fs'); var verifyRequirements = require('./utils/verify-requirements'); var safeExec = require('./utils/child-process-wrapper.js').safeExec; var path = require('path'); -var _ = require('underscore-plus'); // Executes an array of commands one by one. function executeCommands(commands, done, index) { @@ -73,13 +72,15 @@ function bootstrap() { var dedupeApmCommand = apmPath + ' dedupe' + apmFlags; var electronVersion = require('../package.json').electronVersion; - var moduleInstallOptions = {env: _.extend({}, process.env, { - // `node-pre-gyp` will look for these when determining which binary to - // download or how to rebuild. - npm_config_target: electronVersion, - npm_config_runtime: 'electron', - npm_config_disturl: 'https://atom.io/download/atom-shell' - })} + var moduleInstallEnv = {}; + for (var e in process.env) { + moduleInstallEnv[e] = process.env[e]; + } + // `node-pre-gyp` will look for these when determining which binary to + // download or how to rebuild. + moduleInstallEnv.npm_config_target = electronVersion; + moduleInstallEnv.npm_config_runtime = 'electron'; + moduleInstallEnv.npm_config_disturl = 'https://atom.io/download/atom-shell'; if (process.argv.indexOf('--no-quiet') === -1) { buildInstallCommand += ' --loglevel error'; From 91ff9079d45a1d4a50c4e503da4595c04d25a5f1 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 28 Dec 2015 13:17:06 -0500 Subject: [PATCH 270/502] Just pass through the environment. --- script/bootstrap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/bootstrap b/script/bootstrap index e6111db5c..fbdfa13d2 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -114,7 +114,7 @@ function bootstrap() { { command: moduleInstallCommand, message: 'Installing modules...', - options: moduleInstallOptions + options: {env: moduleInstallEnv} }, { command: dedupeApmCommand + ' ' + packagesToDedupe.join(' '), From 2ccbdee48fac79710ac22e034d4a0026cf16454d Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 28 Dec 2015 13:29:28 -0500 Subject: [PATCH 271/502] These commands will log, so no need to do it ourselves. --- script/bootstrap | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/script/bootstrap b/script/bootstrap index fbdfa13d2..e0f92ec7a 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -113,13 +113,9 @@ function bootstrap() { }, { command: moduleInstallCommand, - message: 'Installing modules...', options: {env: moduleInstallEnv} }, - { - command: dedupeApmCommand + ' ' + packagesToDedupe.join(' '), - message: 'Deduping modules...' - } + command: dedupeApmCommand + ' ' + packagesToDedupe.join(' ') ]; process.chdir(path.dirname(__dirname)); From 9773adc8f803e2619fe581ff285be40acac0260d Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 28 Dec 2015 14:28:50 -0500 Subject: [PATCH 272/502] Not an object anymore. --- script/bootstrap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/bootstrap b/script/bootstrap index e0f92ec7a..759b32728 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -115,7 +115,7 @@ function bootstrap() { command: moduleInstallCommand, options: {env: moduleInstallEnv} }, - command: dedupeApmCommand + ' ' + packagesToDedupe.join(' ') + dedupeApmCommand + ' ' + packagesToDedupe.join(' ') ]; process.chdir(path.dirname(__dirname)); From f23d9330fa159b75dfef88cfdfc976c4d3890578 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 28 Dec 2015 14:39:02 -0500 Subject: [PATCH 273/502] Try passing them as args. --- script/bootstrap | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/script/bootstrap b/script/bootstrap index 759b32728..3c517f509 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -68,19 +68,10 @@ function bootstrap() { var buildInstallOptions = {cwd: path.resolve(__dirname, '..', 'build')}; var apmInstallCommand = npmPath + npmFlags + '--target=0.10.35 ' + 'install'; var apmInstallOptions = {cwd: apmInstallPath}; - var moduleInstallCommand = apmPath + ' install' + apmFlags; var dedupeApmCommand = apmPath + ' dedupe' + apmFlags; var electronVersion = require('../package.json').electronVersion; - var moduleInstallEnv = {}; - for (var e in process.env) { - moduleInstallEnv[e] = process.env[e]; - } - // `node-pre-gyp` will look for these when determining which binary to - // download or how to rebuild. - moduleInstallEnv.npm_config_target = electronVersion; - moduleInstallEnv.npm_config_runtime = 'electron'; - moduleInstallEnv.npm_config_disturl = 'https://atom.io/download/atom-shell'; + var moduleInstallCommand = apmPath + ' install ' + '--target=' + electronVersion + ' --runtime=electron --disturl=https://atom.io/download/atom-shell ' + apmFlags; if (process.argv.indexOf('--no-quiet') === -1) { buildInstallCommand += ' --loglevel error'; @@ -113,7 +104,6 @@ function bootstrap() { }, { command: moduleInstallCommand, - options: {env: moduleInstallEnv} }, dedupeApmCommand + ' ' + packagesToDedupe.join(' ') ]; From be9d74adbfd295aeb3d0ff2314acdb0d1c9df5e4 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 28 Dec 2015 15:10:01 -0500 Subject: [PATCH 274/502] Back to using npmrc. --- .npmrc | 3 +++ script/bootstrap | 15 ++++----------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/.npmrc b/.npmrc index c5ff09782..6fc4bd7ae 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,4 @@ cache = ~/.atom/.npm +target = 0.34.5 +runtime = electron +disturl = https://atom.io/download/atom-shell diff --git a/script/bootstrap b/script/bootstrap index 3c517f509..7e5b534c7 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -68,11 +68,9 @@ function bootstrap() { var buildInstallOptions = {cwd: path.resolve(__dirname, '..', 'build')}; var apmInstallCommand = npmPath + npmFlags + '--target=0.10.35 ' + 'install'; var apmInstallOptions = {cwd: apmInstallPath}; + var moduleInstallCommand = apmPath + ' install' + apmFlags; var dedupeApmCommand = apmPath + ' dedupe' + apmFlags; - var electronVersion = require('../package.json').electronVersion; - var moduleInstallCommand = apmPath + ' install ' + '--target=' + electronVersion + ' --runtime=electron --disturl=https://atom.io/download/atom-shell ' + apmFlags; - if (process.argv.indexOf('--no-quiet') === -1) { buildInstallCommand += ' --loglevel error'; apmInstallCommand += ' --loglevel error'; @@ -98,14 +96,9 @@ function bootstrap() { message: 'Installing apm...', options: apmInstallOptions }, - { - command: apmPath + ' clean' + apmFlags, - message: 'Deleting old packages...' - }, - { - command: moduleInstallCommand, - }, - dedupeApmCommand + ' ' + packagesToDedupe.join(' ') + apmPath + ' clean' + apmFlags, + moduleInstallCommand, + dedupeApmCommand + ' ' + packagesToDedupe.join(' '), ]; process.chdir(path.dirname(__dirname)); From 1eceae96cc4172ea769dad4aac17f1005b855e1b Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 28 Dec 2015 15:42:19 -0500 Subject: [PATCH 275/502] Once more, with feeling. --- .npmrc | 3 --- script/bootstrap | 27 ++++++++++++++++++++++++--- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/.npmrc b/.npmrc index 6fc4bd7ae..c5ff09782 100644 --- a/.npmrc +++ b/.npmrc @@ -1,4 +1 @@ cache = ~/.atom/.npm -target = 0.34.5 -runtime = electron -disturl = https://atom.io/download/atom-shell diff --git a/script/bootstrap b/script/bootstrap index 7e5b534c7..fc2df36bd 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -71,6 +71,17 @@ function bootstrap() { var moduleInstallCommand = apmPath + ' install' + apmFlags; var dedupeApmCommand = apmPath + ' dedupe' + apmFlags; + var moduleInstallEnv = {}; + for (var e in process.env) { + moduleInstallEnv[e] = process.env[e]; + } + + var electronVersion = require('../package.json').electronVersion; + moduleInstallEnv.npm_config_target = electronVersion; + moduleInstallEnv.npm_config_runtime = 'electron'; + moduleInstallEnv.npm_config_disturl = 'https://atom.io/download/atom-shell'; + var moduleInstallOptions = {env: moduleInstallEnv}; + if (process.argv.indexOf('--no-quiet') === -1) { buildInstallCommand += ' --loglevel error'; apmInstallCommand += ' --loglevel error'; @@ -96,9 +107,19 @@ function bootstrap() { message: 'Installing apm...', options: apmInstallOptions }, - apmPath + ' clean' + apmFlags, - moduleInstallCommand, - dedupeApmCommand + ' ' + packagesToDedupe.join(' '), + { + command: apmPath + ' clean' + apmFlags, + message: 'Deleting old modules...', + options: moduleInstallOptions + }, + { + command: moduleInstallCommand, + options: moduleInstallOptions + }, + { + command: dedupeApmCommand + ' ' + packagesToDedupe.join(' '), + options: moduleInstallOptions + } ]; process.chdir(path.dirname(__dirname)); From bf0f6afe20aae0472b71accd05736fc165b6d9e2 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 28 Dec 2015 16:18:29 -0500 Subject: [PATCH 276/502] Log the environment. --- script/bootstrap | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/script/bootstrap b/script/bootstrap index fc2df36bd..983d0f033 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -73,7 +73,7 @@ function bootstrap() { var moduleInstallEnv = {}; for (var e in process.env) { - moduleInstallEnv[e] = process.env[e]; + // moduleInstallEnv[e] = process.env[e]; } var electronVersion = require('../package.json').electronVersion; @@ -82,6 +82,9 @@ function bootstrap() { moduleInstallEnv.npm_config_disturl = 'https://atom.io/download/atom-shell'; var moduleInstallOptions = {env: moduleInstallEnv}; + console.log('env:') + console.log(moduleInstallEnv) + if (process.argv.indexOf('--no-quiet') === -1) { buildInstallCommand += ' --loglevel error'; apmInstallCommand += ' --loglevel error'; From a7ba6c86386168c9b893977567856553dfdcc347 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 28 Dec 2015 16:21:37 -0500 Subject: [PATCH 277/502] Try args again. --- script/bootstrap | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/script/bootstrap b/script/bootstrap index 983d0f033..dfeb47b9d 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -71,19 +71,8 @@ function bootstrap() { var moduleInstallCommand = apmPath + ' install' + apmFlags; var dedupeApmCommand = apmPath + ' dedupe' + apmFlags; - var moduleInstallEnv = {}; - for (var e in process.env) { - // moduleInstallEnv[e] = process.env[e]; - } - var electronVersion = require('../package.json').electronVersion; - moduleInstallEnv.npm_config_target = electronVersion; - moduleInstallEnv.npm_config_runtime = 'electron'; - moduleInstallEnv.npm_config_disturl = 'https://atom.io/download/atom-shell'; - var moduleInstallOptions = {env: moduleInstallEnv}; - - console.log('env:') - console.log(moduleInstallEnv) + apmFlags += ' --target=' + electronVersion + ' --runtime=electron --dist-url=https://atom.io/download/atom-shell'; if (process.argv.indexOf('--no-quiet') === -1) { buildInstallCommand += ' --loglevel error'; @@ -113,15 +102,12 @@ function bootstrap() { { command: apmPath + ' clean' + apmFlags, message: 'Deleting old modules...', - options: moduleInstallOptions }, { command: moduleInstallCommand, - options: moduleInstallOptions }, { command: dedupeApmCommand + ' ' + packagesToDedupe.join(' '), - options: moduleInstallOptions } ]; From 666958d6b93832de568e790982a3beff801cb4c0 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 28 Dec 2015 16:50:00 -0500 Subject: [PATCH 278/502] Try just setting the target? --- .npmrc | 2 ++ script/bootstrap | 11 ++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.npmrc b/.npmrc index c5ff09782..9fa375bb0 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,3 @@ cache = ~/.atom/.npm +runtime = electron +disturl = https://atom.io/download/atom-shell diff --git a/script/bootstrap b/script/bootstrap index dfeb47b9d..b01f033c8 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -71,8 +71,14 @@ function bootstrap() { var moduleInstallCommand = apmPath + ' install' + apmFlags; var dedupeApmCommand = apmPath + ' dedupe' + apmFlags; + var moduleInstallEnv = {}; + for (var e in process.env) { + moduleInstallEnv[e] = process.env[e]; + } + var electronVersion = require('../package.json').electronVersion; - apmFlags += ' --target=' + electronVersion + ' --runtime=electron --dist-url=https://atom.io/download/atom-shell'; + moduleInstallEnv.npm_config_target = electronVersion; + var moduleInstallOptions = {env: moduleInstallEnv}; if (process.argv.indexOf('--no-quiet') === -1) { buildInstallCommand += ' --loglevel error'; @@ -102,12 +108,15 @@ function bootstrap() { { command: apmPath + ' clean' + apmFlags, message: 'Deleting old modules...', + options: moduleInstallOptions }, { command: moduleInstallCommand, + options: moduleInstallOptions }, { command: dedupeApmCommand + ' ' + packagesToDedupe.join(' '), + options: moduleInstallOptions } ]; From f04fee2ad9d52178d5e53e34ddcece7f394dedc3 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 28 Dec 2015 17:23:44 -0500 Subject: [PATCH 279/502] Comment why. --- script/bootstrap | 2 ++ 1 file changed, 2 insertions(+) diff --git a/script/bootstrap b/script/bootstrap index b01f033c8..2872d1141 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -76,6 +76,8 @@ function bootstrap() { moduleInstallEnv[e] = process.env[e]; } + // Set our target (Electron) version so that node-pre-gyp can download the + // proper binaries. var electronVersion = require('../package.json').electronVersion; moduleInstallEnv.npm_config_target = electronVersion; var moduleInstallOptions = {env: moduleInstallEnv}; From f4138018364161a0821c3526d35ee0b8d0c0d3f1 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 29 Dec 2015 17:10:01 -0500 Subject: [PATCH 280/502] Remove items that don't have any changes. --- src/git-repository-async.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index b26e2f097..8c7364fe9 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -458,7 +458,12 @@ export default class GitRepositoryAsync { const cachedStatus = this.pathStatusCache[relativePath] || 0 const status = statuses[0] ? statuses[0].statusBit() : Git.Status.STATUS.CURRENT if (status !== cachedStatus) { - this.pathStatusCache[relativePath] = status + if (status === Git.Status.STATUS.CURRENT) { + delete this.pathStatusCache[relativePath] + } else { + this.pathStatusCache[relativePath] = status + } + this.emitter.emit('did-change-status', {path: _path, pathStatus: status}) } From 34ab7983620911ad2b578ebafa3d8cb5217841aa Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 29 Dec 2015 17:10:09 -0500 Subject: [PATCH 281/502] Added getCachedPathStatuses() --- src/git-repository-async.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 8c7364fe9..c8837e893 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -492,6 +492,14 @@ export default class GitRepositoryAsync { .then(relativePath => this.pathStatusCache[relativePath]) } + // Public: Get the cached statuses for the repository. + // + // Returns an {Object} of {Number} statuses, keyed by {String} working + // directory-relative file names. + getCachedPathStatuses () { + return this.pathStatusCache + } + // Public: Returns true if the given status indicates modification. // // * `statusBit` A {Number} representing the status. From 52ec4c7c5c760a70bc3b5b5e6fe610842bfc67a3 Mon Sep 17 00:00:00 2001 From: Harold Pimentel Date: Tue, 29 Dec 2015 20:50:13 -0800 Subject: [PATCH 282/502] update documentation for rowRangeForParagraphAtBufferRow #vc --- src/cursor.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cursor.coffee b/src/cursor.coffee index 5b3b23b73..9a8e9f6d0 100644 --- a/src/cursor.coffee +++ b/src/cursor.coffee @@ -551,7 +551,7 @@ class Cursor extends Model # Public: Retrieves the range for the current paragraph. # - # A paragraph is defined as a block of text surrounded by empty lines. + # A paragraph is defined as a block of text surrounded by empty lines or comments. # # Returns a {Range}. getCurrentParagraphBufferRange: -> From 1548ce8ae6a5ba05e38d6ef1c6af3cf3f26813a6 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 30 Dec 2015 14:43:56 +0000 Subject: [PATCH 283/502] Update deprecated-packages.json --- build/deprecated-packages.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build/deprecated-packages.json b/build/deprecated-packages.json index 4a8b53442..08f4d1186 100644 --- a/build/deprecated-packages.json +++ b/build/deprecated-packages.json @@ -862,6 +862,10 @@ "hasDeprecations": true, "latestHasDeprecations": false }, + "language-jxa": { + "hasDeprecations": true, + "latestHasDeprecations": true + }, "language-rspec": { "version": "<=0.2.1", "hasDeprecations": true, From 3af5e8cc1882418ab9b732ed249d2f96cf446852 Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 30 Dec 2015 12:10:33 -0500 Subject: [PATCH 284/502] Fix path standardization on case-sensitive file systems. --- src/git-repository-async.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index c8837e893..d3ba9897b 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -194,17 +194,16 @@ export default class GitRepositoryAsync { workingDirectory = `${workingDirectory}/` } + const originalPath = _path if (this.isCaseInsensitive) { - const lowerCasePath = _path.toLowerCase() - + _path = _path.toLowerCase() workingDirectory = workingDirectory.toLowerCase() - if (lowerCasePath.indexOf(workingDirectory) === 0) { - return _path.substring(workingDirectory.length) - } else { - if (lowerCasePath === workingDirectory) { - return '' - } - } + } + + if (_path.indexOf(workingDirectory) === 0) { + return originalPath.substring(workingDirectory.length) + } else if (_path === workingDirectory) { + return '' } return _path From 28326b3674b183fc425c7c220152fc92069f7d65 Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 30 Dec 2015 12:11:06 -0500 Subject: [PATCH 285/502] Strip any leading /private/. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I don’t love that we have to do this manually, but I also can’t find any node function that’ll do it for us :( --- src/git-repository-async.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index d3ba9897b..f15bdf439 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -182,6 +182,11 @@ export default class GitRepositoryAsync { return _path } + // Depending on where the paths come from, they may have a '/private/' + // prefix. Standardize by stripping that out. + _path = _path.replace(/^\/private\//, '/') + workingDirectory = workingDirectory.replace(/^\/private\//, '/') + if (process.platform === 'win32') { _path = _path.replace(/\\/g, '/') } else { From c1d00716ee59e144624462e4cb5335e7604cca54 Mon Sep 17 00:00:00 2001 From: Leonard Lamprecht Date: Sat, 2 Jan 2016 18:36:06 +0100 Subject: [PATCH 286/502] Combine certain items into a submenu --- menus/darwin.cson | 15 ++++++++++----- menus/linux.cson | 15 ++++++++++----- menus/win32.cson | 15 ++++++++++----- 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/menus/darwin.cson b/menus/darwin.cson index 52b7a5bc8..6737adf46 100644 --- a/menus/darwin.cson +++ b/menus/darwin.cson @@ -11,11 +11,16 @@ { label: 'Downloading Update', enabled: false, visible: false} { type: 'separator' } { label: 'Preferences…', command: 'application:show-settings' } - { label: 'Open Your Config', command: 'application:open-your-config' } - { label: 'Open Your Init Script', command: 'application:open-your-init-script' } - { label: 'Open Your Keymap', command: 'application:open-your-keymap' } - { label: 'Open Your Snippets', command: 'application:open-your-snippets' } - { label: 'Open Your Stylesheet', command: 'application:open-your-stylesheet' } + { + label: 'Customization', + submenu: [ + { label: 'Config', command: 'application:open-your-config' } + { label: 'Init Script', command: 'application:open-your-init-script' } + { label: 'Keymap', command: 'application:open-your-keymap' } + { label: 'Snippets', command: 'application:open-your-snippets' } + { label: 'Stylesheet', command: 'application:open-your-stylesheet' } + ] + } { type: 'separator' } { label: 'Install Shell Commands', command: 'window:install-shell-commands' } { type: 'separator' } diff --git a/menus/linux.cson b/menus/linux.cson index fa831b4a4..416f16831 100644 --- a/menus/linux.cson +++ b/menus/linux.cson @@ -83,11 +83,16 @@ } { type: 'separator' } { label: '&Preferences', command: 'application:show-settings' } - { label: 'Open Your Config', command: 'application:open-your-config' } - { label: 'Open Your Init Script', command: 'application:open-your-init-script' } - { label: 'Open Your Keymap', command: 'application:open-your-keymap' } - { label: 'Open Your Snippets', command: 'application:open-your-snippets' } - { label: 'Open Your Stylesheet', command: 'application:open-your-stylesheet' } + { + label: 'Customization', + submenu: [ + { label: 'Open Your Config', command: 'application:open-your-config' } + { label: 'Open Your Init Script', command: 'application:open-your-init-script' } + { label: 'Open Your Keymap', command: 'application:open-your-keymap' } + { label: 'Open Your Snippets', command: 'application:open-your-snippets' } + { label: 'Open Your Stylesheet', command: 'application:open-your-stylesheet' } + ] + } { type: 'separator' } ] } diff --git a/menus/win32.cson b/menus/win32.cson index 04da3d388..82921d2a3 100644 --- a/menus/win32.cson +++ b/menus/win32.cson @@ -10,11 +10,16 @@ { label: 'Reopen Last &Item', command: 'pane:reopen-closed-item' } { type: 'separator' } { label: 'Se&ttings', command: 'application:show-settings' } - { label: 'Open Your Config', command: 'application:open-your-config' } - { label: 'Open Your Init Script', command: 'application:open-your-init-script' } - { label: 'Open Your Keymap', command: 'application:open-your-keymap' } - { label: 'Open Your Snippets', command: 'application:open-your-snippets' } - { label: 'Open Your Stylesheet', command: 'application:open-your-stylesheet' } + { + label: 'Customization', + submenu: [ + { label: 'Open Your Config', command: 'application:open-your-config' } + { label: 'Open Your Init Script', command: 'application:open-your-init-script' } + { label: 'Open Your Keymap', command: 'application:open-your-keymap' } + { label: 'Open Your Snippets', command: 'application:open-your-snippets' } + { label: 'Open Your Stylesheet', command: 'application:open-your-stylesheet' } + ] + } { type: 'separator' } { label: '&Save', command: 'core:save' } { label: 'Save &As…', command: 'core:save-as' } From 07eebdccb857f81b65c2299ed533e9fa7728cfb8 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 4 Jan 2016 11:26:58 -0500 Subject: [PATCH 287/502] Use .openExt instead of .open. Use openExt to open the repository so we get the same repo-searching behavior we had with git-utils. --- src/git-repository-async.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index f15bdf439..fe4408685 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -34,7 +34,7 @@ export default class GitRepositoryAsync { this.emitter = new Emitter() this.subscriptions = new CompositeDisposable() this.pathStatusCache = {} - this.repoPromise = Git.Repository.open(_path) + this.repoPromise = Git.Repository.openExt(_path) this.isCaseInsensitive = fs.isCaseInsensitive() this.upstreamByPath = {} From 73cb867ccd3f734cbd4fa6aec682c32c164710dc Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 4 Jan 2016 12:14:08 -0500 Subject: [PATCH 288/502] Do proper path-filtering for status. --- src/git-repository-async.js | 51 ++++++++++++++----------------------- 1 file changed, 19 insertions(+), 32 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index fe4408685..9eb643cc8 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -387,7 +387,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to a {Boolean} that's true if the `path` // is modified. isPathModified (_path) { - return this._filterStatusesByPath(_path) + return this._getStatus([_path]) .then(statuses => statuses.some(status => status.isModified())) } @@ -398,7 +398,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to a {Boolean} that's true if the `path` // is new. isPathNew (_path) { - return this._filterStatusesByPath(_path) + return this._getStatus([_path]) .then(statuses => statuses.some(status => status.isNew())) } @@ -425,11 +425,10 @@ export default class GitRepositoryAsync { // value can be passed to {::isStatusModified} or {::isStatusNew} to get more // information. getDirectoryStatus (directoryPath) { - // XXX _filterSBD already gets repoPromise return this.repoPromise .then(repo => { const relativePath = this.relativize(directoryPath, repo.workdir()) - return this._filterStatusesByDirectory(relativePath) + return this._getStatus([relativePath]) }) .then(statuses => { return Promise.all(statuses.map(s => s.statusBit())).then(bits => { @@ -456,7 +455,7 @@ export default class GitRepositoryAsync { return this.repoPromise .then(repo => { relativePath = this.relativize(_path, repo.workdir()) - return this._filterStatusesByPath(_path) + return this._getStatus([relativePath]) }) .then(statuses => { const cachedStatus = this.pathStatusCache[relativePath] || 0 @@ -778,7 +777,6 @@ export default class GitRepositoryAsync { return this.repoPromise .then(repo => repo.getStatus()) .then(statuses => { - // update the status cache const statusPairs = statuses.map(status => [status.path(), status.statusBit()]) return Promise.all(statusPairs) .then(statusesByPath => _.object(statusesByPath)) @@ -873,36 +871,25 @@ export default class GitRepositoryAsync { this.subscriptions.add(bufferSubscriptions) } - // Get the status for the given path. + // Get the status for the given paths. // - // * `path` The {String} path whose status is wanted. - // - // Returns a {Promise} which resolves to the {NodeGit.StatusFile} status for - // the path. - _filterStatusesByPath (_path) { - // TODO: Is there a more efficient way to do this? - let basePath = null - return this.repoPromise - .then(repo => { - basePath = repo.workdir() - return repo.getStatus() - }) - .then(statuses => { - return statuses.filter(status => _path === path.join(basePath, status.path())) - }) - } - - // Get the status for everything in the given directory. - // - // * `directoryPath` The {String} directory whose status is wanted. + // * `paths` The {String} paths whose status is wanted. If undefined, get the + // status for the whole repository. // // Returns a {Promise} which resolves to an {Array} of {NodeGit.StatusFile} - // statuses for every file in the directory. - _filterStatusesByDirectory (directoryPath) { + // statuses for the paths. + _getStatus (paths) { return this.repoPromise - .then(repo => repo.getStatus()) - .then(statuses => { - return statuses.filter(status => status.path().indexOf(directoryPath) === 0) + .then(repo => { + const opts = { + flags: Git.Status.OPT.INCLUDE_UNTRACKED | Git.Status.OPT.RECURSE_UNTRACKED_DIRS | Git.Status.OPT.DISABLE_PATHSPEC_MATCH + } + + if (paths) { + opts.pathspec = paths + } + + return repo.getStatus(opts) }) } } From d8a5418f1e068370397c7571399bb06e378bb336 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 4 Jan 2016 12:14:24 -0500 Subject: [PATCH 289/502] Only refresh the status for the open project. --- src/git-repository-async.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 9eb643cc8..bfe3bb1ab 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -774,8 +774,14 @@ export default class GitRepositoryAsync { _refreshStatus () { this._refreshingCount++ - return this.repoPromise - .then(repo => repo.getStatus()) + const projectPathsPromises = this.project.getPaths() + .map(p => this.relativizeToWorkingDirectory(p)) + + Promise.all(projectPathsPromises) + .then(paths => paths.filter(p => p.length > 0)) + .then(projectPaths => { + return this._getStatus(projectPaths.length > 0 ? projectPaths : null) + }) .then(statuses => { const statusPairs = statuses.map(status => [status.path(), status.statusBit()]) return Promise.all(statusPairs) From d9b39323cf01273e240cccbf0dab024416856e55 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 4 Jan 2016 12:20:54 -0500 Subject: [PATCH 290/502] Fix the openExt call. --- src/git-repository-async.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index bfe3bb1ab..9f414ecf9 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -34,7 +34,7 @@ export default class GitRepositoryAsync { this.emitter = new Emitter() this.subscriptions = new CompositeDisposable() this.pathStatusCache = {} - this.repoPromise = Git.Repository.openExt(_path) + this.repoPromise = Git.Repository.openExt(_path, 0, '') this.isCaseInsensitive = fs.isCaseInsensitive() this.upstreamByPath = {} From 1a35545bd70022eb348c2878c4d97981b2ed648e Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 4 Jan 2016 12:48:29 -0500 Subject: [PATCH 291/502] _destroyed => _isDestroyed --- src/git-repository-async.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 9f414ecf9..658d3ddbd 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -780,6 +780,8 @@ export default class GitRepositoryAsync { Promise.all(projectPathsPromises) .then(paths => paths.filter(p => p.length > 0)) .then(projectPaths => { + if (this._isDestroyed()) return [] + return this._getStatus(projectPaths.length > 0 ? projectPaths : null) }) .then(statuses => { @@ -818,7 +820,7 @@ export default class GitRepositoryAsync { // // Returns a {Promise} which resolves to the {NodeGit.Repository}. getRepo (_path) { - if (this._destroyed()) { + if (this._isDestroyed()) { const error = new Error('Repository has been destroyed') error.name = GitRepositoryAsync.DestroyedErrorName return Promise.reject(error) @@ -849,7 +851,7 @@ export default class GitRepositoryAsync { // Has the repository been destroyed? // // Returns a {Boolean}. - _destroyed () { + _isDestroyed () { return this.repoPromise == null } From e3451090ed45b5b7122417dbe5409a051ddc29ec Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 4 Jan 2016 12:48:39 -0500 Subject: [PATCH 292/502] We might not have a project. --- src/git-repository-async.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 658d3ddbd..394a158ba 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -774,8 +774,11 @@ export default class GitRepositoryAsync { _refreshStatus () { this._refreshingCount++ - const projectPathsPromises = this.project.getPaths() - .map(p => this.relativizeToWorkingDirectory(p)) + let projectPathsPromises = Promise.resolve([]) + if (this.project) { + projectPathsPromises = this.project.getPaths() + .map(p => this.relativizeToWorkingDirectory(p)) + } Promise.all(projectPathsPromises) .then(paths => paths.filter(p => p.length > 0)) From 5a7b4562e19adcc606d3fa75d1d886be40f41dbd Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 4 Jan 2016 13:14:02 -0500 Subject: [PATCH 293/502] Use a default parameter. --- src/git-repository-async.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 394a158ba..430423ac8 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -29,7 +29,7 @@ export default class GitRepositoryAsync { return 'GitRepositoryAsync.destroyed' } - constructor (_path, options) { + constructor (_path, options = {}) { this.repo = null this.emitter = new Emitter() this.subscriptions = new CompositeDisposable() @@ -40,8 +40,6 @@ export default class GitRepositoryAsync { this._refreshingCount = 0 - options = options || {} - let {refreshOnWindowFocus = true} = options if (refreshOnWindowFocus) { const onWindowFocus = () => this.refreshStatus() From 60fc0acb715132bf7d70244bfb5db45585f20d1f Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 4 Jan 2016 13:25:51 -0500 Subject: [PATCH 294/502] Relativize these paths too. --- src/git-repository-async.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 430423ac8..d9a0c3214 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -385,7 +385,8 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to a {Boolean} that's true if the `path` // is modified. isPathModified (_path) { - return this._getStatus([_path]) + return this.relativizeToWorkingDirectory(_path) + .then(relativePath => this._getStatus([relativePath])) .then(statuses => statuses.some(status => status.isModified())) } @@ -396,7 +397,8 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to a {Boolean} that's true if the `path` // is new. isPathNew (_path) { - return this._getStatus([_path]) + return this.relativizeToWorkingDirectory(_path) + .then(relativePath => this._getStatus([relativePath])) .then(statuses => statuses.some(status => status.isNew())) } From 843442a74178e23a5cbf4175b17d894dc269a1dd Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 4 Jan 2016 13:25:59 -0500 Subject: [PATCH 295/502] So much depends upon a return statement. --- src/git-repository-async.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index d9a0c3214..fffd443a9 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -774,17 +774,15 @@ export default class GitRepositoryAsync { _refreshStatus () { this._refreshingCount++ - let projectPathsPromises = Promise.resolve([]) + let projectPathsPromises = [Promise.resolve('')] if (this.project) { projectPathsPromises = this.project.getPaths() .map(p => this.relativizeToWorkingDirectory(p)) } - Promise.all(projectPathsPromises) + return Promise.all(projectPathsPromises) .then(paths => paths.filter(p => p.length > 0)) .then(projectPaths => { - if (this._isDestroyed()) return [] - return this._getStatus(projectPaths.length > 0 ? projectPaths : null) }) .then(statuses => { From 3752f6616021a5c2caf7a42950719af5ab74d406 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 4 Jan 2016 13:27:55 -0500 Subject: [PATCH 296/502] Use .getRepo everywhere. --- src/git-repository-async.js | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index fffd443a9..1bc0c1684 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -127,13 +127,13 @@ export default class GitRepositoryAsync { // Public: Returns a {Promise} which resolves to the {String} path of the // repository. getPath () { - return this.repoPromise.then(repo => repo.path().replace(/\/$/, '')) + return this.getRepo().then(repo => repo.path().replace(/\/$/, '')) } // Public: Returns a {Promise} which resolves to the {String} working // directory path of the repository. getWorkingDirectory () { - return this.repoPromise.then(repo => repo.workdir()) + return this.getRepo().then(repo => repo.workdir()) } // Public: Returns a {Promise} that resolves to true if at the root, false if @@ -142,7 +142,7 @@ export default class GitRepositoryAsync { if (!this.project) return Promise.resolve(false) if (!this.projectAtRoot) { - this.projectAtRoot = this.repoPromise + this.projectAtRoot = this.getRepo() .then(repo => this.project.relativize(repo.workdir())) .then(relativePath => relativePath === '') } @@ -156,7 +156,7 @@ export default class GitRepositoryAsync { // // Returns a {Promise} which resolves to the relative {String} path. relativizeToWorkingDirectory (_path) { - return this.repoPromise + return this.getRepo() .then(repo => this.relativize(_path, repo.workdir())) } @@ -215,7 +215,7 @@ export default class GitRepositoryAsync { // Public: Returns a {Promise} which resolves to whether the given branch // exists. hasBranch (branch) { - return this.repoPromise + return this.getRepo() .then(repo => repo.getBranch(branch)) .then(branch => branch != null) .catch(_ => false) @@ -244,7 +244,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} that resolves true if the given path is a submodule in // the repository. isSubmodule (_path) { - return this.repoPromise + return this.getRepo() .then(repo => repo.openIndex()) .then(index => Promise.all([index, this.relativizeToWorkingDirectory(_path)])) .then(([index, relativePath]) => { @@ -409,7 +409,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to a {Boolean} that's true if the `path` // is ignored. isPathIgnored (_path) { - return this.repoPromise + return this.getRepo() .then(repo => { const relativePath = this.relativize(_path, repo.workdir()) return Git.Ignore.pathIsIgnored(repo, relativePath) @@ -425,7 +425,7 @@ export default class GitRepositoryAsync { // value can be passed to {::isStatusModified} or {::isStatusNew} to get more // information. getDirectoryStatus (directoryPath) { - return this.repoPromise + return this.getRepo() .then(repo => { const relativePath = this.relativize(directoryPath, repo.workdir()) return this._getStatus([relativePath]) @@ -452,7 +452,7 @@ export default class GitRepositoryAsync { this._refreshingCount++ let relativePath - return this.repoPromise + return this.getRepo() .then(repo => { relativePath = this.relativize(_path, repo.workdir()) return this._getStatus([relativePath]) @@ -564,7 +564,7 @@ export default class GitRepositoryAsync { // * `added` The {Number} of added lines. // * `deleted` The {Number} of deleted lines. getDiffStats (_path) { - return this.repoPromise + return this.getRepo() .then(repo => Promise.all([repo, repo.getHeadCommit()])) .then(([repo, headCommit]) => Promise.all([repo, headCommit.getTree()])) .then(([repo, tree]) => { @@ -600,7 +600,7 @@ export default class GitRepositoryAsync { // * `newLines` The {Number} of lines in the new hunk getLineDiffs (_path, text) { let relativePath = null - return this.repoPromise + return this.getRepo() .then(repo => { relativePath = this.relativize(_path, repo.workdir()) return repo.getHeadCommit() @@ -638,7 +638,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} that resolves or rejects depending on whether the // method was successful. checkoutHead (_path) { - return this.repoPromise + return this.getRepo() .then(repo => { const checkoutOptions = new Git.CheckoutOptions() checkoutOptions.paths = [this.relativize(_path, repo.workdir())] @@ -656,7 +656,7 @@ export default class GitRepositoryAsync { // // Returns a {Promise} that resolves if the method was successful. checkoutReference (reference, create) { - return this.repoPromise + return this.getRepo() .then(repo => repo.checkoutBranch(reference)) .catch(error => { if (create) { @@ -693,7 +693,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to a {NodeGit.Ref} reference to the // created branch. _createBranch (name) { - return this.repoPromise + return this.getRepo() .then(repo => Promise.all([repo, repo.getHeadCommit()])) .then(([repo, commit]) => repo.createBranch(name, commit)) } @@ -751,7 +751,7 @@ export default class GitRepositoryAsync { // // Returns a {Promise} which resolves to the {String} branch name. _refreshBranch () { - return this.repoPromise + return this.getRepo() .then(repo => repo.getCurrentBranch()) .then(ref => ref.name()) .then(branchName => this.branch = branchName) @@ -888,7 +888,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to an {Array} of {NodeGit.StatusFile} // statuses for the paths. _getStatus (paths) { - return this.repoPromise + return this.getRepo() .then(repo => { const opts = { flags: Git.Status.OPT.INCLUDE_UNTRACKED | Git.Status.OPT.RECURSE_UNTRACKED_DIRS | Git.Status.OPT.DISABLE_PATHSPEC_MATCH From a2ab84c3e6bd997ba07ebb86f3fa1176c63c5765 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 4 Jan 2016 13:36:13 -0500 Subject: [PATCH 297/502] Catch errors that are thrown while we're refreshing. --- src/git-repository-async.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 1bc0c1684..95bb407d8 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -810,7 +810,18 @@ export default class GitRepositoryAsync { const branch = this._refreshBranch() const aheadBehind = branch.then(branchName => this._refreshAheadBehindCount(branchName)) - return Promise.all([status, branch, aheadBehind]).then(_ => null) + return Promise.all([status, branch, aheadBehind]) + .then(_ => null) + // Because all these refresh steps happen asynchronously, it's entirely + // possible the repository was destroyed while we were working. In which + // case we should just swallow the error. + .catch(e => { + if (this._isDestroyed()) { + return null + } else { + return Promise.reject(e) + } + }) } // Get the NodeGit repository for the given path. From b234f8481ddc9ff218f48ddc70a086830b6d5771 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 4 Jan 2016 16:12:22 -0500 Subject: [PATCH 298/502] We don't actually use `repo`. --- spec/git-repository-async-spec.js | 3 +-- src/git-repository-async.js | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index e40c0a114..bbaa7ef75 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -31,7 +31,7 @@ describe('GitRepositoryAsync', () => { }) describe('@open(path)', () => { - it('repo is null when no repository is found', async () => { + it('should throw when no repository is found', async () => { repo = GitRepositoryAsync.open(path.join(temp.dir, 'nogit.txt')) let threw = false @@ -42,7 +42,6 @@ describe('GitRepositoryAsync', () => { } expect(threw).toBe(true) - expect(repo.repo).toBe(null) }) }) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 95bb407d8..035af9945 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -30,7 +30,6 @@ export default class GitRepositoryAsync { } constructor (_path, options = {}) { - this.repo = null this.emitter = new Emitter() this.subscriptions = new CompositeDisposable() this.pathStatusCache = {} From a63e30362fe87834aaba0ef53b0f0e7eb083fdb4 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 4 Jan 2016 16:16:50 -0500 Subject: [PATCH 299/502] Documentation for .DestroyedErrorName --- src/git-repository-async.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 035af9945..295fa5ca2 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -25,6 +25,8 @@ export default class GitRepositoryAsync { return Git } + // The name of the error thrown when an action is attempted on a destroyed + // repository. static get DestroyedErrorName () { return 'GitRepositoryAsync.destroyed' } From 6bf9d7eb7d16c5a6e13cbb20ed1001b353489e64 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 4 Jan 2016 16:17:16 -0500 Subject: [PATCH 300/502] Added .openNodeGitRepository --- spec/git-repository-async-spec.js | 13 +++++++++++++ src/git-repository-async.js | 15 ++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index bbaa7ef75..b59bc35ab 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -45,6 +45,19 @@ describe('GitRepositoryAsync', () => { }) }) + describe('.openNodeGitRepository()', () => { + it('returns a new repository instance', async () => { + repo = openFixture('master.git') + + const originalRepo = await repo.getRepo() + expect(originalRepo).not.toBeNull() + + const nodeGitRepo = repo.openNodeGitRepository() + expect(nodeGitRepo).not.toBeNull() + expect(originalRepo).not.toBe(nodeGitRepo) + }) + }) + describe('.getPath()', () => { it('returns the repository path for a repository path', async () => { repo = openFixture('master.git') diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 295fa5ca2..f48988f91 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -35,7 +35,10 @@ export default class GitRepositoryAsync { this.emitter = new Emitter() this.subscriptions = new CompositeDisposable() this.pathStatusCache = {} - this.repoPromise = Git.Repository.openExt(_path, 0, '') + // NB: This needs to happen before the following .openNodeGitRepository + // call. + this.openedPath = _path + this.repoPromise = this.openNodeGitRepository() this.isCaseInsensitive = fs.isCaseInsensitive() this.upstreamByPath = {} @@ -851,6 +854,16 @@ export default class GitRepositoryAsync { }) } + // Open a new instance of the underlying {NodeGit.Repository}. + // + // By opening multiple connections to the same underlying repository, users + // can safely access the same repository concurrently. + // + // Returns the new {NodeGit.Repository}. + openNodeGitRepository () { + return Git.Repository.openExt(this.openedPath, 0, '') + } + // Section: Private // ================ From 4f0218c0a297c42eacb8b527b50cbdb8aa06281f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 Jan 2016 10:35:33 +0100 Subject: [PATCH 301/502] :memo: Fix mistyped spec description --- spec/text-editor-component-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 041eca4cb..99fd1bc54 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -1825,7 +1825,7 @@ describe('TextEditorComponent', function () { expect(component.lineNodeForScreenRow(2).dataset.screenRow).toBe("2") }) - it('does not render highlights for off-screen lines until they come on-screen', async function () { + it('measures block decorations taking into account both top and bottom margins', async function () { wrapperNode.style.height = 9 * lineHeightInPixels + 'px' component.measureDimensions() await nextViewUpdatePromise() From 0d2801812757739afb41ae698c151a6e410c5585 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 Jan 2016 10:39:01 +0100 Subject: [PATCH 302/502] :fire: Remove redundant setup in specs --- spec/text-editor-component-spec.js | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 99fd1bc54..7717ea55d 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -1662,15 +1662,17 @@ describe('TextEditorComponent', function () { return [item, blockDecoration] } + beforeEach(async function () { + wrapperNode.style.height = 9 * lineHeightInPixels + 'px' + component.measureDimensions() + await nextViewUpdatePromise() + }) + afterEach(function () { atom.themes.removeStylesheet('test') }) it("renders visible and yet-to-be-measured block decorations, inserting them between the appropriate lines and refreshing them as needed", async function () { - wrapperNode.style.height = 9 * lineHeightInPixels + 'px' - component.measureDimensions() - await nextViewUpdatePromise() - let [item1, blockDecoration1] = createBlockDecorationForScreenRowWith(0, {className: "decoration-1"}) let [item2, blockDecoration2] = createBlockDecorationForScreenRowWith(2, {className: "decoration-2"}) let [item3, blockDecoration3] = createBlockDecorationForScreenRowWith(4, {className: "decoration-3"}) @@ -1776,10 +1778,6 @@ describe('TextEditorComponent', function () { }) it("correctly sets screen rows on elements, both initially and when decorations move", async function () { - wrapperNode.style.height = 9 * lineHeightInPixels + 'px' - component.measureDimensions() - await nextViewUpdatePromise() - let [item, blockDecoration] = createBlockDecorationForScreenRowWith(0, {className: "decoration-1"}) atom.styles.addStyleSheet( 'atom-text-editor .decoration-1 { width: 30px; height: 80px; }', @@ -1826,10 +1824,6 @@ describe('TextEditorComponent', function () { }) it('measures block decorations taking into account both top and bottom margins', async function () { - wrapperNode.style.height = 9 * lineHeightInPixels + 'px' - component.measureDimensions() - await nextViewUpdatePromise() - let [item, blockDecoration] = createBlockDecorationForScreenRowWith(0, {className: "decoration-1"}) atom.styles.addStyleSheet( 'atom-text-editor .decoration-1 { width: 30px; height: 30px; margin-top: 10px; margin-bottom: 5px; }', From c7a7f0c6dd9c897c1a2f36edb35647e42410f6d5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 Jan 2016 10:56:29 +0100 Subject: [PATCH 303/502] :bug: Ignore setting dimensions for destroyed decorations --- spec/text-editor-presenter-spec.coffee | 9 +++++++++ src/text-editor-presenter.coffee | 2 ++ 2 files changed, 11 insertions(+) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index b604a7022..18e8d313b 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -2375,6 +2375,15 @@ describe "TextEditorPresenter", -> isVisible: true } + it "doesn't throw an error when setting the dimensions for a destroyed decoration", -> + blockDecoration = addBlockDecorationForScreenRow(0) + presenter = buildPresenter(explicitHeight: 30, lineHeight: 10, tileSize: 2, scrollTop: 0) + blockDecoration.destroy() + + presenter.setBlockDecorationDimensions(blockDecoration, 30, 30) + + expect(getState(presenter).content.blockDecorations).toEqual({}) + describe ".overlays", -> [item] = [] stateForOverlay = (presenter, decoration) -> diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 73848bdb3..77de7e8d3 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -1270,6 +1270,8 @@ class TextEditorPresenter @emitDidUpdateState() setBlockDecorationDimensions: (decoration, width, height) -> + return unless @observedBlockDecorations.has(decoration) + @lineTopIndex.resizeBlock(decoration.getId(), height) @invalidatedDimensionsByBlockDecoration.delete(decoration) From 1376afe17eb109d5c0ac2e93d575082671263cde Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 Jan 2016 10:57:32 +0100 Subject: [PATCH 304/502] :art: --- spec/text-editor-presenter-spec.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 18e8d313b..b65253a4d 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -2377,9 +2377,9 @@ describe "TextEditorPresenter", -> it "doesn't throw an error when setting the dimensions for a destroyed decoration", -> blockDecoration = addBlockDecorationForScreenRow(0) - presenter = buildPresenter(explicitHeight: 30, lineHeight: 10, tileSize: 2, scrollTop: 0) - blockDecoration.destroy() + presenter = buildPresenter() + blockDecoration.destroy() presenter.setBlockDecorationDimensions(blockDecoration, 30, 30) expect(getState(presenter).content.blockDecorations).toEqual({}) From 6b4a05495e2c5615561cdcbedd7b28613b17c2a9 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 5 Jan 2016 12:38:10 -0500 Subject: [PATCH 305/502] Commas are important. --- script/bootstrap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/bootstrap b/script/bootstrap index 3f9b5dc46..72c12aedf 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -118,7 +118,7 @@ function bootstrap() { }, { command: apmPath + ' clean' + apmFlags, - message: 'Deleting old packages...' + message: 'Deleting old packages...', options: moduleInstallOptions }, { From 11a15dce37b69243edfeabb8796388ba72c0491a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 Jan 2016 12:33:49 +0100 Subject: [PATCH 306/502] Ignore errors when loading an invalid blob store --- spec/file-system-blob-store-spec.coffee | 26 +++++++++++++++++++++++++ src/file-system-blob-store.js | 19 +++++++++++++----- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/spec/file-system-blob-store-spec.coffee b/spec/file-system-blob-store-spec.coffee index c1cc29449..5147e59ee 100644 --- a/spec/file-system-blob-store-spec.coffee +++ b/spec/file-system-blob-store-spec.coffee @@ -1,4 +1,6 @@ temp = require 'temp' +path = require 'path' +fs = require 'fs-plus' FileSystemBlobStore = require '../src/file-system-blob-store' describe "FileSystemBlobStore", -> @@ -77,3 +79,27 @@ describe "FileSystemBlobStore", -> expect(blobStore.get("b", "invalidation-key-2")).toBeUndefined() expect(blobStore.get("b", "invalidation-key-3")).toBeUndefined() expect(blobStore.get("c", "invalidation-key-4")).toBeUndefined() + + it "ignores errors when loading an invalid blob store", -> + blobStore.set("a", "invalidation-key-1", new Buffer("a")) + blobStore.set("b", "invalidation-key-2", new Buffer("b")) + blobStore.save() + + # Simulate corruption + fs.writeFileSync(path.join(storageDirectory, "MAP"), new Buffer([0])) + fs.writeFileSync(path.join(storageDirectory, "INVKEYS"), new Buffer([0])) + fs.writeFileSync(path.join(storageDirectory, "BLOB"), new Buffer([0])) + + blobStore = FileSystemBlobStore.load(storageDirectory) + + expect(blobStore.get("a", "invalidation-key-1")).toBeUndefined() + expect(blobStore.get("b", "invalidation-key-2")).toBeUndefined() + + blobStore.set("a", "invalidation-key-1", new Buffer("x")) + blobStore.set("b", "invalidation-key-2", new Buffer("y")) + blobStore.save() + + blobStore = FileSystemBlobStore.load(storageDirectory) + + expect(blobStore.get("a", "invalidation-key-1")).toEqual(new Buffer("x")) + expect(blobStore.get("b", "invalidation-key-2")).toEqual(new Buffer("y")) diff --git a/src/file-system-blob-store.js b/src/file-system-blob-store.js index e565a8857..7bbbdcb14 100644 --- a/src/file-system-blob-store.js +++ b/src/file-system-blob-store.js @@ -12,12 +12,16 @@ class FileSystemBlobStore { } constructor (directory) { - this.inMemoryBlobs = new Map() - this.invalidationKeys = {} this.blobFilename = path.join(directory, 'BLOB') this.blobMapFilename = path.join(directory, 'MAP') this.invalidationKeysFilename = path.join(directory, 'INVKEYS') this.lockFilename = path.join(directory, 'LOCK') + this.reset() + } + + reset () { + this.inMemoryBlobs = new Map() + this.invalidationKeys = {} this.storedBlob = new Buffer(0) this.storedBlobMap = {} } @@ -32,9 +36,14 @@ class FileSystemBlobStore { if (!fs.existsSync(this.invalidationKeysFilename)) { return } - this.storedBlob = fs.readFileSync(this.blobFilename) - this.storedBlobMap = JSON.parse(fs.readFileSync(this.blobMapFilename)) - this.invalidationKeys = JSON.parse(fs.readFileSync(this.invalidationKeysFilename)) + + try { + this.storedBlob = fs.readFileSync(this.blobFilename) + this.storedBlobMap = JSON.parse(fs.readFileSync(this.blobMapFilename)) + this.invalidationKeys = JSON.parse(fs.readFileSync(this.invalidationKeysFilename)) + } catch (e) { + this.reset() + } } save () { From 757bbae1e283784debd8d8d2ddf16eba11248ac9 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 5 Jan 2016 15:26:16 -0500 Subject: [PATCH 307/502] s//openNodeGitRepository/openRepository. --- spec/git-repository-async-spec.js | 4 ++-- src/git-repository-async.js | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index b59bc35ab..70260fe49 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -45,14 +45,14 @@ describe('GitRepositoryAsync', () => { }) }) - describe('.openNodeGitRepository()', () => { + describe('.openRepository()', () => { it('returns a new repository instance', async () => { repo = openFixture('master.git') const originalRepo = await repo.getRepo() expect(originalRepo).not.toBeNull() - const nodeGitRepo = repo.openNodeGitRepository() + const nodeGitRepo = repo.openRepository() expect(nodeGitRepo).not.toBeNull() expect(originalRepo).not.toBe(nodeGitRepo) }) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index f48988f91..899675beb 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -35,10 +35,9 @@ export default class GitRepositoryAsync { this.emitter = new Emitter() this.subscriptions = new CompositeDisposable() this.pathStatusCache = {} - // NB: This needs to happen before the following .openNodeGitRepository - // call. + // NB: This needs to happen before the following .openRepository call. this.openedPath = _path - this.repoPromise = this.openNodeGitRepository() + this.repoPromise = this.openRepository() this.isCaseInsensitive = fs.isCaseInsensitive() this.upstreamByPath = {} @@ -860,7 +859,7 @@ export default class GitRepositoryAsync { // can safely access the same repository concurrently. // // Returns the new {NodeGit.Repository}. - openNodeGitRepository () { + openRepository () { return Git.Repository.openExt(this.openedPath, 0, '') } From 5045477b5dbb5c31a0954a8716591eb26b77a3b0 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 5 Jan 2016 15:28:48 -0500 Subject: [PATCH 308/502] path is never used. --- src/git-repository-async.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 899675beb..259e73f18 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -2,7 +2,6 @@ import fs from 'fs-plus' import Git from 'nodegit' -import path from 'path' import {Emitter, CompositeDisposable, Disposable} from 'event-kit' const modifiedStatusFlags = Git.Status.STATUS.WT_MODIFIED | Git.Status.STATUS.INDEX_MODIFIED | Git.Status.STATUS.WT_DELETED | Git.Status.STATUS.INDEX_DELETED | Git.Status.STATUS.WT_TYPECHANGE | Git.Status.STATUS.INDEX_TYPECHANGE From 51bf940fb3dcbb4abbc23c9dcacd5fe724469c8c Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 5 Jan 2016 15:51:34 -0500 Subject: [PATCH 309/502] :arrow_up: nodegit@0.6.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 73ab5026b..72e1f892c 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "key-path-helpers": "^0.4.0", "less-cache": "0.22", "marked": "^0.3.4", - "nodegit": "0.6.0", + "nodegit": "0.6.3", "normalize-package-data": "^2.0.0", "nslog": "^3", "oniguruma": "^5", From 26cd7c63ccbda50b5147393874623448d4d92800 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 5 Jan 2016 17:10:01 -0500 Subject: [PATCH 310/502] s/upstreamByPath/upstream MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We’ll track submodule status separately. --- src/git-repository-async.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 259e73f18..1c77575a6 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -38,7 +38,8 @@ export default class GitRepositoryAsync { this.openedPath = _path this.repoPromise = this.openRepository() this.isCaseInsensitive = fs.isCaseInsensitive() - this.upstreamByPath = {} + this.upstream = {} + this.submodulesByName = {} this._refreshingCount = 0 @@ -292,7 +293,8 @@ export default class GitRepositoryAsync { // * `ahead` The {Number} of commits ahead. // * `behind` The {Number} of commits behind. getCachedUpstreamAheadBehindCount (_path) { - return this.upstreamByPath[_path || '.'] + // TODO: take submodules into account + return this.upstream } // Public: Returns the git configuration value specified by the key. @@ -767,7 +769,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} which will resolve to {null}. _refreshAheadBehindCount (branchName) { return this.getAheadBehindCount(branchName) - .then(counts => this.upstreamByPath['.'] = counts) + .then(counts => this.upstream = counts) } // Refresh the cached status. From eedac0a9513f14aac391605d6be7720cfb4a7ec4 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 5 Jan 2016 17:31:34 -0500 Subject: [PATCH 311/502] We'll store them by path instead of by name. --- src/git-repository-async.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 1c77575a6..b1daca77a 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -39,7 +39,7 @@ export default class GitRepositoryAsync { this.repoPromise = this.openRepository() this.isCaseInsensitive = fs.isCaseInsensitive() this.upstream = {} - this.submodulesByName = {} + this.submodulesByPath = {} this._refreshingCount = 0 From 8603ceb7e808e28e76d8c17a6fc923e1e5cda00c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 Jan 2016 11:08:30 +0100 Subject: [PATCH 312/502] Make spec more comprehensive --- spec/text-editor-presenter-spec.coffee | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index b65253a4d..99c56f1fc 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -2289,7 +2289,7 @@ describe "TextEditorPresenter", -> presenter.setBlockDecorationDimensions(blockDecoration1, 0, 20) expect(stateForBlockDecoration(presenter, blockDecoration1)).toBeUndefined() - it "contains state for block decorations, indicating the screen row they belong to both initially and when their markers move", -> + it "contains state for on-screen and unmeasured block decorations, both initially and when they are updated or destroyed", -> item = {} blockDecoration1 = addBlockDecorationForScreenRow(0, item) blockDecoration2 = addBlockDecorationForScreenRow(4, item) @@ -2375,6 +2375,17 @@ describe "TextEditorPresenter", -> isVisible: true } + blockDecoration1.destroy() + + expect(stateForBlockDecoration(presenter, blockDecoration1)).toBeUndefined() + expect(stateForBlockDecoration(presenter, blockDecoration2)).toBeUndefined() + expect(stateForBlockDecoration(presenter, blockDecoration3)).toBeUndefined() + expectValues stateForBlockDecoration(presenter, blockDecoration4), { + decoration: blockDecoration4 + screenRow: 10 + isVisible: true + } + it "doesn't throw an error when setting the dimensions for a destroyed decoration", -> blockDecoration = addBlockDecorationForScreenRow(0) presenter = buildPresenter() From 4d7af57db317a3ded916cb54fa49f2389d74df66 Mon Sep 17 00:00:00 2001 From: Leonard Lamprecht Date: Thu, 7 Jan 2016 11:51:54 +0100 Subject: [PATCH 313/502] Move to top level, add separator and ellipses --- menus/darwin.cson | 16 ++++++---------- menus/linux.cson | 16 ++++++---------- menus/win32.cson | 16 ++++++---------- 3 files changed, 18 insertions(+), 30 deletions(-) diff --git a/menus/darwin.cson b/menus/darwin.cson index 6737adf46..b2f496685 100644 --- a/menus/darwin.cson +++ b/menus/darwin.cson @@ -11,16 +11,12 @@ { label: 'Downloading Update', enabled: false, visible: false} { type: 'separator' } { label: 'Preferences…', command: 'application:show-settings' } - { - label: 'Customization', - submenu: [ - { label: 'Config', command: 'application:open-your-config' } - { label: 'Init Script', command: 'application:open-your-init-script' } - { label: 'Keymap', command: 'application:open-your-keymap' } - { label: 'Snippets', command: 'application:open-your-snippets' } - { label: 'Stylesheet', command: 'application:open-your-stylesheet' } - ] - } + { type: 'separator' } + { label: 'Config…', command: 'application:open-your-config' } + { label: 'Init Script…', command: 'application:open-your-init-script' } + { label: 'Keymap…', command: 'application:open-your-keymap' } + { label: 'Snippets…', command: 'application:open-your-snippets' } + { label: 'Stylesheet…', command: 'application:open-your-stylesheet' } { type: 'separator' } { label: 'Install Shell Commands', command: 'window:install-shell-commands' } { type: 'separator' } diff --git a/menus/linux.cson b/menus/linux.cson index 416f16831..4e444ea4e 100644 --- a/menus/linux.cson +++ b/menus/linux.cson @@ -83,16 +83,12 @@ } { type: 'separator' } { label: '&Preferences', command: 'application:show-settings' } - { - label: 'Customization', - submenu: [ - { label: 'Open Your Config', command: 'application:open-your-config' } - { label: 'Open Your Init Script', command: 'application:open-your-init-script' } - { label: 'Open Your Keymap', command: 'application:open-your-keymap' } - { label: 'Open Your Snippets', command: 'application:open-your-snippets' } - { label: 'Open Your Stylesheet', command: 'application:open-your-stylesheet' } - ] - } + { type: 'separator' } + { label: 'Config…', command: 'application:open-your-config' } + { label: 'Init Script…', command: 'application:open-your-init-script' } + { label: 'Keymap…', command: 'application:open-your-keymap' } + { label: 'Snippets…', command: 'application:open-your-snippets' } + { label: 'Stylesheet…', command: 'application:open-your-stylesheet' } { type: 'separator' } ] } diff --git a/menus/win32.cson b/menus/win32.cson index 82921d2a3..db8565cc6 100644 --- a/menus/win32.cson +++ b/menus/win32.cson @@ -10,16 +10,12 @@ { label: 'Reopen Last &Item', command: 'pane:reopen-closed-item' } { type: 'separator' } { label: 'Se&ttings', command: 'application:show-settings' } - { - label: 'Customization', - submenu: [ - { label: 'Open Your Config', command: 'application:open-your-config' } - { label: 'Open Your Init Script', command: 'application:open-your-init-script' } - { label: 'Open Your Keymap', command: 'application:open-your-keymap' } - { label: 'Open Your Snippets', command: 'application:open-your-snippets' } - { label: 'Open Your Stylesheet', command: 'application:open-your-stylesheet' } - ] - } + { type: 'separator' } + { label: 'Config…', command: 'application:open-your-config' } + { label: 'Init Script…', command: 'application:open-your-init-script' } + { label: 'Keymap…', command: 'application:open-your-keymap' } + { label: 'Snippets…', command: 'application:open-your-snippets' } + { label: 'Stylesheet…', command: 'application:open-your-stylesheet' } { type: 'separator' } { label: '&Save', command: 'core:save' } { label: 'Save &As…', command: 'core:save-as' } From 75fe311e22930a23fc7837defbc4906abea2428f Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 7 Jan 2016 12:19:51 -0500 Subject: [PATCH 314/502] Refresh the status for submodules too. --- src/git-repository-async.js | 85 +++++++++++++++++++++++++++++++------ 1 file changed, 72 insertions(+), 13 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index b1daca77a..7dfe378c3 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -1,6 +1,7 @@ 'use babel' import fs from 'fs-plus' +import path from 'path' import Git from 'nodegit' import {Emitter, CompositeDisposable, Disposable} from 'event-kit' @@ -772,12 +773,11 @@ export default class GitRepositoryAsync { .then(counts => this.upstream = counts) } - // Refresh the cached status. + // Get the status for this repository. // - // Returns a {Promise} which will resolve to {null}. - _refreshStatus () { - this._refreshingCount++ - + // Returns a {Promise} that will resolve to an object of {String} paths to the + // {Number} status. + _getRepositoryStatus () { let projectPathsPromises = [Promise.resolve('')] if (this.project) { projectPathsPromises = this.project.getPaths() @@ -791,15 +791,71 @@ export default class GitRepositoryAsync { }) .then(statuses => { const statusPairs = statuses.map(status => [status.path(), status.statusBit()]) - return Promise.all(statusPairs) - .then(statusesByPath => _.object(statusesByPath)) + return _.object(statusPairs) }) - .then(newPathStatusCache => { - if (!_.isEqual(this.pathStatusCache, newPathStatusCache) && this.emitter != null) { + } + + // Get the status for the given submodule. + // + // * `submodule` The {NodeGit.Repository} for the submodule. + // + // Returns a {Promise} which resolves to an {Object}, keyed by {String} + // repo-relative {Number} statuses. + _getSubmoduleStatus (submodule) { + return this._getStatus(null, submodule) + .then(statuses => { + return this.getRepo().then(repo => { + const statusPairs = statuses.map(status => { + const absolutePath = path.join(submodule.workdir(), status.path()) + // Get the path relative to the containing repository. + const relativePath = this.relativize(absolutePath, repo.workdir()) + return [relativePath, status.statusBit()] + }) + return _.object(statusPairs) + }) + }) + } + + // Get the status for the submodules in the repository. + // + // Returns a {Promise} that will resolve to an object of {String} paths to the + // {Number} status. + _getSubmoduleStatuses () { + return this.getRepo() + .then(repo => Promise.all([repo, repo.getSubmoduleNames()])) + .then(([repo, submodules]) => { + return Promise.all(submodules.map(s => Git.Submodule.lookup(repo, s))) + }) + .then(submodules => submodules.map(s => s.path())) + .then(relativePaths => { + return this.getRepo().then(repo => { + const workingDirectory = repo.workdir() + return relativePaths.map(p => path.join(workingDirectory, p)) + }) + }) + .then(paths => Promise.all(paths.map(p => Git.Repository.open(p)))) + .then(repos => { + return Promise.all(repos.map(repo => this._getSubmoduleStatus(repo))) + }) + .then(statuses => _.extend({}, ...statuses)) + .then(statuses => { + console.log(statuses) + }) + } + + // Refresh the cached status. + // + // Returns a {Promise} which will resolve to {null}. + _refreshStatus () { + this._refreshingCount++ + + return Promise.all([this._getRepositoryStatus(), this._getSubmoduleStatuses()]) + .then(([repositoryStatus, submoduleStatus]) => { + const statusesByPath = _.extend({}, repositoryStatus, submoduleStatus) + if (!_.isEqual(this.pathStatusCache, statusesByPath) && this.emitter != null) { this.emitter.emit('did-change-statuses') } - this.pathStatusCache = newPathStatusCache - return newPathStatusCache + this.pathStatusCache = statusesByPath }) .then(_ => this._refreshingCount--) } @@ -909,11 +965,14 @@ export default class GitRepositoryAsync { // // * `paths` The {String} paths whose status is wanted. If undefined, get the // status for the whole repository. + // * `repo` The {NodeGit.Repository} the repository to get status from. If + // undefined, it will use this repository. // // Returns a {Promise} which resolves to an {Array} of {NodeGit.StatusFile} // statuses for the paths. - _getStatus (paths) { - return this.getRepo() + _getStatus (paths, repo) { + const repoPromise = (repo ? Promise.resolve(repo) : this.getRepo()) + return repoPromise .then(repo => { const opts = { flags: Git.Status.OPT.INCLUDE_UNTRACKED | Git.Status.OPT.RECURSE_UNTRACKED_DIRS | Git.Status.OPT.DISABLE_PATHSPEC_MATCH From a86d3b6d94070587bb17049a2579bf19742c463e Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 7 Jan 2016 12:44:56 -0500 Subject: [PATCH 315/502] Stop logging. --- src/git-repository-async.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 7dfe378c3..2e418a294 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -838,9 +838,6 @@ export default class GitRepositoryAsync { return Promise.all(repos.map(repo => this._getSubmoduleStatus(repo))) }) .then(statuses => _.extend({}, ...statuses)) - .then(statuses => { - console.log(statuses) - }) } // Refresh the cached status. From 0bef58a8a4b155e0f8f06d4b987f32e2a845ca1c Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 7 Jan 2016 13:05:36 -0500 Subject: [PATCH 316/502] Added a fixture that has submodules. --- .../git/repo-with-submodules/.gitmodules | 6 + spec/fixtures/git/repo-with-submodules/README | 0 .../You-Dont-Need-jQuery/.babelrc | 3 + .../You-Dont-Need-jQuery/.eslintrc | 18 + .../You-Dont-Need-jQuery/.gitignore | 5 + .../You-Dont-Need-jQuery/.travis.yml | 10 + .../You-Dont-Need-jQuery/LICENSE | 22 + .../You-Dont-Need-jQuery/README-es.md | 637 +++++++++ .../You-Dont-Need-jQuery/README-id.md | 634 +++++++++ .../You-Dont-Need-jQuery/README-it.md | 651 +++++++++ .../You-Dont-Need-jQuery/README-my.md | 615 +++++++++ .../You-Dont-Need-jQuery/README-ru.md | 650 +++++++++ .../You-Dont-Need-jQuery/README-tr.md | 665 +++++++++ .../You-Dont-Need-jQuery/README-vi.md | 634 +++++++++ .../You-Dont-Need-jQuery/README.ko-KR.md | 709 ++++++++++ .../You-Dont-Need-jQuery/README.md | 1190 +++++++++++++++++ .../You-Dont-Need-jQuery/README.pt-BR.md | 628 +++++++++ .../You-Dont-Need-jQuery/README.zh-CN.md | 656 +++++++++ .../You-Dont-Need-jQuery/git.git | 1 + .../You-Dont-Need-jQuery/karma.conf.js | 92 ++ .../You-Dont-Need-jQuery/package.json | 53 + .../You-Dont-Need-jQuery/test/README.md | 13 + .../You-Dont-Need-jQuery/test/css.spec.js | 1 + .../You-Dont-Need-jQuery/test/dom.spec.js | 1 + .../You-Dont-Need-jQuery/test/query.spec.js | 71 + .../test/utilities.spec.js | 1 + .../git.git/COMMIT_EDITMSG | 9 + .../git/repo-with-submodules/git.git/HEAD | 1 + .../git/repo-with-submodules/git.git/config | 11 + .../repo-with-submodules/git.git/description | 1 + .../git.git/hooks/applypatch-msg.sample | 15 + .../git.git/hooks/commit-msg.sample | 24 + .../git.git/hooks/post-update.sample | 8 + .../git.git/hooks/pre-applypatch.sample | 14 + .../git.git/hooks/pre-commit.sample | 49 + .../git.git/hooks/pre-push.sample | 53 + .../git.git/hooks/pre-rebase.sample | 169 +++ .../git.git/hooks/prepare-commit-msg.sample | 36 + .../git.git/hooks/update.sample | 128 ++ .../git/repo-with-submodules/git.git/index | Bin 0 -> 377 bytes .../repo-with-submodules/git.git/info/exclude | 6 + .../repo-with-submodules/git.git/logs/HEAD | 2 + .../git.git/logs/refs/heads/master | 2 + .../git.git/modules/You-Dont-Need-jQuery/HEAD | 1 + .../modules/You-Dont-Need-jQuery/ORIG_HEAD | 1 + .../modules/You-Dont-Need-jQuery/config | 14 + .../modules/You-Dont-Need-jQuery/description | 1 + .../modules/You-Dont-Need-jQuery/gitdir | 1 + .../hooks/applypatch-msg.sample | 15 + .../hooks/commit-msg.sample | 24 + .../hooks/post-update.sample | 8 + .../hooks/pre-applypatch.sample | 14 + .../hooks/pre-commit.sample | 49 + .../hooks/pre-push.sample | 53 + .../hooks/pre-rebase.sample | 169 +++ .../hooks/prepare-commit-msg.sample | 36 + .../You-Dont-Need-jQuery/hooks/update.sample | 128 ++ .../modules/You-Dont-Need-jQuery/index | Bin 0 -> 1919 bytes .../modules/You-Dont-Need-jQuery/info/exclude | 6 + .../modules/You-Dont-Need-jQuery/logs/HEAD | 1 + .../logs/refs/heads/master | 1 + .../logs/refs/remotes/origin/HEAD | 1 + ...8b3bc339acd655e8dae9c0dcea8bb2ec174d16.idx | Bin 0 -> 18152 bytes ...b3bc339acd655e8dae9c0dcea8bb2ec174d16.pack | Bin 0 -> 202626 bytes .../modules/You-Dont-Need-jQuery/packed-refs | 2 + .../You-Dont-Need-jQuery/refs/heads/master | 1 + .../refs/remotes/origin/HEAD | 1 + .../git.git/modules/jstips/HEAD | 1 + .../git.git/modules/jstips/ORIG_HEAD | 1 + .../git.git/modules/jstips/config | 14 + .../git.git/modules/jstips/description | 1 + .../git.git/modules/jstips/gitdir | 1 + .../jstips/hooks/applypatch-msg.sample | 15 + .../modules/jstips/hooks/commit-msg.sample | 24 + .../modules/jstips/hooks/post-update.sample | 8 + .../jstips/hooks/pre-applypatch.sample | 14 + .../modules/jstips/hooks/pre-commit.sample | 49 + .../modules/jstips/hooks/pre-push.sample | 53 + .../modules/jstips/hooks/pre-rebase.sample | 169 +++ .../jstips/hooks/prepare-commit-msg.sample | 36 + .../modules/jstips/hooks/update.sample | 128 ++ .../git.git/modules/jstips/index | Bin 0 -> 427 bytes .../git.git/modules/jstips/info/exclude | 6 + .../git.git/modules/jstips/logs/HEAD | 1 + .../modules/jstips/logs/refs/heads/master | 1 + .../jstips/logs/refs/remotes/origin/HEAD | 1 + ...68a55e02b6b7b75582924204669e4f3ed5276f.idx | Bin 0 -> 6728 bytes ...8a55e02b6b7b75582924204669e4f3ed5276f.pack | Bin 0 -> 1081001 bytes .../git.git/modules/jstips/packed-refs | 2 + .../git.git/modules/jstips/refs/heads/master | 1 + .../modules/jstips/refs/remotes/origin/HEAD | 1 + .../3e/2fe2f8226faab789f70d6d8a7231e4f2bd54df | Bin 0 -> 165 bytes .../40/f4e79926a85134d4c905d04e70573b6616f3bc | Bin 0 -> 126 bytes .../50/b89367d8f0acd312ef5aa012eac20a75c91351 | Bin 0 -> 85 bytes .../54/3b9bebdc6bd5c4b22136034a95dd097a57d3dd | Bin 0 -> 51 bytes .../d2/b0ad9cbc6f6c4372e8956e5cc5af771b2342e5 | Bin 0 -> 156 bytes .../d3/e073baf592c56614c68ead9e2cd0a3880140cd | 1 + .../e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 | Bin 0 -> 15 bytes .../git.git/refs/heads/master | 1 + .../jstips/CONTRIBUTING.md | 20 + .../git/repo-with-submodules/jstips/README.md | 323 +++++ .../git/repo-with-submodules/jstips/git.git | 1 + .../jstips/resources/jstips-header-blog.gif | Bin 0 -> 1061017 bytes .../jstips/resources/log.js | 3 + 104 files changed, 9897 insertions(+) create mode 100644 spec/fixtures/git/repo-with-submodules/.gitmodules create mode 100644 spec/fixtures/git/repo-with-submodules/README create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/.babelrc create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/.eslintrc create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/.gitignore create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/.travis.yml create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/LICENSE create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-es.md create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-id.md create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-it.md create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-my.md create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-ru.md create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-tr.md create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-vi.md create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README.ko-KR.md create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README.md create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README.pt-BR.md create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README.zh-CN.md create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/git.git create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/karma.conf.js create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/package.json create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/README.md create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/css.spec.js create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/dom.spec.js create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/query.spec.js create mode 100644 spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/utilities.spec.js create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/COMMIT_EDITMSG create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/HEAD create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/config create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/description create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/hooks/applypatch-msg.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/hooks/commit-msg.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/hooks/post-update.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/hooks/pre-applypatch.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/hooks/pre-commit.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/hooks/pre-push.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/hooks/pre-rebase.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/hooks/prepare-commit-msg.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/hooks/update.sample create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/index create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/info/exclude create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/logs/HEAD create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/logs/refs/heads/master create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/HEAD create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/ORIG_HEAD create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/config create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/description create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/gitdir create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/applypatch-msg.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/commit-msg.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/post-update.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/pre-applypatch.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/pre-commit.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/pre-push.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/pre-rebase.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/prepare-commit-msg.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/update.sample create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/index create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/info/exclude create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/logs/HEAD create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/logs/refs/heads/master create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/logs/refs/remotes/origin/HEAD create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/objects/pack/pack-d38b3bc339acd655e8dae9c0dcea8bb2ec174d16.idx create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/objects/pack/pack-d38b3bc339acd655e8dae9c0dcea8bb2ec174d16.pack create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/packed-refs create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/refs/heads/master create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/refs/remotes/origin/HEAD create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/HEAD create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/ORIG_HEAD create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/config create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/description create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/gitdir create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/hooks/applypatch-msg.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/hooks/commit-msg.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/hooks/post-update.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/hooks/pre-applypatch.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/hooks/pre-commit.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/hooks/pre-push.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/hooks/pre-rebase.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/hooks/prepare-commit-msg.sample create mode 100755 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/hooks/update.sample create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/index create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/info/exclude create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/logs/HEAD create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/logs/refs/heads/master create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/logs/refs/remotes/origin/HEAD create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/objects/pack/pack-e568a55e02b6b7b75582924204669e4f3ed5276f.idx create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/objects/pack/pack-e568a55e02b6b7b75582924204669e4f3ed5276f.pack create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/packed-refs create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/refs/heads/master create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/refs/remotes/origin/HEAD create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/objects/3e/2fe2f8226faab789f70d6d8a7231e4f2bd54df create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/objects/40/f4e79926a85134d4c905d04e70573b6616f3bc create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/objects/50/b89367d8f0acd312ef5aa012eac20a75c91351 create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/objects/54/3b9bebdc6bd5c4b22136034a95dd097a57d3dd create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/objects/d2/b0ad9cbc6f6c4372e8956e5cc5af771b2342e5 create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/objects/d3/e073baf592c56614c68ead9e2cd0a3880140cd create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/refs/heads/master create mode 100644 spec/fixtures/git/repo-with-submodules/jstips/CONTRIBUTING.md create mode 100644 spec/fixtures/git/repo-with-submodules/jstips/README.md create mode 100644 spec/fixtures/git/repo-with-submodules/jstips/git.git create mode 100644 spec/fixtures/git/repo-with-submodules/jstips/resources/jstips-header-blog.gif create mode 100644 spec/fixtures/git/repo-with-submodules/jstips/resources/log.js diff --git a/spec/fixtures/git/repo-with-submodules/.gitmodules b/spec/fixtures/git/repo-with-submodules/.gitmodules new file mode 100644 index 000000000..40f4e7992 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/.gitmodules @@ -0,0 +1,6 @@ +[submodule "jstips"] + path = jstips + url = https://github.com/loverajoel/jstips +[submodule "You-Dont-Need-jQuery"] + path = You-Dont-Need-jQuery + url = https://github.com/oneuijs/You-Dont-Need-jQuery diff --git a/spec/fixtures/git/repo-with-submodules/README b/spec/fixtures/git/repo-with-submodules/README new file mode 100644 index 000000000..e69de29bb diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/.babelrc b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/.babelrc new file mode 100644 index 000000000..e2472af86 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/.babelrc @@ -0,0 +1,3 @@ +{ + presets: ["es2015", "stage-0"] +} diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/.eslintrc b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/.eslintrc new file mode 100644 index 000000000..46faa7c5f --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/.eslintrc @@ -0,0 +1,18 @@ +{ + "extends": "eslint-config-airbnb", + "env": { + "browser": true, + "mocha": true, + "node": true + }, + "rules": { + "valid-jsdoc": 2, + "no-param-reassign": 0, + "comma-dangle": 0, + "one-var": 0, + "no-else-return": 1, + "no-unused-expressions": 0, + "indent": 1, + "eol-last": 0 + } +} diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/.gitignore b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/.gitignore new file mode 100644 index 000000000..539682cbf --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +*.log +node_modules +coverage +logs diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/.travis.yml b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/.travis.yml new file mode 100644 index 000000000..69cf1679a --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/.travis.yml @@ -0,0 +1,10 @@ +language: node_js +node_js: + - "5" + - "4" +before_script: + - export DISPLAY=:99.0 + - sh -e /etc/init.d/xvfb start +script: + - npm run lint + - npm test diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/LICENSE b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/LICENSE new file mode 100644 index 000000000..df3175928 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 oneuijs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-es.md b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-es.md new file mode 100644 index 000000000..652124255 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-es.md @@ -0,0 +1,637 @@ + +> #### You Don't Need jQuery + +Tú no necesitas jQuery +--- +El desarrollo Frontend evoluciona día a día, y los navegadores modernos ya han implementado nativamente APIs para trabajar con DOM/BOM, las cuales son muy buenas, por lo que definitivamente no es necesario aprender jQuery desde cero para manipular el DOM. En la actualidad, gracias al surgimiento de librerías frontend como React, Angular y Vue, manipular el DOM es contrario a los patrones establecidos, y jQuery se ha vuelto menos importante. Este proyecto resume la mayoría de métodos alternativos a jQuery, pero de forma nativa con soporte IE 10+. + +## Tabla de Contenidos + +1. [Query Selector](#query-selector) +1. [CSS & Estilo](#css--estilo) +1. [Manipulación DOM](#manipulación-dom) +1. [Ajax](#ajax) +1. [Eventos](#eventos) +1. [Utilidades](#utilidades) +1. [Traducción](#traducción) +1. [Soporte de Navegadores](#soporte-de-navegadores) + + +## Query Selector + +En lugar de los selectores comunes como clase, id o atributos podemos usar `document.querySelector` o `document.querySelectorAll` como alternativas. Las diferencias radican en: +* `document.querySelector` devuelve el primer elemento que cumpla con la condición +* `document.querySelectorAll` devuelve todos los elementos que cumplen con la condición en forma de NodeList. Puede ser convertido a Array usando `[].slice.call(document.querySelectorAll(selector) || []);` +* Si ningún elemento cumple con la condición, jQuery retornaría `[]` mientras la API DOM retornaría `null`. Nótese el NullPointerException. Se puede usar `||` para establecer el valor por defecto al no encontrar elementos, como en `document.querySelectorAll(selector) || []` + +> Notice: `document.querySelector` and `document.querySelectorAll` are quite **SLOW**, try to use `getElementById`, `document.getElementsByClassName` o `document.getElementsByTagName` if you want to Obtener a performance bonus. + +- [1.0](#1.0) Buscar por selector + + ```js + // jQuery + $('selector'); + + // Nativo + document.querySelectorAll('selector'); + ``` + +- [1.1](#1.1) Buscar por Clase + + ```js + // jQuery + $('.class'); + + // Nativo + document.querySelectorAll('.class'); + + // Forma alternativa + document.getElementsByClassName('class'); + ``` + +- [1.2](#1.2) Buscar por id + + ```js + // jQuery + $('#id'); + + // Nativo + document.querySelector('#id'); + + // Forma alternativa + document.getElementById('id'); + ``` + +- [1.3](#1.3) Buscar por atributo + + ```js + // jQuery + $('a[target=_blank]'); + + // Nativo + document.querySelectorAll('a[target=_blank]'); + ``` + +- [1.4](#1.4) Buscar + + + Buscar nodos + + ```js + // jQuery + $el.find('li'); + + // Nativo + el.querySelectorAll('li'); + ``` + + + Buscar "body" + + ```js + // jQuery + $('body'); + + // Nativo + document.body; + ``` + + + Buscar Atributo + + ```js + // jQuery + $el.attr('foo'); + + // Nativo + e.getAttribute('foo'); + ``` + + + Buscar atributo "data" + + ```js + // jQuery + $el.data('foo'); + + // Nativo + // Usando getAttribute + el.getAttribute('data-foo'); + // También puedes utilizar `dataset` desde IE 11+ + el.dataset['foo']; + ``` + +- [1.5](#1.5) Elementos Hermanos/Previos/Siguientes + + + Elementos hermanos + + ```js + // jQuery + $el.siblings(); + + // Nativo + [].filter.call(el.parentNode.children, function(child) { + return child !== el; + }); + ``` + + + Elementos previos + + ```js + // jQuery + $el.prev(); + + // Nativo + el.previousElementSibling; + ``` + + + Elementos siguientes + + ```js + // jQuery + $el.next(); + + // Nativo + el.nextElementSibling; + ``` + +- [1.6](#1.6) Closest + + Retorna el elemento más cercano que coincida con la condición, partiendo desde el nodo actual hasta document. + + ```js + // jQuery + $el.closest(queryString); + + // Nativo + function closest(el, selector) { + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + while (el) { + if (matchesSelector.call(el, selector)) { + return el; + } else { + el = el.parentElement; + } + } + return null; + } + ``` + +- [1.7](#1.7) Parents Until + + Obtiene los ancestros de cada elemento en el set actual de elementos que cumplan con la condición, sin incluir el actual + + ```js + // jQuery + $el.parentsUntil(selector, filter); + + // Nativo + function parentsUntil(el, selector, filter) { + const result = []; + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + // Partir desde el elemento padre + el = el.parentElement; + while (el && !matchesSelector.call(el, selector)) { + if (!filter) { + result.push(el); + } else { + if (matchesSelector.call(el, filter)) { + result.push(el); + } + } + el = el.parentElement; + } + return result; + } + ``` + +- [1.8](#1.8) Formularios + + + Input/Textarea + + ```js + // jQuery + $('#my-input').val(); + + // Nativo + document.querySelector('#my-input').value; + ``` + + + Obtener el índice de e.currentTarget en `.radio` + + ```js + // jQuery + $(e.currentTarget).index('.radio'); + + // Nativo + [].indexOf.call(document.querySelectAll('.radio'), e.currentTarget); + ``` + +- [1.9](#1.9) Contenidos de Iframe + + `$('iframe').contents()` devuelve `contentDocument` para este iframe específico + + + Contenidos de Iframe + + ```js + // jQuery + $iframe.contents(); + + // Nativo + iframe.contentDocument; + ``` + + + Buscar dentro de un Iframe + + ```js + // jQuery + $iframe.contents().find('.css'); + + // Nativo + iframe.contentDocument.querySelectorAll('.css'); + ``` + +**[⬆ volver al inicio](#tabla-de-contenidos)** + +## CSS & Estilo + +- [2.1](#2.1) CSS + + + Obtener Estilo + + ```js + // jQuery + $el.css("color"); + + // Nativo + // NOTA: Bug conocido, retornará 'auto' si el valor de estilo es 'auto' + const win = el.ownerDocument.defaultView; + // null significa que no tiene pseudo estilos + win.getComputedStyle(el, null).color; + ``` + + + Establecer style + + ```js + // jQuery + $el.css({ color: "#ff0011" }); + + // Nativo + el.style.color = '#ff0011'; + ``` + + + Obtener/Establecer Estilos + + Nótese que si se desea establecer múltiples estilos a la vez, se puede utilizar el método [setStyles](https://github.com/oneuijs/oui-dom-utils/blob/master/src/index.js#L194) en el paquete oui-dom-utils. + + + Agregar clase + + ```js + // jQuery + $el.addClass(className); + + // Nativo + el.classList.add(className); + ``` + + + Quitar Clase + + ```js + // jQuery + $el.removeClass(className); + + // Nativo + el.classList.remove(className); + ``` + + + Consultar si tiene clase + + ```js + // jQuery + $el.hasClass(className); + + // Nativo + el.classList.contains(className); + ``` + + + Toggle class + + ```js + // jQuery + $el.toggleClass(className); + + // Nativo + el.classList.toggle(className); + ``` + +- [2.2](#2.2) Width & Height + + Ancho y Alto son teóricamente idénticos. Usaremos el Alto como ejemplo: + + + Alto de Ventana + + ```js + // alto de ventana + $(window).height(); + // Sin scrollbar, se comporta como jQuery + window.document.documentElement.clientHeight; + // Con scrollbar + window.innerHeight; + ``` + + + Alto de Documento + + ```js + // jQuery + $(document).height(); + + // Nativo + document.documentElement.scrollHeight; + ``` + + + Alto de Elemento + + ```js + // jQuery + $el.height(); + + // Nativo + function getHeight(el) { + const styles = this.getComputedStyles(el); + const height = el.offsetHeight; + const borderTopWidth = parseFloat(styles.borderTopWidth); + const borderBottomWidth = parseFloat(styles.borderBottomWidth); + const paddingTop = parseFloat(styles.paddingTop); + const paddingBottom = parseFloat(styles.paddingBottom); + return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom; + } + // Precisión de integer(when `border-box`, it's `height`; when `content-box`, it's `height + padding + border`) + el.clientHeight; + // Precisión de decimal(when `border-box`, it's `height`; when `content-box`, it's `height + padding + border`) + el.getBoundingClientRect().height; + ``` + +- [2.3](#2.3) Posición & Offset + + + Posición + + ```js + // jQuery + $el.position(); + + // Nativo + { left: el.offsetLeft, top: el.offsetTop } + ``` + + + Offset + + ```js + // jQuery + $el.offset(); + + // Nativo + function getOffset (el) { + const box = el.getBoundingClientRect(); + + return { + top: box.top + window.pageYOffset - document.documentElement.clientTop, + left: box.left + window.pageXOffset - document.documentElement.clientLeft + } + } + ``` + +- [2.4](#2.4) Posición del Scroll Vertical + + ```js + // jQuery + $(window).scrollTop(); + + // Nativo + (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop; + ``` + +**[⬆ volver al inicio](#tabla-de-contenidos)** + +## Manipulación DOM + +- [3.1](#3.1) Remove + ```js + // jQuery + $el.remove(); + + // Nativo + el.parentNode.removeChild(el); + ``` + +- [3.2](#3.2) Text + + + Obtener Texto + + ```js + // jQuery + $el.text(); + + // Nativo + el.textContent; + ``` + + + Establecer Texto + + ```js + // jQuery + $el.text(string); + + // Nativo + el.textContent = string; + ``` + +- [3.3](#3.3) HTML + + + Obtener HTML + + ```js + // jQuery + $el.html(); + + // Nativo + el.innerHTML; + ``` + + + Establecer HTML + + ```js + // jQuery + $el.html(htmlString); + + // Nativo + el.innerHTML = htmlString; + ``` + +- [3.4](#3.4) Append + + Añadir elemento hijo después del último hijo del elemento padre + + ```js + // jQuery + $el.append("
hello
"); + + // Nativo + el.insertAdjacentHTML("beforeend","
hello
"); + ``` + +- [3.5](#3.5) Prepend + + Añadir elemento hijo después del último hijo del elemento padre + + ```js + // jQuery + $el.prepend("
hello
"); + + // Nativo + el.insertAdjacentHTML("afterbegin","
hello
"); + ``` + +- [3.6](#3.6) insertBefore + + Insertar un nuevo nodo antes del primero de los elementos seleccionados + + ```js + // jQuery + $newEl.insertBefore(queryString); + + // Nativo + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target); + ``` + +- [3.7](#3.7) insertAfter + + Insertar un nuevo nodo después de los elementos seleccionados + + ```js + // jQuery + $newEl.insertAfter(queryString); + + // Nativo + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target.nextSibling); + ``` + +**[⬆ volver al inicio](#tabla-de-contenidos)** + +## Ajax + +Reemplazar con [fetch](https://github.com/camsong/fetch-ie8) y [fetch-jsonp](https://github.com/camsong/fetch-jsonp) ++[Fetch API](https://fetch.spec.whatwg.org/) es el nuevo estándar quue reemplaza a XMLHttpRequest para efectuar peticiones AJAX. Funciona en Chrome y Firefox, como también es posible usar un polyfill en otros navegadores. ++ ++Es una buena alternativa utilizar [github/fetch](http://github.com/github/fetch) en IE9+ o [fetch-ie8](https://github.com/camsong/fetch-ie8/) en IE8+, [fetch-jsonp](https://github.com/camsong/fetch-jsonp) para efectuar peticiones JSONP. +**[⬆ volver al inicio](#tabla-de-contenidos)** + +## Eventos + +Para un reemplazo completo con namespace y delegación, utilizar https://github.com/oneuijs/oui-dom-events + +- [5.1](#5.1) Asignar un evento con "on" + + ```js + // jQuery + $el.on(eventName, eventHandler); + + // Nativo + el.addEventListener(eventName, eventHandler); + ``` + +- [5.2](#5.2) Desasignar un evento con "off" + + ```js + // jQuery + $el.off(eventName, eventHandler); + + // Nativo + el.removeEventListener(eventName, eventHandler); + ``` + +- [5.3](#5.3) Trigger + + ```js + // jQuery + $(el).trigger('custom-event', {key1: 'data'}); + + // Nativo + if (window.CustomEvent) { + const event = new CustomEvent('custom-event', {detail: {key1: 'data'}}); + } else { + const event = document.createEvent('CustomEvent'); + event.initCustomEvent('custom-event', true, true, {key1: 'data'}); + } + + el.dispatchEvent(event); + ``` + +**[⬆ volver al inicio](#tabla-de-contenidos)** + +## Utilidades + +- [6.1](#6.1) isArray + + ```js + // jQuery + $.isArray(range); + + // Nativo + Array.isArray(range); + ``` + +- [6.2](#6.2) Trim + + ```js + // jQuery + $.trim(string); + + // Nativo + string.trim(); + ``` + +- [6.3](#6.3) Object Assign + + Utilizar polyfill para object.assign https://github.com/ljharb/object.assign + + ```js + // jQuery + $.extend({}, defaultOpts, opts); + + // Nativo + Object.assign({}, defaultOpts, opts); + ``` + +- [6.4](#6.4) Contains + + ```js + // jQuery + $.contains(el, child); + + // Nativo + el !== child && el.contains(child); + ``` + +**[⬆ volver al inicio](#tabla-de-contenidos)** + +## Traducción + +* [한국어](./README.ko-KR.md) +* [简体中文](./README.zh-CN.md) +* [Bahasa Melayu](./README-my.md) +* [Bahasa Indonesia](./README-id.md) +* [Português(PT-BR)](./README.pt-BR.md) +* [Tiếng Việt Nam](./README-vi.md) +* [Español](./README-es.md) +* [РуÑÑкий](./README-ru.md) +* [Türkçe](./README-tr.md) + +## Soporte de Navegadores + +![Chrome](https://raw.github.com/alrra/browser-logos/master/chrome/chrome_48x48.png) | ![Firefox](https://raw.github.com/alrra/browser-logos/master/firefox/firefox_48x48.png) | ![IE](https://raw.github.com/alrra/browser-logos/master/internet-explorer/internet-explorer_48x48.png) | ![Opera](https://raw.github.com/alrra/browser-logos/master/opera/opera_48x48.png) | ![Safari](https://raw.github.com/alrra/browser-logos/master/safari/safari_48x48.png) +--- | --- | --- | --- | --- | +Última ✔ | Última ✔ | 10+ ✔ | Última ✔ | 6.1+ ✔ | + +# Licencia + +MIT diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-id.md b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-id.md new file mode 100644 index 000000000..13ca75b6c --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-id.md @@ -0,0 +1,634 @@ +## Anda tidak memerlukan jQuery + +Dewasa ini perkembangan environment frontend sangatlah pesat, dimana banyak browser sudah mengimplementasikan DOM/BOM APIs dengan baik. Kita tidak perlu lagi belajar jQuery dari nol untuk keperluan manipulasi DOM atau events. Disaat yang sama; dengan berterimakasih kepada library frontend terkini seperti React, Angular dan Vue; Memanipulasi DOM secara langsung telah menjadi anti-pattern alias sesuatu yang tidak perlu dilakukan. Dengan kata lain, jQuery sekarang menjadi semakin tidak diperlukan. Projek ini memberikan informasi mengenai metode alternatif dari jQuery untuk implementasi Native dengan support untuk browser IE 10+. + + +## Daftar Isi + +1. [Query Selector](#query-selector) +1. [CSS & Style](#css-style) +1. [DOM Manipulation](#dom-manipulation) +1. [Ajax](#ajax) +1. [Events](#events) +1. [Utilities](#utilities) +1. [Translation](#translation) +1. [Browser Support](#browser-yang-di-support) + +## Query Selector + +Untuk selector-selector umum seperti class, id atau attribute, kita dapat menggunakan `document.querySelector` atau `document.querySelectorAll` sebagai pengganti. Perbedaan diantaranya adalah: +* `document.querySelector` mengembalikan elemen pertama yang cocok +* `document.querySelectorAll` mengembalikan semua elemen yang cocok sebagai NodeList. Hasilnya bisa dikonversikan menjadi Array `[].slice.call(document.querySelectorAll(selector) || []);` +* Bila tidak ada hasil pengembalian elemen yang cocok, jQuery akan mengembalikan `[]` sedangkan DOM API akan mengembalikan `null`. Mohon diperhatikan mengenai Null Pointer Exception. Anda juga bisa menggunakan operator `||` untuk set nilai awal jika hasil pencarian tidak ditemukan : `document.querySelectorAll(selector) || []` + +> Perhatian: `document.querySelector` dan `document.querySelectorAll` sedikit **LAMBAT**. Silahkan menggunakan `getElementById`, `document.getElementsByClassName` atau `document.getElementsByTagName` jika anda menginginkan tambahan performa. + +- [1.0](#1.0) Query by selector + + ```js + // jQuery + $('selector'); + + // Native + document.querySelectorAll('selector'); + ``` + +- [1.1](#1.1) Query by class + + ```js + // jQuery + $('.class'); + + // Native + document.querySelectorAll('.class'); + + // or + document.getElementsByClassName('class'); + ``` + +- [1.2](#1.2) Query by id + + ```js + // jQuery + $('#id'); + + // Native + document.querySelector('#id'); + + // or + document.getElementById('id'); + ``` + +- [1.3](#1.3) Query menggunakan attribute + + ```js + // jQuery + $('a[target=_blank]'); + + // Native + document.querySelectorAll('a[target=_blank]'); + ``` + +- [1.4](#1.4) Pencarian. + + + Mencari nodes + + ```js + // jQuery + $el.find('li'); + + // Native + el.querySelectorAll('li'); + ``` + + + Mencari body + + ```js + // jQuery + $('body'); + + // Native + document.body; + ``` + + + Mencari Attribute + + ```js + // jQuery + $el.attr('foo'); + + // Native + e.getAttribute('foo'); + ``` + + + Mencari data attribute + + ```js + // jQuery + $el.data('foo'); + + // Native + // gunakan getAttribute + el.getAttribute('data-foo'); + // anda juga bisa menggunakan `dataset` bila anda perlu support IE 11+ + el.dataset['foo']; + ``` + +- [1.5](#1.5) Elemen-elemen Sibling/Previous/Next + + + Elemen Sibling + + ```js + // jQuery + $el.siblings(); + + // Native + [].filter.call(el.parentNode.children, function(child) { + return child !== el; + }); + ``` + + + Elemen Previous + + ```js + // jQuery + $el.prev(); + + // Native + el.previousElementSibling; + + ``` + + + Elemen Next + + ```js + // next + $el.next(); + el.nextElementSibling; + ``` + +- [1.6](#1.6) Closest + + Mengembalikan elemen pertama yang cocok dari selector yang digunakan, dengan cara mencari mulai dari elemen-sekarang sampai ke document. + + ```js + // jQuery + $el.closest(queryString); + + // Native + function closest(el, selector) { + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + while (el) { + if (matchesSelector.call(el, selector)) { + return el; + } else { + el = el.parentElement; + } + } + return null; + } + ``` + +- [1.7](#1.7) Parents Until + + Digunakan untuk mendapatkan "ancestor" dari setiap elemen yang ditemukan. Namun tidak termasuk elemen-sekarang yang didapat dari pencarian oleh selector, DOM node, atau object jQuery. + + ```js + // jQuery + $el.parentsUntil(selector, filter); + + // Native + function parentsUntil(el, selector, filter) { + const result = []; + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + // match start from parent + el = el.parentElement; + while (el && !matchesSelector.call(el, selector)) { + if (!filter) { + result.push(el); + } else { + if (matchesSelector.call(el, filter)) { + result.push(el); + } + } + el = el.parentElement; + } + return result; + } + ``` + +- [1.8](#1.8) Form + + + Input/Textarea + + ```js + // jQuery + $('#my-input').val(); + + // Native + document.querySelector('#my-input').value; + ``` + + + Get index of e.currentTarget between `.radio` + + ```js + // jQuery + $(e.currentTarget).index('.radio'); + + // Native + [].indexOf.call(document.querySelectAll('.radio'), e.currentTarget); + ``` + +- [1.9](#1.9) Iframe Contents + + `$('iframe').contents()` mengembalikan `contentDocument` + + + Iframe contents + + ```js + // jQuery + $iframe.contents(); + + // Native + iframe.contentDocument; + ``` + + + Iframe Query + + ```js + // jQuery + $iframe.contents().find('.css'); + + // Native + iframe.contentDocument.querySelectorAll('.css'); + ``` + +**[⬆ back to top](#daftar-isi)** + +## CSS Style + +- [2.1](#2.1) CSS + + + Get style + + ```js + // jQuery + $el.css("color"); + + // Native + // PERHATIAN: ada bug disini, dimana fungsi ini akan mengembalikan nilai 'auto' bila nilai dari atribut style adalah 'auto' + const win = el.ownerDocument.defaultView; + // null artinya tidak mengembalikan pseudo styles + win.getComputedStyle(el, null).color; + ``` + + + Set style + + ```js + // jQuery + $el.css({ color: "#ff0011" }); + + // Native + el.style.color = '#ff0011'; + ``` + + + Get/Set Styles + + Mohon dicatat jika anda ingin men-set banyak style bersamaan, anda dapat menemukan referensi di metode [setStyles](https://github.com/oneuijs/oui-dom-utils/blob/master/src/index.js#L194) pada package oui-dom-utils + + + Add class + + ```js + // jQuery + $el.addClass(className); + + // Native + el.classList.add(className); + ``` + + + Remove class + + ```js + // jQuery + $el.removeClass(className); + + // Native + el.classList.remove(className); + ``` + + + has class + + ```js + // jQuery + $el.hasClass(className); + + // Native + el.classList.contains(className); + ``` + + + Toggle class + + ```js + // jQuery + $el.toggleClass(className); + + // Native + el.classList.toggle(className); + ``` + +- [2.2](#2.2) Width & Height + + Secara teori, width dan height identik, contohnya Height: + + + Window height + + ```js + // window height + $(window).height(); + // without scrollbar, behaves like jQuery + window.document.documentElement.clientHeight; + // with scrollbar + window.innerHeight; + ``` + + + Document height + + ```js + // jQuery + $(document).height(); + + // Native + document.documentElement.scrollHeight; + ``` + + + Element height + + ```js + // jQuery + $el.height(); + + // Native + function getHeight(el) { + const styles = this.getComputedStyles(el); + const height = el.offsetHeight; + const borderTopWidth = parseFloat(styles.borderTopWidth); + const borderBottomWidth = parseFloat(styles.borderBottomWidth); + const paddingTop = parseFloat(styles.paddingTop); + const paddingBottom = parseFloat(styles.paddingBottom); + return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom; + } + // accurate to integer(when `border-box`, it's `height`; when `content-box`, it's `height + padding + border`) + el.clientHeight; + // accurate to decimal(when `border-box`, it's `height`; when `content-box`, it's `height + padding + border`) + el.getBoundingClientRect().height; + ``` + +- [2.3](#2.3) Position & Offset + + + Position + + ```js + // jQuery + $el.position(); + + // Native + { left: el.offsetLeft, top: el.offsetTop } + ``` + + + Offset + + ```js + // jQuery + $el.offset(); + + // Native + function getOffset (el) { + const box = el.getBoundingClientRect(); + + return { + top: box.top + window.pageYOffset - document.documentElement.clientTop, + left: box.left + window.pageXOffset - document.documentElement.clientLeft + } + } + ``` + +- [2.4](#2.4) Scroll Top + + ```js + // jQuery + $(window).scrollTop(); + + // Native + (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop; + ``` + +**[⬆ back to top](#daftar-isi)** + +## DOM Manipulation + +- [3.1](#3.1) Remove + ```js + // jQuery + $el.remove(); + + // Native + el.parentNode.removeChild(el); + ``` + +- [3.2](#3.2) Text + + + Get text + + ```js + // jQuery + $el.text(); + + // Native + el.textContent; + ``` + + + Set text + + ```js + // jQuery + $el.text(string); + + // Native + el.textContent = string; + ``` + +- [3.3](#3.3) HTML + + + Get HTML + + ```js + // jQuery + $el.html(); + + // Native + el.innerHTML; + ``` + + + Set HTML + + ```js + // jQuery + $el.html(htmlString); + + // Native + el.innerHTML = htmlString; + ``` + +- [3.4](#3.4) Append + + Menambahkan elemen-anak setelah anak terakhir dari elemen-parent + + ```js + // jQuery + $el.append("
hello
"); + + // Native + let newEl = document.createElement('div'); + newEl.setAttribute('id', 'container'); + newEl.innerHTML = 'hello'; + el.appendChild(newEl); + ``` + +- [3.5](#3.5) Prepend + + ```js + // jQuery + $el.prepend("
hello
"); + + // Native + let newEl = document.createElement('div'); + newEl.setAttribute('id', 'container'); + newEl.innerHTML = 'hello'; + el.insertBefore(newEl, el.firstChild); + ``` + +- [3.6](#3.6) insertBefore + + Memasukkan node baru sebelum elemen yang dipilih. + + ```js + // jQuery + $newEl.insertBefore(queryString); + + // Native + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target); + ``` + +- [3.7](#3.7) insertAfter + + Memasukkan node baru sesudah elemen yang dipilih. + + ```js + // jQuery + $newEl.insertAfter(queryString); + + // Native + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target.nextSibling); + ``` + +**[⬆ back to top](#daftar-isi)** + +## Ajax + +Gantikan dengan [fetch](https://github.com/camsong/fetch-ie8) dan [fetch-jsonp](https://github.com/camsong/fetch-jsonp) + +**[⬆ back to top](#daftar-isi)** + +## Events + +Untuk penggantian secara menyeluruh dengan namespace dan delegation, rujuk ke https://github.com/oneuijs/oui-dom-events + +- [5.1](#5.1) Bind event dengan menggunakan on + + ```js + // jQuery + $el.on(eventName, eventHandler); + + // Native + el.addEventListener(eventName, eventHandler); + ``` + +- [5.2](#5.2) Unbind event dengan menggunakan off + + ```js + // jQuery + $el.off(eventName, eventHandler); + + // Native + el.removeEventListener(eventName, eventHandler); + ``` + +- [5.3](#5.3) Trigger + + ```js + // jQuery + $(el).trigger('custom-event', {key1: 'data'}); + + // Native + if (window.CustomEvent) { + const event = new CustomEvent('custom-event', {detail: {key1: 'data'}}); + } else { + const event = document.createEvent('CustomEvent'); + event.initCustomEvent('custom-event', true, true, {key1: 'data'}); + } + + el.dispatchEvent(event); + ``` + +**[⬆ back to top](#daftar-isi)** + +## Utilities + +- [6.1](#6.1) isArray + + ```js + // jQuery + $.isArray(range); + + // Native + Array.isArray(range); + ``` + +- [6.2](#6.2) Trim + + ```js + // jQuery + $.trim(string); + + // Native + string.trim(); + ``` + +- [6.3](#6.3) Object Assign + + Extend, use object.assign polyfill https://github.com/ljharb/object.assign + + ```js + // jQuery + $.extend({}, defaultOpts, opts); + + // Native + Object.assign({}, defaultOpts, opts); + ``` + +- [6.4](#6.4) Contains + + ```js + // jQuery + $.contains(el, child); + + // Native + el !== child && el.contains(child); + ``` + +**[⬆ back to top](#daftar-isi)** + +## Terjemahan + +* [한국어](./README.ko-KR.md) +* [简体中文](./README.zh-CN.md) +* [Bahasa Melayu](./README-my.md) +* [Bahasa Indonesia](./README-id.md) +* [Português(PT-BR)](./README.pt-BR.md) +* [Tiếng Việt Nam](./README-vi.md) +* [РуÑÑкий](./README-ru.md) +* [Türkçe](./README-tr.md) + +## Browser yang di Support + +![Chrome](https://raw.github.com/alrra/browser-logos/master/chrome/chrome_48x48.png) | ![Firefox](https://raw.github.com/alrra/browser-logos/master/firefox/firefox_48x48.png) | ![IE](https://raw.github.com/alrra/browser-logos/master/internet-explorer/internet-explorer_48x48.png) | ![Opera](https://raw.github.com/alrra/browser-logos/master/opera/opera_48x48.png) | ![Safari](https://raw.github.com/alrra/browser-logos/master/safari/safari_48x48.png) +--- | --- | --- | --- | --- | +Latest ✔ | Latest ✔ | 10+ ✔ | Latest ✔ | 6.1+ ✔ | + +# License + +MIT diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-it.md b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-it.md new file mode 100644 index 000000000..2af89202a --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-it.md @@ -0,0 +1,651 @@ +## Non hai bisogno di jQuery + +Il mondo del Frontend si evolve rapidamente oggigiorno, i browsers moderni hanno gia' implementato un'ampia gamma di DOM/BOM API soddisfacenti. Non dobbiamo imparare jQuery dalle fondamenta per la manipolazione del DOM o di eventi. Nel frattempo, grazie al prevalicare di librerie per il frontend come React, Angular a Vue, manipolare il DOM direttamente diventa un anti-pattern, di consequenza jQuery non e' mai stato meno importante. Questo progetto sommarizza la maggior parte dei metodi e implementazioni alternative a jQuery, con il supporto di IE 10+. + +## Tabella contenuti + +1. [Query Selector](#query-selector) +1. [CSS & Style](#css--style) +1. [Manipolazione DOM](#manipolazione-dom) +1. [Ajax](#ajax) +1. [Eventi](#eventi) +1. [Utilities](#utilities) +1. [Alternative](#alternative) +1. [Traduzioni](#traduzioni) +1. [Supporto Browsers](#supporto-browsers) + +## Query Selector + +Al posto di comuni selettori come class, id o attributi possiamo usare `document.querySelector` o `document.querySelectorAll` per sostituzioni. La differenza risiede in: +* `document.querySelector` restituisce il primo elemento combiaciante +* `document.querySelectorAll` restituisce tutti gli elementi combiacianti della NodeList. Puo' essere convertito in Array usando `[].slice.call(document.querySelectorAll(selector) || []);` +* Se nessun elemento combiacia, jQuery restituitirebbe `[]` li' dove il DOM API ritornera' `null`. Prestate attenzione al Null Pointer Exception. Potete anche usare `||` per settare valori di default se non trovato, come `document.querySelectorAll(selector) || []` + +> Notare: `document.querySelector` e `document.querySelectorAll` sono abbastanza **SLOW**, provate ad usare `getElementById`, `document.getElementsByClassName` o `document.getElementsByTagName` se volete avere un bonus in termini di performance. + +- [1.0](#1.0) Query da selettore + + ```js + // jQuery + $('selector'); + + // Nativo + document.querySelectorAll('selector'); + ``` + +- [1.1](#1.1) Query da classe + + ```js + // jQuery + $('.class'); + + // Nativo + document.querySelectorAll('.class'); + + // or + document.getElementsByClassName('class'); + ``` + +- [1.2](#1.2) Query da id + + ```js + // jQuery + $('#id'); + + // Nativo + document.querySelector('#id'); + + // o + document.getElementById('id'); + ``` + +- [1.3](#1.3) Query da attributo + + ```js + // jQuery + $('a[target=_blank]'); + + // Nativo + document.querySelectorAll('a[target=_blank]'); + ``` + +- [1.4](#1.4) Trovare qualcosa. + + + Trovare nodes + + ```js + // jQuery + $el.find('li'); + + // Nativo + el.querySelectorAll('li'); + ``` + + + Trovare body + + ```js + // jQuery + $('body'); + + // Nativo + document.body; + ``` + + + Trovare Attributi + + ```js + // jQuery + $el.attr('foo'); + + // Nativo + e.getAttribute('foo'); + ``` + + + Trovare attributo data + + ```js + // jQuery + $el.data('foo'); + + // Nativo + // using getAttribute + el.getAttribute('data-foo'); + // potete usare `dataset` solo se supportate IE 11+ + el.dataset['foo']; + ``` + +- [1.5](#1.5) Fratelli/Precedento/Successivo Elemento + + + Elementi fratelli + + ```js + // jQuery + $el.siblings(); + + // Nativo + [].filter.call(el.parentNode.children, function(child) { + return child !== el; + }); + ``` + + + Elementi precedenti + + ```js + // jQuery + $el.prev(); + + // Nativo + el.previousElementSibling; + ``` + + + Elementi successivi + + ```js + // jQuery + $el.next(); + + // Nativo + el.nextElementSibling; + ``` + +- [1.6](#1.6) Il piu' vicino + + Restituisce il primo elementi combiaciante il selettore fornito, attraversando dall'elemento corrente fino al document . + + ```js + // jQuery + $el.closest(queryString); + + // Nativo - Solo ultimo, NO IE + el.closest(selector); + + // Nativo - IE10+ + function closest(el, selector) { + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + while (el) { + if (matchesSelector.call(el, selector)) { + return el; + } else { + el = el.parentElement; + } + } + return null; + } + ``` + +- [1.7](#1.7) Fino a parenti + + Ottiene il parente di ogni elemento nel set corrente di elementi combiacianti, fino a ma non incluso, l'elemento combiaciante il selettorer, DOM node, o jQuery object. + + ```js + // jQuery + $el.parentsUntil(selector, filter); + + // Nativo + function parentsUntil(el, selector, filter) { + const result = []; + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + // il match parte dal parente + el = el.parentElement; + while (el && !matchesSelector.call(el, selector)) { + if (!filter) { + result.push(el); + } else { + if (matchesSelector.call(el, filter)) { + result.push(el); + } + } + el = el.parentElement; + } + return result; + } + ``` + +- [1.8](#1.8) Form + + + Input/Textarea + + ```js + // jQuery + $('#my-input').val(); + + // Native + document.querySelector('#my-input').value; + ``` + + + Get index of e.currentTarget between `.radio` + + ```js + // jQuery + $(e.currentTarget).index('.radio'); + + // Nativo + [].indexOf.call(document.querySelectAll('.radio'), e.currentTarget); + ``` + +- [1.9](#1.9) Iframe Contents + + `$('iframe').contents()` restituisce `contentDocument` per questo specifico iframe + + + Iframe contenuti + + ```js + // jQuery + $iframe.contents(); + + // Nativo + iframe.contentDocument; + ``` + + + Iframe Query + + ```js + // jQuery + $iframe.contents().find('.css'); + + // Nativo + iframe.contentDocument.querySelectorAll('.css'); + ``` + +**[⬆ back to top](#table-of-contents)** + +## CSS & Style + +- [2.1](#2.1) CSS + + + Ottenere style + + ```js + // jQuery + $el.css("color"); + + // Nativo + // NOTA: Bug conosciuto, restituira' 'auto' se il valore di style e' 'auto' + const win = el.ownerDocument.defaultView; + // null significa che non restituira' lo psuedo style + win.getComputedStyle(el, null).color; + ``` + + + Settare style + + ```js + // jQuery + $el.css({ color: "#ff0011" }); + + // Nativo + el.style.color = '#ff0011'; + ``` + + + Ottenere/Settare Styles + + Nota che se volete settare styles multipli in una sola volta, potete riferire [setStyles](https://github.com/oneuijs/oui-dom-utils/blob/master/src/index.js#L194) metodo in oui-dom-utils package. + + + + Aggiungere classe + + ```js + // jQuery + $el.addClass(className); + + // Nativo + el.classList.add(className); + ``` + + + Rimouvere class + + ```js + // jQuery + $el.removeClass(className); + + // Nativo + el.classList.remove(className); + ``` + + + has class + + ```js + // jQuery + $el.hasClass(className); + + // Nativo + el.classList.contains(className); + ``` + + + Toggle class + + ```js + // jQuery + $el.toggleClass(className); + + // Nativo + el.classList.toggle(className); + ``` + +- [2.2](#2.2) Width & Height + + Width e Height sono teoricamente identici, prendendo Height come esempio: + + + Window height + + ```js + // window height + $(window).height(); + // senza scrollbar, si comporta comporta jQuery + window.document.documentElement.clientHeight; + // con scrollbar + window.innerHeight; + ``` + + + Document height + + ```js + // jQuery + $(document).height(); + + // Nativo + document.documentElement.scrollHeight; + ``` + + + Element height + + ```js + // jQuery + $el.height(); + + // Nativo + function getHeight(el) { + const styles = this.getComputedStyles(el); + const height = el.offsetHeight; + const borderTopWidth = parseFloat(styles.borderTopWidth); + const borderBottomWidth = parseFloat(styles.borderBottomWidth); + const paddingTop = parseFloat(styles.paddingTop); + const paddingBottom = parseFloat(styles.paddingBottom); + return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom; + } + // preciso a intero(quando `border-box`, e' `height`; quando `content-box`, e' `height + padding + border`) + el.clientHeight; + // preciso a decimale(quando `border-box`, e' `height`; quando `content-box`, e' `height + padding + border`) + el.getBoundingClientRect().height; + ``` + +- [2.3](#2.3) Position & Offset + + + Position + + ```js + // jQuery + $el.position(); + + // Nativo + { left: el.offsetLeft, top: el.offsetTop } + ``` + + + Offset + + ```js + // jQuery + $el.offset(); + + // Nativo + function getOffset (el) { + const box = el.getBoundingClientRect(); + + return { + top: box.top + window.pageYOffset - document.documentElement.clientTop, + left: box.left + window.pageXOffset - document.documentElement.clientLeft + } + } + ``` + +- [2.4](#2.4) Scroll Top + + ```js + // jQuery + $(window).scrollTop(); + + // Nativo + (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop; + ``` + +**[⬆ back to top](#table-of-contents)** + +## Manipolazione DOM + +- [3.1](#3.1) Remove + ```js + // jQuery + $el.remove(); + + // Nativo + el.parentNode.removeChild(el); + ``` + +- [3.2](#3.2) Text + + + Get text + + ```js + // jQuery + $el.text(); + + // Nativo + el.textContent; + ``` + + + Set text + + ```js + // jQuery + $el.text(string); + + // Nativo + el.textContent = string; + ``` + +- [3.3](#3.3) HTML + + + Ottenere HTML + + ```js + // jQuery + $el.html(); + + // Nativo + el.innerHTML; + ``` + + + Settare HTML + + ```js + // jQuery + $el.html(htmlString); + + // Nativo + el.innerHTML = htmlString; + ``` + +- [3.4](#3.4) Append + + appendere elemento figlio dopo l'ultimo elemento figlio del genitore + + ```js + // jQuery + $el.append("
hello
"); + + // Nativo + el.insertAdjacentHTML("beforeend","
hello
"); + ``` + +- [3.5](#3.5) Prepend + + ```js + // jQuery + $el.prepend("
hello
"); + + // Nativo + el.insertAdjacentHTML("afterbegin","
hello
"); + ``` + +- [3.6](#3.6) insertBefore + + Inserire un nuovo node dopo l'elmento selezionato + + ```js + // jQuery + $newEl.insertBefore(queryString); + + // Nativo + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target); + ``` + +- [3.7](#3.7) insertAfter + + Insert a new node after the selected elements + + ```js + // jQuery + $newEl.insertAfter(queryString); + + // Nativo + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target.nextSibling); + ``` + +- [3.8](#3.8) is + + Restituisce `true` se combacia con l'elemento selezionato + + ```js + // jQuery - Notare `is` funziona anche con `function` o `elements` non di importanza qui + $el.is(selector); + + // Nativo + el.matches(selector); + ``` + +**[⬆ back to top](#table-of-contents)** + +## Ajax + +Sostituire con [fetch](https://github.com/camsong/fetch-ie8) and [fetch-jsonp](https://github.com/camsong/fetch-jsonp) + +**[⬆ back to top](#table-of-contents)** + +## Eventi + +Per una completa sostituzione con namespace e delegation, riferire a https://github.com/oneuijs/oui-dom-events + +- [5.1](#5.1) Bind un evento con on + + ```js + // jQuery + $el.on(eventName, eventHandler); + + // Nativo + el.addEventListener(eventName, eventHandler); + ``` + +- [5.2](#5.2) Unbind an event with off + + ```js + // jQuery + $el.off(eventName, eventHandler); + + // Nativo + el.removeEventListener(eventName, eventHandler); + ``` + +- [5.3](#5.3) Trigger + + ```js + // jQuery + $(el).trigger('custom-event', {key1: 'data'}); + + // Nativo + if (window.CustomEvent) { + const event = new CustomEvent('custom-event', {detail: {key1: 'data'}}); + } else { + const event = document.createEvent('CustomEvent'); + event.initCustomEvent('custom-event', true, true, {key1: 'data'}); + } + + el.dispatchEvent(event); + ``` + +**[⬆ back to top](#table-of-contents)** + +## Utilities + +- [6.1](#6.1) isArray + + ```js + // jQuery + $.isArray(range); + + // Nativo + Array.isArray(range); + ``` + +- [6.2](#6.2) Trim + + ```js + // jQuery + $.trim(string); + + // Nativo + string.trim(); + ``` + +- [6.3](#6.3) Object Assign + + Extend, usa object.assign polyfill https://github.com/ljharb/object.assign + + ```js + // jQuery + $.extend({}, defaultOpts, opts); + + // Nativo + Object.assign({}, defaultOpts, opts); + ``` + +- [6.4](#6.4) Contains + + ```js + // jQuery + $.contains(el, child); + + // Nativo + el !== child && el.contains(child); + ``` + +**[⬆ back to top](#table-of-contents)** + +## Alternative + +* [Forse non hai bisogno di jQuery](http://youmightnotneedjquery.com/) - Esempi di come creare eventi comuni, elementi, ajax etc usando puramente javascript. +* [npm-dom](http://github.com/npm-dom) e [webmodules](http://github.com/webmodules) - Organizzazione dove puoi trovare moduli per il DOM individuale su NPM + +## Traduzioni + +* [한국어](./README.ko-KR.md) +* [简体中文](./README.zh-CN.md) +* [Bahasa Melayu](./README-my.md) +* [Bahasa Indonesia](./README-id.md) +* [Português(PT-BR)](./README.pt-BR.md) +* [Tiếng Việt Nam](./README-vi.md) +* [Español](./README-es.md) +* [Italiano](./README-it.md) +* [Türkçe](./README-tr.md) + +## Supporto Browsers + +![Chrome](https://raw.github.com/alrra/browser-logos/master/chrome/chrome_48x48.png) | ![Firefox](https://raw.github.com/alrra/browser-logos/master/firefox/firefox_48x48.png) | ![IE](https://raw.github.com/alrra/browser-logos/master/internet-explorer/internet-explorer_48x48.png) | ![Opera](https://raw.github.com/alrra/browser-logos/master/opera/opera_48x48.png) | ![Safari](https://raw.github.com/alrra/browser-logos/master/safari/safari_48x48.png) +--- | --- | --- | --- | --- | +Ultimo ✔ | Ultimo ✔ | 10+ ✔ | Ultimo ✔ | 6.1+ ✔ | + +# Licenza + +MIT diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-my.md b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-my.md new file mode 100644 index 000000000..1f0a759c3 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-my.md @@ -0,0 +1,615 @@ +## Anda tidak memerlukan jQuery + +Mutakhir ini perkembangan dalam persekitaran frontend berlaku begitu pesat sekali. Justeru itu kebanyakan pelayar moden telahpun menyediakan API yang memadai untuk pengaksesan DOM/BOM. Kita tak payah lagi belajar jQuery dari asas untuk memanipulasi DOM dan acara-acara. Projek ini menawarkan perlaksanaan alternatif kepada kebanyakan kaedah-kaedah jQuery yang menyokong IE 10+. + +## Isi Kandungan + +1. [Pemilihan elemen](#pemilihan-elemen) +1. [CSS & Penggayaan](#css-penggayaan) +1. [Manipulasi DOM](#manipulasi-dom) +1. [Ajax](#ajax) +1. [Events](#events) +1. [Utiliti](#utiliti) +1. [Terjemahan](#terjemahan) +1. [Browser Support](#browser-support) + +## Pemilihan Elemen + +Pemilihan elemen yang umum seperti class, id atau atribut, biasanya kita boleh pakai `document.querySelector` atau `document.querySelectorAll` sebagai ganti. Bezanya terletak pada +* `document.querySelector` akan mengembalikan elemen pertama sekali yang sepadan dijumpai +* `document.querySelectorAll` akan mengembalikan kesemua elemen yang sepadan dijumpai kedalam sebuah NodeList. Ia boleh ditukar kedalam bentuk array menggunakan `[].slice.call` +* Sekiranya tiada elemen yang sepadan dijumpai, jQuery akan mengembalikan `[]` dimana API DOM pula akan mengembalikan `null`. Sila ambil perhatian pada Null Pointer Exception + +> AWAS: `document.querySelector` dan `document.querySelectorAll` agak **LEMBAB** berbanding `getElementById`, `document.getElementsByClassName` atau `document.getElementsByTagName` jika anda menginginkan bonus dari segi prestasi. + +- [1.1](#1.1) Pemilihan menggunakan class + + ```js + // jQuery + $('.css'); + + // Native + document.querySelectorAll('.css'); + ``` + +- [1.2](#1.2) Pemilihan menggunakan id + + ```js + // jQuery + $('#id'); + + // Native + document.querySelector('#id'); + ``` + +- [1.3](#1.3) Pemilihan menggunakan atribut + + ```js + // jQuery + $('a[target=_blank]'); + + // Native + document.querySelectorAll('a[target=_blank]'); + ``` + +- [1.4](#1.4) Cari sth. + + + Find nodes + + ```js + // jQuery + $el.find('li'); + + // Native + el.querySelectorAll('li'); + ``` + + + Cari body + + ```js + // jQuery + $('body'); + + // Native + document.body; + ``` + + + Cari Attribute + + ```js + // jQuery + $el.attr('foo'); + + // Native + e.getAttribute('foo'); + ``` + + + Cari atribut data + + ```js + // jQuery + $el.data('foo'); + + // Native + // menggunakan getAttribute + el.getAttribute('data-foo'); + // anda boleh juga gunakan `dataset` jika ingin pakai IE 11+ + el.dataset['foo']; + ``` + +- [1.5](#1.5) Sibling/Previous/Next Elements + + + Sibling elements + + ```js + // jQuery + $el.siblings(); + + // Native + [].filter.call(el.parentNode.children, function(child) { + return child !== el; + }); + ``` + + + Previous elements + + ```js + // jQuery + $el.prev(); + + // Native + el.previousElementSibling; + + ``` + + + Next elements + + ```js + // next + $el.next(); + el.nextElementSibling; + ``` + +- [1.6](#1.6) Closest + + Return the first matched element by provided selector, traversing from current element to document. + + ```js + // jQuery + $el.closest(queryString); + + // Native + function closest(el, selector) { + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + while (el) { + if (matchesSelector.call(el, selector)) { + return el; + } else { + el = el.parentElement; + } + } + return null; + } + ``` + +- [1.7](#1.7) Parents Until + + Get the ancestors of each element in the current set of matched elements, up to but not including the element matched by the selector, DOM node, or jQuery object. + + ```js + // jQuery + $el.parentsUntil(selector, filter); + + // Native + function parentsUntil(el, selector, filter) { + const result = []; + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + // match start from parent + el = el.parentElement; + while (el && !matchesSelector.call(el, selector)) { + if (!filter) { + result.push(el); + } else { + if (matchesSelector.call(el, filter)) { + result.push(el); + } + } + el = el.parentElement; + } + return result; + } + ``` + +- [1.8](#1.8) Form + + + Input/Textarea + + ```js + // jQuery + $('#my-input').val(); + + // Native + document.querySelector('#my-input').value; + ``` + + + Get index of e.currentTarget between `.radio` + + ```js + // jQuery + $(e.currentTarget).index('.radio'); + + // Native + [].indexOf.call(document.querySelectAll('.radio'), e.currentTarget); + ``` + +- [1.9](#1.9) Iframe Contents + + `$('iframe').contents()` returns `contentDocument` for this specific iframe + + + Iframe contents + + ```js + // jQuery + $iframe.contents(); + + // Native + iframe.contentDocument; + ``` + + + Iframe Query + + ```js + // jQuery + $iframe.contents().find('.css'); + + // Native + iframe.contentDocument.querySelectorAll('.css'); + ``` + +**[⬆ back to top](#table-of-contents)** + +## CSS & Style + +- [2.1](#2.1) CSS + + + Get style + + ```js + // jQuery + $el.css("color"); + + // Native + // NOTE: Known bug, will return 'auto' if style value is 'auto' + const win = el.ownerDocument.defaultView; + // null means not return presudo styles + win.getComputedStyle(el, null).color; + ``` + + + Set style + + ```js + // jQuery + $el.css({ color: "#ff0011" }); + + // Native + el.style.color = '#ff0011'; + ``` + + + Get/Set Styles + + Note that if you want to set multiple styles once, you could refer to [setStyles](https://github.com/oneuijs/oui-dom-utils/blob/master/src/index.js#L194) method in oui-dom-utils package. + + + + Add class + + ```js + // jQuery + $el.addClass(className); + + // Native + el.classList.add(className); + ``` + + + Remove class + + ```js + // jQuery + $el.removeClass(className); + + // Native + el.classList.remove(className); + ``` + + + has class + + ```js + // jQuery + $el.hasClass(className); + + // Native + el.classList.contains(className); + ``` + + + Toggle class + + ```js + // jQuery + $el.toggleClass(className); + + // Native + el.classList.toggle(className); + ``` + +- [2.2](#2.2) Width & Height + + Width and Height are theoretically identical, take Height as example: + + + Window height + + ```js + // window height + $(window).height(); + // without scrollbar, behaves like jQuery + window.document.documentElement.clientHeight; + // with scrollbar + window.innerHeight; + ``` + + + Document height + + ```js + // jQuery + $(document).height(); + + // Native + document.documentElement.scrollHeight; + ``` + + + Element height + + ```js + // jQuery + $el.height(); + + // Native + function getHeight(el) { + const styles = this.getComputedStyles(el); + const height = el.offsetHeight; + const borderTopWidth = parseFloat(styles.borderTopWidth); + const borderBottomWidth = parseFloat(styles.borderBottomWidth); + const paddingTop = parseFloat(styles.paddingTop); + const paddingBottom = parseFloat(styles.paddingBottom); + return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom; + } + // accurate to integer(when `border-box`, it's `height`; when `content-box`, it's `height + padding + border`) + el.clientHeight; + // accurate to decimal(when `border-box`, it's `height`; when `content-box`, it's `height + padding + border`) + el.getBoundingClientRect().height; + ``` + +- [2.3](#2.3) Position & Offset + + + Position + + ```js + // jQuery + $el.position(); + + // Native + { left: el.offsetLeft, top: el.offsetTop } + ``` + + + Offset + + ```js + // jQuery + $el.offset(); + + // Native + function getOffset (el) { + const box = el.getBoundingClientRect(); + + return { + top: box.top + window.pageYOffset - document.documentElement.clientTop, + left: box.left + window.pageXOffset - document.documentElement.clientLeft + } + } + ``` + +- [2.4](#2.4) Scroll Top + + ```js + // jQuery + $(window).scrollTop(); + + // Native + (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop; + ``` + +**[⬆ back to top](#table-of-contents)** + +## DOM Manipulation + +- [3.1](#3.1) Remove + ```js + // jQuery + $el.remove(); + + // Native + el.parentNode.removeChild(el); + ``` + +- [3.2](#3.2) Text + + + Get text + + ```js + // jQuery + $el.text(); + + // Native + el.textContent; + ``` + + + Set text + + ```js + // jQuery + $el.text(string); + + // Native + el.textContent = string; + ``` + +- [3.3](#3.3) HTML + + + Get HTML + + ```js + // jQuery + $el.html(); + + // Native + el.innerHTML; + ``` + + + Set HTML + + ```js + // jQuery + $el.html(htmlString); + + // Native + el.innerHTML = htmlString; + ``` + +- [3.4](#3.4) Append + + append child element after the last child of parent element + + ```js + // jQuery + $el.append("
hello
"); + + // Native + let newEl = document.createElement('div'); + newEl.setAttribute('id', 'container'); + newEl.innerHTML = 'hello'; + el.appendChild(newEl); + ``` + +- [3.5](#3.5) Prepend + + ```js + // jQuery + $el.prepend("
hello
"); + + // Native + let newEl = document.createElement('div'); + newEl.setAttribute('id', 'container'); + newEl.innerHTML = 'hello'; + el.insertBefore(newEl, el.firstChild); + ``` + +- [3.6](#3.6) insertBefore + + Insert a new node before the selected elements + + ```js + // jQuery + $newEl.insertBefore(queryString); + + // Native + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target); + ``` + +- [3.7](#3.7) insertAfter + + Insert a new node after the selected elements + + ```js + // jQuery + $newEl.insertAfter(queryString); + + // Native + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target.nextSibling); + ``` + +**[⬆ back to top](#table-of-contents)** + +## Ajax + +Replace with [fetch](https://github.com/camsong/fetch-ie8) and [fetch-jsonp](https://github.com/camsong/fetch-jsonp) + +**[⬆ back to top](#table-of-contents)** + +## Events + +For a complete replacement with namespace and delegation, refer to https://github.com/oneuijs/oui-dom-events + +- [5.1](#5.1) Bind an event with on + + ```js + // jQuery + $el.on(eventName, eventHandler); + + // Native + el.addEventListener(eventName, eventHandler); + ``` + +- [5.2](#5.2) Unbind an event with off + + ```js + // jQuery + $el.off(eventName, eventHandler); + + // Native + el.removeEventListener(eventName, eventHandler); + ``` + +- [5.3](#5.3) Trigger + + ```js + // jQuery + $(el).trigger('custom-event', {key1: 'data'}); + + // Native + if (window.CustomEvent) { + const event = new CustomEvent('custom-event', {detail: {key1: 'data'}}); + } else { + const event = document.createEvent('CustomEvent'); + event.initCustomEvent('custom-event', true, true, {key1: 'data'}); + } + + el.dispatchEvent(event); + ``` + +**[⬆ back to top](#table-of-contents)** + +## Utility + +- [6.1](#6.1) isArray + + ```js + // jQuery + $.isArray(range); + + // Native + Array.isArray(range); + ``` + +- [6.2](#6.2) Trim + + ```js + // jQuery + $.trim(string); + + // Native + String.trim(string); + ``` + +- [6.3](#6.3) Object Assign + + Extend, use object.assign polyfill https://github.com/ljharb/object.assign + + ```js + // jQuery + $.extend({}, defaultOpts, opts); + + // Native + Object.assign({}, defaultOpts, opts); + ``` + +- [6.4](#6.4) Contains + + ```js + // jQuery + $.contains(el, child); + + // Native + el !== child && el.contains(child); + ``` + +**[⬆ back to top](#table-of-contents)** + +## Terjemahan + +* [한국어](./README.ko-KR.md) +* [简体中文](./README.zh-CN.md) +* [English](./README.md) +* [РуÑÑкий](./README-ru.md) +* [Türkçe](./README-tr.md) + +## Sokongan Pelayar + +![Chrome](https://raw.github.com/alrra/browser-logos/master/chrome/chrome_48x48.png) | ![Firefox](https://raw.github.com/alrra/browser-logos/master/firefox/firefox_48x48.png) | ![IE](https://raw.github.com/alrra/browser-logos/master/internet-explorer/internet-explorer_48x48.png) | ![Opera](https://raw.github.com/alrra/browser-logos/master/opera/opera_48x48.png) | ![Safari](https://raw.github.com/alrra/browser-logos/master/safari/safari_48x48.png) +--- | --- | --- | --- | --- | +Latest ✔ | Latest ✔ | 10+ ✔ | Latest ✔ | 6.1+ ✔ | + +# Lesen + +MIT diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-ru.md b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-ru.md new file mode 100644 index 000000000..561bdb96f --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-ru.md @@ -0,0 +1,650 @@ +## Вам не нужен jQuery + +Ð’ наше Ð²Ñ€ÐµÐ¼Ñ Ñреда фронт Ñнд разработки быÑтро развиваетÑÑ, Ñовременные браузеры уже реализовали значимую чаÑть DOM/BOM APIs и Ñто хорошо. Вам не нужно изучать jQuery Ñ Ð½ÑƒÐ»Ñ Ð´Ð»Ñ Ð¼Ð°Ð½Ð¸Ð¿ÑƒÐ»Ñцией DOM'ом или обектами Ñобытий. Ð’ то же времÑ, Ð±Ð»Ð°Ð³Ð¾Ð´Ð°Ñ€Ñ Ð»Ð¸Ð´Ð¸Ñ€ÑƒÑŽÑ‰Ð¸Ð¼ фронт Ñнд библиотекам, таким как React, Angular и Vue, манипулÑÑ†Ð¸Ñ DOM'ом напрÑмую ÑтановитÑÑ Ð¿Ñ€Ð¾Ñ‚Ð¸Ð²Ð¾ шаблонной, jQuery никогда не был менее важен. Этот проект Ñуммирует большинÑтво альтернатив методов jQuery в нативном иÑполнении Ñ Ð¿Ð¾Ð´Ð´ÐµÑ€Ð¶ÐºÐ¾Ð¹ IE 10+. + +## Содержание + +1. [Query Selector](#query-selector) +1. [CSS & Style](#css--style) +1. [МанипулÑÑ†Ð¸Ñ DOM](#МанипулÑции-dom) +1. [Ajax](#ajax) +1. [СобытиÑ](#СобытиÑ) +1. [Утилиты](#Утилиты) +1. [Ðльтернативы](#Ðльтернативы) +1. [Переводы](#Переводы) +1. [Поддержка браузеров](#Поддержка-браузеров) + +## Query Selector +Ð”Ð»Ñ Ñ‡Ð°Ñто иÑпользуемых Ñелекторов, таких как class, id или attribute мы можем иÑпользовать `document.querySelector` или `document.querySelectorAll` Ð´Ð»Ñ Ð·Ð°Ð¼ÐµÐ½Ñ‹. Разница такова: +* `document.querySelector` возвращает первый Ñовпавший Ñлемент +* `document.querySelectorAll` возвращает вÑе ÑовÑпавшие Ñлементы как коллекцию узлов(NodeList). Его можно конвертировать в маÑÑив иÑÐ¿Ð¾Ð»ÑŒÐ·ÑƒÑ `[].slice.call(document.querySelectorAll(selector) || []);` +* ЕÑли никакие Ñлементы не Ñовпадут, jQuery вернет `[]` где DOM API вернет `null`. Обратите внимание на указатель иÑÐºÐ»ÑŽÑ‡ÐµÐ½Ð¸Ñ Null (Null Pointer Exception). Ð’Ñ‹ так же можете иÑпользовать `||` Ð´Ð»Ñ ÑƒÑтановки Ð·Ð½Ð°Ñ‡ÐµÐ½Ð¸Ñ Ð¿Ð¾ умолчанию еÑли не было найдемо Ñовпадений `document.querySelectorAll(selector) || []` + +> Заметка: `document.querySelector` и `document.querySelectorAll` доÑтаточно **МЕДЛЕÐÐЫ**, ÑтарайтеÑÑŒ иÑпользовать `getElementById`, `document.getElementsByClassName` или `document.getElementsByTagName` еÑли хотите улучшить производительноÑть. + +- [1.0](#1.0) Query by selector + + ```js + // jQuery + $('selector'); + + // Ðативно + document.querySelectorAll('selector'); + ``` + +- [1.1](#1.1) Ð—Ð°Ð¿Ñ€Ð¾Ñ Ð¿Ð¾ клаÑÑу + + ```js + // jQuery + $('.class'); + + // Ðативно + document.querySelectorAll('.class'); + + // или + document.getElementsByClassName('class'); + ``` + +- [1.2](#1.2) Ð—Ð°Ð¿Ñ€Ð¾Ñ Ð¿Ð¾ ID + + ```js + // jQuery + $('#id'); + + // Ðативно + document.querySelector('#id'); + + // или + document.getElementById('id'); + ``` + +- [1.3](#1.3) Ð—Ð°Ð¿Ñ€Ð¾Ñ Ð¿Ð¾ атрибуту + + ```js + // jQuery + $('a[target=_blank]'); + + // Ðативно + document.querySelectorAll('a[target=_blank]'); + ``` + +- [1.4](#1.4) Ðайти Ñреди потомков + + + Ðайти nodes + + ```js + // jQuery + $el.find('li'); + + // Ðативно + el.querySelectorAll('li'); + ``` + + + Ðайти body + + ```js + // jQuery + $('body'); + + // Ðативно + document.body; + ``` + + + Ðайти атрибуты + + ```js + // jQuery + $el.attr('foo'); + + // Ðативно + e.getAttribute('foo'); + ``` + + + Ðайти data attribute + + ```js + // jQuery + $el.data('foo'); + + // Ðативно + // иÑÐ¿Ð¾Ð»ÑŒÐ·ÑƒÑ getAttribute + el.getAttribute('data-foo'); + // также можно иÑпользовать `dataset`, еÑли не требуетÑÑ Ð¿Ð¾Ð´Ð´ÐµÑ€Ð¶ÐºÐ° ниже IE 11. + el.dataset['foo']; + ``` + +- [1.5](#1.5) РодÑтвенные/Предыдущие/Следующие Элементы + + + РодÑтвенные Ñлементы + + ```js + // jQuery + $el.siblings(); + + // Ðативно + [].filter.call(el.parentNode.children, function(child) { + return child !== el; + }); + ``` + + + Предыдущие Ñлементы + + ```js + // jQuery + $el.prev(); + + // Ðативно + el.previousElementSibling; + ``` + + + Следующие Ñлементы + + ```js + // jQuery + $el.next(); + + // Ðативно + el.nextElementSibling; + ``` + +- [1.6](#1.6) Closest + + Возвращает первый Ñовпавший Ñлемент по предоÑтавленному Ñелектору, Ð¾Ð±Ð¾Ñ…Ð¾Ð´Ñ Ð¾Ñ‚ текущего Ñлементы до документа. + + ```js + // jQuery + $el.closest(queryString); + + // Ðативно - Only latest, NO IE + el.closest(selector); + + // Ðативно - IE10+ + function closest(el, selector) { + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + while (el) { + if (matchesSelector.call(el, selector)) { + return el; + } else { + el = el.parentElement; + } + } + return null; + } + ``` + +- [1.7](#1.7) Родители до + + Получить родителей кажого Ñлемента в текущем Ñете Ñовпавших Ñлементов, но не Ð²ÐºÐ»ÑŽÑ‡Ð°Ñ Ñлемент Ñовпавший Ñ Ñелектором, узел DOM'а, или объект jQuery. + + ```js + // jQuery + $el.parentsUntil(selector, filter); + + // Ðативно + function parentsUntil(el, selector, filter) { + const result = []; + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + // Совпадать Ð½Ð°Ñ‡Ð¸Ð½Ð°Ñ Ð¾Ñ‚ Ñ€Ð¾Ð´Ð¸Ñ‚ÐµÐ»Ñ + el = el.parentElement; + while (el && !matchesSelector.call(el, selector)) { + if (!filter) { + result.push(el); + } else { + if (matchesSelector.call(el, filter)) { + result.push(el); + } + } + el = el.parentElement; + } + return result; + } + ``` + +- [1.8](#1.8) От + + + Input/Textarea + + ```js + // jQuery + $('#my-input').val(); + + // Ðативно + document.querySelector('#my-input').value; + ``` + + + получить Ð¸Ð½Ð´ÐµÐºÑ e.currentTarget между `.radio` + + ```js + // jQuery + $(e.currentTarget).index('.radio'); + + // Ðативно + [].indexOf.call(document.querySelectAll('.radio'), e.currentTarget); + ``` + +- [1.9](#1.9) Контент Iframe + + `$('iframe').contents()` возвращает `contentDocument` Ð´Ð»Ñ Ð¸Ð¼ÐµÐ½Ð½Ð¾ Ñтого iframe + + + Контент Iframe + + ```js + // jQuery + $iframe.contents(); + + // Ðативно + iframe.contentDocument; + ``` + + + Iframe Query + + ```js + // jQuery + $iframe.contents().find('.css'); + + // Ðативно + iframe.contentDocument.querySelectorAll('.css'); + ``` + +**[⬆ Ðаверх](#Содержание)** + +## CSS & Style + +- [2.1](#2.1) CSS + + + Получить Ñтиль + + ```js + // jQuery + $el.css("color"); + + // Ðативно + // ЗÐМЕТКÐ: ИзвеÑÑ‚Ð½Ð°Ñ Ð¾ÑˆÐ¸ÐºÐ°, возвращает 'auto' еÑли значение ÑÑ‚Ð¸Ð»Ñ 'auto' + const win = el.ownerDocument.defaultView; + // null означает не возвращать пÑевдоÑтили + win.getComputedStyle(el, null).color; + ``` + + + ПриÑвоение style + + ```js + // jQuery + $el.css({ color: "#ff0011" }); + + // Ðативно + el.style.color = '#ff0011'; + ``` + + + Получение/ПриÑвоение Ñтилей + + Заметьте что еÑли вы хотите приÑвоить неÑколько Ñтилей за раз, вы можете ÑоÑлатьÑÑ Ð½Ð° [setStyles](https://github.com/oneuijs/oui-dom-utils/blob/master/src/index.js#L194) метод в oui-dom-utils package. + + + + Добавить клаÑÑ + + ```js + // jQuery + $el.addClass(className); + + // Ðативно + el.classList.add(className); + ``` + + + Удалить class + + ```js + // jQuery + $el.removeClass(className); + + // Ðативно + el.classList.remove(className); + ``` + + + Имеет клаÑÑ + + ```js + // jQuery + $el.hasClass(className); + + // Ðативно + el.classList.contains(className); + ``` + + + Переключать клаÑÑ + + ```js + // jQuery + $el.toggleClass(className); + + // Ðативно + el.classList.toggle(className); + ``` + +- [2.2](#2.2) Ширина и Ð’Ñ‹Ñота + + Ширина и выÑота теоритечеÑки идентичны, например возьмем выÑоту: + + + выÑота окна + + ```js + // Ð’Ñ‹Ñота окна + $(window).height(); + // без Ñкроллбара, ведет ÑÐµÐ±Ñ ÐºÐ°Ðº jQuery + window.document.documentElement.clientHeight; + // вмеÑте Ñ Ñкроллбаром + window.innerHeight; + ``` + + + выÑота документа + + ```js + // jQuery + $(document).height(); + + // Ðативно + document.documentElement.scrollHeight; + ``` + + + Ð’Ñ‹Ñота Ñлемента + + ```js + // jQuery + $el.height(); + + // Ðативно + function getHeight(el) { + const styles = this.getComputedStyles(el); + const height = el.offsetHeight; + const borderTopWidth = parseFloat(styles.borderTopWidth); + const borderBottomWidth = parseFloat(styles.borderBottomWidth); + const paddingTop = parseFloat(styles.paddingTop); + const paddingBottom = parseFloat(styles.paddingBottom); + return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom; + } + // С точноÑтью до целого чиÑла(когда `border-box`, Ñто `height`; когда `content-box`, Ñто `height + padding + border`) + el.clientHeight; + // Ñ Ñ‚Ð¾Ñ‡Ð½Ð¾Ñтью до деÑÑтых(когда `border-box`, Ñто `height`; когда `content-box`, Ñто `height + padding + border`) + el.getBoundingClientRect().height; + ``` + +- [2.3](#2.3) ÐŸÐ¾Ð·Ð¸Ñ†Ð¸Ñ Ð¸ Ñмещение + + + ÐŸÐ¾Ð·Ð¸Ñ†Ð¸Ñ + + ```js + // jQuery + $el.position(); + + // Ðативно + { left: el.offsetLeft, top: el.offsetTop } + ``` + + + Смещение + + ```js + // jQuery + $el.offset(); + + // Ðативно + function getOffset (el) { + const box = el.getBoundingClientRect(); + + return { + top: box.top + window.pageYOffset - document.documentElement.clientTop, + left: box.left + window.pageXOffset - document.documentElement.clientLeft + } + } + ``` + +- [2.4](#2.4) Прокрутка вверх + + ```js + // jQuery + $(window).scrollTop(); + + // Ðативно + (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop; + ``` + +**[⬆ Ðаверх](#Содержание)** + +## МанипулÑции DOM + +- [3.1](#3.1) Remove + ```js + // jQuery + $el.remove(); + + // Ðативно + el.parentNode.removeChild(el); + ``` + +- [3.2](#3.2) ТекÑÑ‚ + + + Получить текÑÑ‚ + + ```js + // jQuery + $el.text(); + + // Ðативно + el.textContent; + ``` + + + ПриÑвоить текÑÑ‚ + + ```js + // jQuery + $el.text(string); + + // Ðативно + el.textContent = string; + ``` + +- [3.3](#3.3) HTML + + + Получить HTML + + ```js + // jQuery + $el.html(); + + // Ðативно + el.innerHTML; + ``` + + + ПриÑвоить HTML + + ```js + // jQuery + $el.html(htmlString); + + // Ðативно + el.innerHTML = htmlString; + ``` + +- [3.4](#3.4) Append + + Добавление Ñлемента ребенка поÑле поÑледнего ребенка Ñлемента Ñ€Ð¾Ð´Ð¸Ñ‚ÐµÐ»Ñ + + ```js + // jQuery + $el.append("
hello
"); + + // Ðативно + el.insertAdjacentHTML("beforeend","
hello
"); + ``` + +- [3.5](#3.5) Prepend + + ```js + // jQuery + $el.prepend("
hello
"); + + // Ðативно + el.insertAdjacentHTML("afterbegin","
hello
"); + ``` + +- [3.6](#3.6) insertBefore + + Ð’Ñтавка нового Ñлемента перед выбранным Ñлементом + + ```js + // jQuery + $newEl.insertBefore(queryString); + + // Ðативно + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target); + ``` + +- [3.7](#3.7) insertAfter + + Ð’Ñтавка новго Ñлемента поÑле выбранного Ñлемента + + ```js + // jQuery + $newEl.insertAfter(queryString); + + // Ðативно + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target.nextSibling); + ``` + +- [3.8](#3.8) is + + Возвращает `true` еÑли Ñовпадает Ñ Ñелектором запроÑа + + ```js + // jQuery - заметьте что `is` так же работает Ñ `function` или `elements` которые не имют к Ñтому Ð¾Ñ‚Ð½Ð¾ÑˆÐµÐ½Ð¸Ñ + $el.is(selector); + + // Ðативно + el.matches(selector); + ``` + +**[⬆ Ðаверх](#Содержание)** + +## Ajax + +Заменить Ñ [fetch](https://github.com/camsong/fetch-ie8) и [fetch-jsonp](https://github.com/camsong/fetch-jsonp) + +**[⬆ Ðаверх](#Содержание)** + +## Ð¡Ð¾Ð±Ñ‹Ñ‚Ð¸Ñ + +Ð”Ð»Ñ Ð¿Ð¾Ð»Ð½Ð¾Ð¹ замены Ñ Ð¿Ñ€Ð¾ÑтранÑтвом имен и делегациÑ, ÑоÑлатьÑÑ Ð½Ð° [oui-dom-events](https://github.com/oneuijs/oui-dom-events) + +- [5.1](#5.1) СвÑзать Ñобытие иÑÐ¿Ð¾Ð»ÑŒÐ·ÑƒÑ on + + ```js + // jQuery + $el.on(eventName, eventHandler); + + // Ðативно + el.addEventListener(eventName, eventHandler); + ``` + +- [5.2](#5.2) ОтвÑзать Ñобытие иÑÐ¿Ð¾Ð»ÑŒÐ·ÑƒÑ off + + ```js + // jQuery + $el.off(eventName, eventHandler); + + // Ðативно + el.removeEventListener(eventName, eventHandler); + ``` + +- [5.3](#5.3) Trigger + + ```js + // jQuery + $(el).trigger('custom-event', {key1: 'data'}); + + // Ðативно + if (window.CustomEvent) { + const event = new CustomEvent('custom-event', {detail: {key1: 'data'}}); + } else { + const event = document.createEvent('CustomEvent'); + event.initCustomEvent('custom-event', true, true, {key1: 'data'}); + } + + el.dispatchEvent(event); + ``` + +**[⬆ Ðаверх](#Содержание)** + +## Утилиты + +- [6.1](#6.1) isArray + + ```js + // jQuery + $.isArray(range); + + // Ðативно + Array.isArray(range); + ``` + +- [6.2](#6.2) Trim + + ```js + // jQuery + $.trim(string); + + // Ðативно + string.trim(); + ``` + +- [6.3](#6.3) Ðазначение объекта + + Дополнительно, иÑпользуйте полифил object.assign https://github.com/ljharb/object.assign + + ```js + // jQuery + $.extend({}, defaultOpts, opts); + + // Ðативно + Object.assign({}, defaultOpts, opts); + ``` + +- [6.4](#6.4) Contains + + ```js + // jQuery + $.contains(el, child); + + // Ðативно + el !== child && el.contains(child); + ``` + +**[⬆ Ðаверх](#Содержание)** + +## Ðльтернативы + +* [You Might Not Need jQuery](http://youmightnotneedjquery.com/) - Примеры как иÑполнÑÑŽÑ‚ÑÑ Ñ‡Ð°Ñтые ÑобытиÑ, Ñлементы, ajax и тд Ñ Ð²Ð°Ð½Ð¸Ð»ÑŒÐ½Ñ‹Ð¼ javascript. +* [npm-dom](http://github.com/npm-dom) и [webmodules](http://github.com/webmodules) - Отдельные DOM модули можно найти на NPM + +## Переводы + +* [한국어](./README.ko-KR.md) +* [简体中文](./README.zh-CN.md) +* [Bahasa Melayu](./README-my.md) +* [Bahasa Indonesia](./README-id.md) +* [Português(PT-BR)](./README.pt-BR.md) +* [Tiếng Việt Nam](./README-vi.md) +* [Español](./README-es.md) +* [РуÑÑкий](./README-ru.md) +* [Türkçe](./README-tr.md) + +## Поддержка браузеров + +![Chrome](https://raw.github.com/alrra/browser-logos/master/chrome/chrome_48x48.png) | ![Firefox](https://raw.github.com/alrra/browser-logos/master/firefox/firefox_48x48.png) | ![IE](https://raw.github.com/alrra/browser-logos/master/internet-explorer/internet-explorer_48x48.png) | ![Opera](https://raw.github.com/alrra/browser-logos/master/opera/opera_48x48.png) | ![Safari](https://raw.github.com/alrra/browser-logos/master/safari/safari_48x48.png) +--- | --- | --- | --- | --- | +Latest ✔ | Latest ✔ | 10+ ✔ | Latest ✔ | 6.1+ ✔ | + +# License + +MIT diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-tr.md b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-tr.md new file mode 100644 index 000000000..85d614f5f --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-tr.md @@ -0,0 +1,665 @@ +## jQuery'e İhtiyacınız Yok [![Build Status](https://travis-ci.org/oneuijs/You-Dont-Need-jQuery.svg)](https://travis-ci.org/oneuijs/You-Dont-Need-jQuery) + +Önyüz ortamları bugünlerde çok hızlı geliÅŸiyor, öyle ki modern tarayıcılar DOM/DOM APİ'lere ait önemli gereklilikleri çoktan yerine getirdiler. DOM iÅŸleme ve olaylar için, en baÅŸtan jQuery ögrenmemize gerek kalmadı. Bu arada, üstünlükleri ile jQuery'i önemsizleÅŸtiren ve doÄŸrudan DOM deÄŸiÅŸikliklerinin bir Anti-pattern olduÄŸunu gösteren, React, Angular ve Vue gibi geliÅŸmiÅŸ önyüz kütüphanelerine ayrıca teÅŸekkür ederiz. Bu proje, IE10+ desteÄŸi ile coÄŸunluÄŸu jQuery yöntemlerine alternatif olan yerleÅŸik uygulamaları içerir. + +## İçerik Tablosu + +1. [Sorgu seçiciler](#sorgu-seçiciler) +1. [CSS & Stil](#css--stil) +1. [DOM düzenleme](#dom-düzenleme) +1. [Ajax](#ajax) +1. [Olaylar](#olaylar) +1. [Araçlar](#araçlar) +1. [Alternatifler](#alternatifler) +1. [Çeviriler](#Çeviriler) +1. [Tarayıcı desteÄŸi](#tarayıcı-desteÄŸi) + +## Sorgu seçiciler + +Yaygın olan class, id ve özellik seçiciler yerine, `document.querySelector` yada `document.querySelectorAll` kullanabiliriz. Ayrıldıkları nokta: +* `document.querySelector` ilk seçilen öğeyi döndürür +* `document.querySelectorAll` Seçilen tüm öğeleri NodeList olarak geri döndürür. `[].slice.call(document.querySelectorAll(selector) || []);` kullanarak bir diziye dönüştürebilirsiniz. +* Herhangi bir öğenin seçilememesi durumda ise, jQuery `[]` döndürürken, DOM API `null` döndürecektir. Null Pointer istisnası almamak için `||` ile varsayılan deÄŸere atama yapabilirsiniz, örnek: `document.querySelectorAll(selector) || []` + +> Uyarı: `document.querySelector` ve `document.querySelectorAll` biraz **YAVAÅž** olabilir, Daha hızlısını isterseniz, `getElementById`, `document.getElementsByClassName` yada `document.getElementsByTagName` kullanabilirsiniz. + +- [1.0](#1.0) Seçici ile sorgu + + ```js + // jQuery + $('selector'); + + // YerleÅŸik + document.querySelectorAll('selector'); + ``` + +- [1.1](#1.1) Sınıf ile sorgu + + ```js + // jQuery + $('.class'); + + // YerleÅŸik + document.querySelectorAll('.class'); + + // yada + document.getElementsByClassName('class'); + ``` + +- [1.2](#1.2) Id ile sorgu + + ```js + // jQuery + $('#id'); + + // YerleÅŸik + document.querySelector('#id'); + + // yada + document.getElementById('id'); + ``` + +- [1.3](#1.3) Özellik ile sorgu + + ```js + // jQuery + $('a[target=_blank]'); + + // YerleÅŸik + document.querySelectorAll('a[target=_blank]'); + ``` + +- [1.4](#1.4) Öğe eriÅŸimi + + + Node'a eriÅŸim + + ```js + // jQuery + $el.find('li'); + + // YerleÅŸik + el.querySelectorAll('li'); + ``` + + + Body'e eriÅŸim + + ```js + // jQuery + $('body'); + + // YerleÅŸik + document.body; + ``` + + + ÖzelliÄŸe eriÅŸim + + ```js + // jQuery + $el.attr('foo'); + + // YerleÅŸik + el.getAttribute('foo'); + ``` + + + Data özelliÄŸine eriÅŸim + + ```js + // jQuery + $el.data('foo'); + + // YerleÅŸik + // getAttribute kullanarak + el.getAttribute('data-foo'); + // EÄŸer IE 11+ kullanıyor iseniz, `dataset` ile de eriÅŸebilirsiniz + el.dataset['foo']; + ``` + +- [1.5](#1.5) KardeÅŸ/Önceki/Sonraki öğeler + + + KardeÅŸ öğeler + + ```js + // jQuery + $el.siblings(); + + // YerleÅŸik + [].filter.call(el.parentNode.children, function(child) { + return child !== el; + }); + ``` + + + Önceki öğeler + + ```js + // jQuery + $el.prev(); + + // YerleÅŸik + el.previousElementSibling; + ``` + + + Sonraki öğeler + + ```js + // jQuery + $el.next(); + + // YerleÅŸik + el.nextElementSibling; + ``` + +- [1.6](#1.6) En yakın + + Verilen seçici ile eÅŸleÅŸen ilk öğeyi döndürür, geçerli öğeden baÅŸlayarak document'a kadar geçiÅŸ yapar. + + ```js + // jQuery + $el.closest(selector); + + // YerleÅŸik - Sadece en güncellerde, IE desteklemiyor + el.closest(selector); + + // YerleÅŸik - IE10+ + function closest(el, selector) { + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + while (el) { + if (matchesSelector.call(el, selector)) { + return el; + } else { + el = el.parentElement; + } + } + return null; + } + ``` + +- [1.7](#1.7) Önceki atalar + + Verilen seçici ile eÅŸleÅŸen öğe veya DOM node veya jQuery nesnesi hariç, mevcut öğe ile aradaki tüm önceki ataları bir set dahilinde verir. + + ```js + // jQuery + $el.parentsUntil(selector, filter); + + // YerleÅŸik + function parentsUntil(el, selector, filter) { + const result = []; + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + // eÅŸleÅŸtirme, atadan baÅŸlar + el = el.parentElement; + while (el && !matchesSelector.call(el, selector)) { + if (!filter) { + result.push(el); + } else { + if (matchesSelector.call(el, filter)) { + result.push(el); + } + } + el = el.parentElement; + } + return result; + } + ``` + +- [1.8](#1.8) Form + + + Input/Textarea + + ```js + // jQuery + $('#my-input').val(); + + // YerleÅŸik + document.querySelector('#my-input').value; + ``` + + + e.currentTarget ile `.radio` arasındaki dizini verir + + ```js + // jQuery + $(e.currentTarget).index('.radio'); + + // YerleÅŸik + [].indexOf.call(document.querySelectAll('.radio'), e.currentTarget); + ``` + +- [1.9](#1.9) Iframe İçeriÄŸi + + Mevcut Iframe için `$('iframe').contents()` yerine `contentDocument` döndürür. + + + Iframe İçeriÄŸi + + ```js + // jQuery + $iframe.contents(); + + // YerleÅŸik + iframe.contentDocument; + ``` + + + Iframe seçici + + ```js + // jQuery + $iframe.contents().find('.css'); + + // YerleÅŸik + iframe.contentDocument.querySelectorAll('.css'); + ``` + +**[⬆ üste dön](#İçerik-tablosu)** + +## CSS & Stil + +- [2.1](#2.1) CSS + + + Stili verir + + ```js + // jQuery + $el.css("color"); + + // YerleÅŸik + // NOT: Bilinen bir hata, eÄŸer stil deÄŸeri 'auto' ise 'auto' döndürür + const win = el.ownerDocument.defaultView; + // null sahte tipleri döndürmemesi için + win.getComputedStyle(el, null).color; + ``` + + + Stil deÄŸiÅŸtir + + ```js + // jQuery + $el.css({ color: "#ff0011" }); + + // YerleÅŸik + el.style.color = '#ff0011'; + ``` + + + Stil deÄŸeri al/deÄŸiÅŸtir + + EÄŸer aynı anda birden fazla stili deÄŸiÅŸtirmek istiyor iseniz, oui-dom-utils paketi içindeki [setStyles](https://github.com/oneuijs/oui-dom-utils/blob/master/src/index.js#L194) metoduna göz atınız. + + + + Sınıf ekle + + ```js + // jQuery + $el.addClass(className); + + // YerleÅŸik + el.classList.add(className); + ``` + + + Sınıf çıkart + + ```js + // jQuery + $el.removeClass(className); + + // YerleÅŸik + el.classList.remove(className); + ``` + + + sınfı var mı? + + ```js + // jQuery + $el.hasClass(className); + + // YerleÅŸik + el.classList.contains(className); + ``` + + + Sınfı takas et + + ```js + // jQuery + $el.toggleClass(className); + + // YerleÅŸik + el.classList.toggle(className); + ``` + +- [2.2](#2.2) GeniÅŸlik ve Yükseklik + + GeniÅŸlik ve Yükseklik teorik olarak aynı ÅŸekilde, örnek olarak Yükseklik veriliyor + + + Window YüksekliÄŸi + + ```js + // window yüksekliÄŸi + $(window).height(); + // kaydırma çubuÄŸu olmaksızın, jQuery ile aynı + window.document.documentElement.clientHeight; + // kaydırma çubuÄŸu ile birlikte + window.innerHeight; + ``` + + + Document yüksekliÄŸi + + ```js + // jQuery + $(document).height(); + + // YerleÅŸik + document.documentElement.scrollHeight; + ``` + + + Öğe yüksekliÄŸi + + ```js + // jQuery + $el.height(); + + // YerleÅŸik + function getHeight(el) { + const styles = this.getComputedStyles(el); + const height = el.offsetHeight; + const borderTopWidth = parseFloat(styles.borderTopWidth); + const borderBottomWidth = parseFloat(styles.borderBottomWidth); + const paddingTop = parseFloat(styles.paddingTop); + const paddingBottom = parseFloat(styles.paddingBottom); + return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom; + } + // Tamsayı olarak daha doÄŸru olanı(`border-box` iken, `height` esas; `content-box` ise, `height + padding + border` esas alınır) + el.clientHeight; + // Ondalık olarak daha doÄŸru olanı(`border-box` iken, `height` esas; `content-box` ise, `height + padding + border` esas alınır) + el.getBoundingClientRect().height; + ``` + +- [2.3](#2.3) Pozisyon ve Ara-Açıklığı + + + Pozisyon + + ```js + // jQuery + $el.position(); + + // YerleÅŸik + { left: el.offsetLeft, top: el.offsetTop } + ``` + + + Ara-Açıklığı + + ```js + // jQuery + $el.offset(); + + // YerleÅŸik + function getOffset (el) { + const box = el.getBoundingClientRect(); + + return { + top: box.top + window.pageYOffset - document.documentElement.clientTop, + left: box.left + window.pageXOffset - document.documentElement.clientLeft + } + } + ``` + +- [2.4](#2.4) Üste kaydır + + ```js + // jQuery + $(window).scrollTop(); + + // YerleÅŸik + (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop; + ``` + +**[⬆ üste dön](#İçerik-tablosu)** + +## DOM düzenleme + +- [3.1](#3.1) Çıkartma + ```js + // jQuery + $el.remove(); + + // YerleÅŸik + el.parentNode.removeChild(el); + ``` + +- [3.2](#3.2) Metin + + + Get text + + ```js + // jQuery + $el.text(); + + // YerleÅŸik + el.textContent; + ``` + + + Set text + + ```js + // jQuery + $el.text(string); + + // YerleÅŸik + el.textContent = string; + ``` + +- [3.3](#3.3) HTML + + + HTML'i alma + + ```js + // jQuery + $el.html(); + + // YerleÅŸik + el.innerHTML; + ``` + + + HTML atama + + ```js + // jQuery + $el.html(htmlString); + + // YerleÅŸik + el.innerHTML = htmlString; + ``` + +- [3.4](#3.4) Sona ekleme + + Ata öğenin son çocuÄŸundan sonra öğe ekleme + + ```js + // jQuery + $el.append("
hello
"); + + // YerleÅŸik + el.insertAdjacentHTML("beforeend","
hello
"); + ``` + +- [3.5](#3.5) Öne ekleme + + ```js + // jQuery + $el.prepend("
hello
"); + + // YerleÅŸik + el.insertAdjacentHTML("afterbegin","
hello
"); + ``` + +- [3.6](#3.6) Öncesine Ekleme + + Seçili öğeden önceki yere yeni öğe ekleme + + ```js + // jQuery + $newEl.insertBefore(queryString); + + // YerleÅŸik + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target); + ``` + +- [3.7](#3.7) Sonrasına ekleme + + Seçili öğeden sonraki yere yeni öğe ekleme + + ```js + // jQuery + $newEl.insertAfter(queryString); + + // YerleÅŸik + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target.nextSibling); + ``` + +- [3.8](#3.8) eÅŸit mi? + + Sorgu seçici ile eÅŸleÅŸiyor ise `true` döner + + ```js + // jQuery için not: `is` aynı zamanda `function` veya `elements` için de geçerlidir fakat burada bir önemi bulunmuyor + $el.is(selector); + + // YerleÅŸik + el.matches(selector); + ``` +- [3.9](#3.9) Klonlama + + Mevcut öğenin bir derin kopyasını oluÅŸturur + + ```js + // jQuery + $el.clone(); + + // YerleÅŸik + el.cloneNode(); + + // Derin kopya için, `true` parametresi kullanınız + ``` +**[⬆ üste dön](#İçerik-tablosu)** + +## Ajax + +[Fetch API](https://fetch.spec.whatwg.org/) ajax için XMLHttpRequest yerine kullanan yeni standarttır. Chrome ve Firefox destekler, eski tarayıcılar için polyfill kullanabilirsiniz. + +IE9+ ve üstü için [github/fetch](http://github.com/github/fetch) yada IE8+ ve üstü için [fetch-ie8](https://github.com/camsong/fetch-ie8/), JSONP istekler için [fetch-jsonp](https://github.com/camsong/fetch-jsonp) deneyiniz. + +**[⬆ üste dön](#İçerik-tablosu)** + +## Olaylar + +Namespace ve Delegasyon ile tam olarak deÄŸiÅŸtirmek için, https://github.com/oneuijs/oui-dom-events sayfasına bakınız + +- [5.1](#5.1) on ile bir öğeye baÄŸlama + + ```js + // jQuery + $el.on(eventName, eventHandler); + + // YerleÅŸik + el.addEventListener(eventName, eventHandler); + ``` + +- [5.2](#5.2) off ile bir baÄŸlamayı sonlandırma + + ```js + // jQuery + $el.off(eventName, eventHandler); + + // YerleÅŸik + el.removeEventListener(eventName, eventHandler); + ``` + +- [5.3](#5.3) Tetikleyici + + ```js + // jQuery + $(el).trigger('custom-event', {key1: 'data'}); + + // YerleÅŸik + if (window.CustomEvent) { + const event = new CustomEvent('custom-event', {detail: {key1: 'data'}}); + } else { + const event = document.createEvent('CustomEvent'); + event.initCustomEvent('custom-event', true, true, {key1: 'data'}); + } + + el.dispatchEvent(event); + ``` + +**[⬆ üste dön](#İçerik-tablosu)** + +## Araçlar + +- [6.1](#6.1) isArray + + ```js + // jQuery + $.isArray(range); + + // YerleÅŸik + Array.isArray(range); + ``` + +- [6.2](#6.2) Trim + + ```js + // jQuery + $.trim(string); + + // YerleÅŸik + string.trim(); + ``` + +- [6.3](#6.3) Nesne atama + + Türetmek için, object.assign polyfill'ini deneyiniz https://github.com/ljharb/object.assign + + ```js + // jQuery + $.extend({}, defaultOpts, opts); + + // YerleÅŸik + Object.assign({}, defaultOpts, opts); + ``` + +- [6.4](#6.4) İçerme + + ```js + // jQuery + $.contains(el, child); + + // YerleÅŸik + el !== child && el.contains(child); + ``` + +**[⬆ üste dön](#İçerik-tablosu)** + +## Alternatifler + +* [jQuery'e İhtiyacınız Yok](http://youmightnotneedjquery.com/) - Yaygın olan olay, öğe ve ajax iÅŸlemlerinin yalın Javascript'teki karşılıklarına ait örnekler +* [npm-dom](http://github.com/npm-dom) ve [webmodules](http://github.com/webmodules) - NPM için ayrı DOM modül organizasyonları + +## Çeviriler + +* [한국어](./README.ko-KR.md) +* [简体中文](./README.zh-CN.md) +* [Bahasa Melayu](./README-my.md) +* [Bahasa Indonesia](./README-id.md) +* [Português(PT-BR)](./README.pt-BR.md) +* [Tiếng Việt Nam](./README-vi.md) +* [Español](./README-es.md) +* [РуÑÑкий](./README-ru.md) +* [Türkçe](./README-tr.md) + +## Tarayıcı DesteÄŸi + +![Chrome](https://raw.github.com/alrra/browser-logos/master/chrome/chrome_48x48.png) | ![Firefox](https://raw.github.com/alrra/browser-logos/master/firefox/firefox_48x48.png) | ![IE](https://raw.github.com/alrra/browser-logos/master/internet-explorer/internet-explorer_48x48.png) | ![Opera](https://raw.github.com/alrra/browser-logos/master/opera/opera_48x48.png) | ![Safari](https://raw.github.com/alrra/browser-logos/master/safari/safari_48x48.png) +--- | --- | --- | --- | --- | +Latest ✔ | Latest ✔ | 10+ ✔ | Latest ✔ | 6.1+ ✔ | + +# Lisans + +MIT diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-vi.md b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-vi.md new file mode 100644 index 000000000..7eb60f456 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-vi.md @@ -0,0 +1,634 @@ +## Bạn không cần jQuery nữa đâu + +Ngày nay, môi trưá»ng lập trình front-end phát triển rất nhanh chóng, các trình duyệt hiện đại đã cung cấp các API đủ tốt để làm việc vá»›i DOM/BOM. Bạn không còn cần phải há»c vá» jQuery nữa. Äồng thá»i, nhá» sá»± ra Ä‘á»i cá»§a các thư viện như React, Angular và Vue đã khiến cho việc can thiệp trá»±c tiếp vào DOM trở thành má»™t việc không tốt. jQuery đã không còn quan trá»ng như trước nữa. Bài viết này tổng hợp những cách để thay thế các hàm cá»§a jQuery bằng các hàm được há»— trợ bởi trình duyệt, và hó cÅ©ng hoạt động trên IE 10+ + +## Danh mục + +1. [Query Selector](#query-selector) +1. [CSS & Style](#css--style) +1. [Thao tác vá»›i DOM](#thao-tác-vá»›i-dom) +1. [Ajax](#ajax) +1. [Events](#events) +1. [Hàm tiện ích](#hàm-tiện-ích) +1. [Ngôn ngữ khác](#ngôn-ngữ-khác) +1. [Các trình duyệt há»— trợ](#các-trình-duyệt-há»—-trợ) + +## Query Selector + +Äối vá»›i những selector phổ biến như class, id hoặc thuá»™c tính thì chúng ta có thể sá»­ dụng `document.querySelector` hoặc `document.querySelectorAll` để thay thế cho jQuery selector. Sá»± khác biệt cá»§a hai hàm này là ở chá»—: + +* `document.querySelector` trả vá» element đầu tiên được tìm thấy +* `document.querySelectorAll` trả vá» tất cả các element được tìm thấy dưới dạng má»™t instance cá»§a NodeList. Nó có thể được convert qua array bằng cách `[].slice.call(document.querySelectorAll(selector) || []);` +* Nếu không có element nào được tìm thấy, thì jQuery sẽ trả vá» má»™t array rá»—ng `[]` trong khi đó DOM API sẽ trả vá» `null`. Hãy chú ý đến Null Pointer Exception. Bạn có thể sá»­ dụng toán tá»­ `||` để đặt giá trị default nếu như không có element nào được tìm thấy, ví dụ như `document.querySelectorAll(selector) || []` + +> Chú ý : `document.querySelector` và `document.querySelectorAll` hoạt động khá **CHẬM**, hãy thá»­ dùng `getElementById`, `document.getElementsByClassName` hoặc `document.getElementsByTagName` nếu bạn muốn đạt hiệu suất tốt hÆ¡n. + +- [1.0](#1.0) Query bằng selector + + ```js + // jQuery + $('selector'); + + // Native + document.querySelectorAll('selector'); + ``` + +- [1.1](#1.1) Query bằng class + + ```js + // jQuery + $('.class'); + + // Native + document.querySelectorAll('.class'); + + // hoặc + document.getElementsByClassName('class'); + ``` + +- [1.2](#1.2) Query bằng id + + ```js + // jQuery + $('#id'); + + // Native + document.querySelector('#id'); + + // hoặc + document.getElementById('id'); + ``` + +- [1.3](#1.3) Query bằng thuá»™c tính + + ```js + // jQuery + $('a[target=_blank]'); + + // Native + document.querySelectorAll('a[target=_blank]'); + ``` + +- [1.4](#1.4) Tìm bất cứ gì. + + + Tìm node + + ```js + // jQuery + $el.find('li'); + + // Native + el.querySelectorAll('li'); + ``` + + + Tìm body + + ```js + // jQuery + $('body'); + + // Native + document.body; + ``` + + + lấy thuá»™c tính + + ```js + // jQuery + $el.attr('foo'); + + // Native + e.getAttribute('foo'); + ``` + + + Lấy giá trị cá»§a thuá»™c tính `data` + + ```js + // jQuery + $el.data('foo'); + + // Native + // using getAttribute + el.getAttribute('data-foo'); + // you can also use `dataset` if only need to support IE 11+ + el.dataset['foo']; + ``` + +- [1.5](#1.5) Tìm element cùng level/trước/sau + + + Element cùng level + + ```js + // jQuery + $el.siblings(); + + // Native + [].filter.call(el.parentNode.children, function(child) { + return child !== el; + }); + ``` + + + Element ở phía trước + + ```js + // jQuery + $el.prev(); + + // Native + el.previousElementSibling; + + ``` + + + Element ở phía sau + + ```js + // next + $el.next(); + el.nextElementSibling; + ``` + +- [1.6](#1.6) Element gần nhất + + Trả vá» element đầu tiên có selector khá»›p vá»›i yêu cầu khi duyệt từ element hiện tại trở lên tá»›i document. + + ```js + // jQuery + $el.closest(queryString); + + // Native + function closest(el, selector) { + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + while (el) { + if (matchesSelector.call(el, selector)) { + return el; + } else { + el = el.parentElement; + } + } + return null; + } + ``` + +- [1.7](#1.7) Tìm parent + + Truy ngược má»™t cách đệ quy tổ tiên cá»§a element hiện tại, cho đến khi tìm được má»™t element tổ tiên ( element cần tìm ) mà element đó là con trá»±c tiếp cá»§a element khá»›p vá»›i selector được cung cấp, Return lại element cần tìm đó. + + ```js + // jQuery + $el.parentsUntil(selector, filter); + + // Native + function parentsUntil(el, selector, filter) { + const result = []; + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + // match start from parent + el = el.parentElement; + while (el && !matchesSelector.call(el, selector)) { + if (!filter) { + result.push(el); + } else { + if (matchesSelector.call(el, filter)) { + result.push(el); + } + } + el = el.parentElement; + } + return result; + } + ``` + +- [1.8](#1.8) Form + + + Input/Textarea + + ```js + // jQuery + $('#my-input').val(); + + // Native + document.querySelector('#my-input').value; + ``` + + + Lấy index cá»§a e.currentTarget trong danh sách các element khá»›p vá»›i selector `.radio` + + ```js + // jQuery + $(e.currentTarget).index('.radio'); + + // Native + [].indexOf.call(document.querySelectAll('.radio'), e.currentTarget); + ``` + +- [1.9](#1.9) Ná»™i dung Iframe + + `$('iframe').contents()` trả vá» thuá»™c tính `contentDocument` cá»§a iframe được tìm thấy + + + Ná»i dung iframe + + ```js + // jQuery + $iframe.contents(); + + // Native + iframe.contentDocument; + ``` + + + Query Iframe + + ```js + // jQuery + $iframe.contents().find('.css'); + + // Native + iframe.contentDocument.querySelectorAll('.css'); + ``` + +**[⬆ Trở vỠđầu](#danh-mục)** + +## CSS & Style + +- [2.1](#2.1) CSS + + + Lấy style + + ```js + // jQuery + $el.css("color"); + + // Native + // NOTE: Bug đã được biết, sẽ trả vá» 'auto' nếu giá trị cá»§a style là 'auto' + const win = el.ownerDocument.defaultView; + // null means not return presudo styles + win.getComputedStyle(el, null).color; + ``` + + + Äặt style + + ```js + // jQuery + $el.css({ color: "#ff0011" }); + + // Native + el.style.color = '#ff0011'; + ``` + + + Lấy/Äặt Nhiá»u style + + Nếu bạn muốn đặt nhiá»u style má»™t lần, bạn có thể sẽ thích phương thức [setStyles](https://github.com/oneuijs/oui-dom-utils/blob/master/src/index.js#L194) trong thư viện oui-dom-utils. + + + + Thêm class và element + + ```js + // jQuery + $el.addClass(className); + + // Native + el.classList.add(className); + ``` + + + Loại bá» class class ra khá»i element + + ```js + // jQuery + $el.removeClass(className); + + // Native + el.classList.remove(className); + ``` + + + Kiểm tra xem element có class nào đó hay không + + ```js + // jQuery + $el.hasClass(className); + + // Native + el.classList.contains(className); + ``` + + + Toggle class + + ```js + // jQuery + $el.toggleClass(className); + + // Native + el.classList.toggle(className); + ``` + +- [2.2](#2.2) Chiá»u rá»™ng, chiá»u cao + + Vá» mặt lý thuyết thì chiá»u rá»™ng và chiá»u cao giống như nhau trong cả jQuery và DOM API: + + + Chiá»u rá»™ng cá»§a window + + ```js + // window height + $(window).height(); + // trừ Ä‘i scrollbar + window.document.documentElement.clientHeight; + // Tính luôn scrollbar + window.innerHeight; + ``` + + + Chiá»u cao cá»§a Document + + ```js + // jQuery + $(document).height(); + + // Native + document.documentElement.scrollHeight; + ``` + + + Chiá»u cao cá»§a element + + ```js + // jQuery + $el.height(); + + // Native + function getHeight(el) { + const styles = this.getComputedStyles(el); + const height = el.offsetHeight; + const borderTopWidth = parseFloat(styles.borderTopWidth); + const borderBottomWidth = parseFloat(styles.borderBottomWidth); + const paddingTop = parseFloat(styles.paddingTop); + const paddingBottom = parseFloat(styles.paddingBottom); + return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom; + } + // chính xác tá»›i số nguyên(khi có thuá»™c tính `box-sizing` là `border-box`, nó là `height`; khi box-sizing là `content-box`, nó là `height + padding + border`) + el.clientHeight; + // Chính xác tá»›i số thập phân(khi `box-sizing` là `border-box`, nó là `height`; khi `box-sizing` là `content-box`, nó là `height + padding + border`) + el.getBoundingClientRect().height; + ``` + +- [2.3](#2.3) Position & Offset + + + Position + + ```js + // jQuery + $el.position(); + + // Native + { left: el.offsetLeft, top: el.offsetTop } + ``` + + + Offset + + ```js + // jQuery + $el.offset(); + + // Native + function getOffset (el) { + const box = el.getBoundingClientRect(); + + return { + top: box.top + window.pageYOffset - document.documentElement.clientTop, + left: box.left + window.pageXOffset - document.documentElement.clientLeft + } + } + ``` + +- [2.4](#2.4) Scroll Top + + ```js + // jQuery + $(window).scrollTop(); + + // Native + (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop; + ``` + +**[⬆ Trở vỠđầu](#danh-mục)** + +## Thao tác vá»›i DOM + +- [3.1](#3.1) Loại bá» + ```js + // jQuery + $el.remove(); + + // Native + el.parentNode.removeChild(el); + ``` + +- [3.2](#3.2) Text + + + Lấy text + + ```js + // jQuery + $el.text(); + + // Native + el.textContent; + ``` + + + Äặt giá trị text + + ```js + // jQuery + $el.text(string); + + // Native + el.textContent = string; + ``` + +- [3.3](#3.3) HTML + + + Lấy HTML + + ```js + // jQuery + $el.html(); + + // Native + el.innerHTML; + ``` + + + Äặt giá trị HTML + + ```js + // jQuery + $el.html(htmlString); + + // Native + el.innerHTML = htmlString; + ``` + +- [3.4](#3.4) Append + + append má»™t element sau element con cuối cùng cá»§a element cha + + ```js + // jQuery + $el.append("
hello
"); + + // Native + let newEl = document.createElement('div'); + newEl.setAttribute('id', 'container'); + newEl.innerHTML = 'hello'; + el.appendChild(newEl); + ``` + +- [3.5](#3.5) Prepend + + ```js + // jQuery + $el.prepend("
hello
"); + + // Native + let newEl = document.createElement('div'); + newEl.setAttribute('id', 'container'); + newEl.innerHTML = 'hello'; + el.insertBefore(newEl, el.firstChild); + ``` + +- [3.6](#3.6) insertBefore + + Chèn má»™t node vào trước element được query. + + ```js + // jQuery + $newEl.insertBefore(queryString); + + // Native + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target); + ``` + +- [3.7](#3.7) insertAfter + + Chèn node vào sau element được query + + ```js + // jQuery + $newEl.insertAfter(queryString); + + // Native + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target.nextSibling); + ``` + +**[⬆ Trở vỠđầu](#danh-mục)** + +## Ajax + +Thay thế bằng [fetch](https://github.com/camsong/fetch-ie8) và [fetch-jsonp](https://github.com/camsong/fetch-jsonp) + +**[⬆ Trở vỠđầu](#danh-mục)** + +## Events + +Äể có má»™t sá»± thay thế đầy đủ nhất, bạn nên sá»­ dụng https://github.com/oneuijs/oui-dom-events + +- [5.1](#5.1) Bind event bằng on + + ```js + // jQuery + $el.on(eventName, eventHandler); + + // Native + el.addEventListener(eventName, eventHandler); + ``` + +- [5.2](#5.2) Unbind event bằng off + + ```js + // jQuery + $el.off(eventName, eventHandler); + + // Native + el.removeEventListener(eventName, eventHandler); + ``` + +- [5.3](#5.3) Trigger + + ```js + // jQuery + $(el).trigger('custom-event', {key1: 'data'}); + + // Native + if (window.CustomEvent) { + const event = new CustomEvent('custom-event', {detail: {key1: 'data'}}); + } else { + const event = document.createEvent('CustomEvent'); + event.initCustomEvent('custom-event', true, true, {key1: 'data'}); + } + + el.dispatchEvent(event); + ``` + +**[⬆ Trở vỠđầu](#danh-mục)** + +## Hàm tiện ích + +- [6.1](#6.1) isArray + + ```js + // jQuery + $.isArray(range); + + // Native + Array.isArray(range); + ``` + +- [6.2](#6.2) Trim + + ```js + // jQuery + $.trim(string); + + // Native + string.trim(); + ``` + +- [6.3](#6.3) Object Assign + + Mở rá»™ng, sá»­ dụng object.assign https://github.com/ljharb/object.assign + + ```js + // jQuery + $.extend({}, defaultOpts, opts); + + // Native + Object.assign({}, defaultOpts, opts); + ``` + +- [6.4](#6.4) Contains + + ```js + // jQuery + $.contains(el, child); + + // Native + el !== child && el.contains(child); + ``` + +**[⬆ Trở vỠđầu](#danh-mục)** + +## Ngôn ngữ khác + +* [한국어](./README.ko-KR.md) +* [简体中文](./README.zh-CN.md) +* [Bahasa Melayu](./README-my.md) +* [Português(PT-BR)](./README.pt-BR.md) +* [Tiếng Việt Nam](./README-vi.md) +* [РуÑÑкий](./README-ru.md) +* [Türkçe](./README-tr.md) + +## Các trình duyệt há»— trợ + +![Chrome](https://raw.github.com/alrra/browser-logos/master/chrome/chrome_48x48.png) | ![Firefox](https://raw.github.com/alrra/browser-logos/master/firefox/firefox_48x48.png) | ![IE](https://raw.github.com/alrra/browser-logos/master/internet-explorer/internet-explorer_48x48.png) | ![Opera](https://raw.github.com/alrra/browser-logos/master/opera/opera_48x48.png) | ![Safari](https://raw.github.com/alrra/browser-logos/master/safari/safari_48x48.png) +--- | --- | --- | --- | --- | +Latest ✔ | Latest ✔ | 10+ ✔ | Latest ✔ | 6.1+ ✔ | + +# Giấy phép + +MIT diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README.ko-KR.md b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README.ko-KR.md new file mode 100644 index 000000000..54455115e --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README.ko-KR.md @@ -0,0 +1,709 @@ +## You Don't Need jQuery + +오늘날 프론트엔드 개발 í™˜ê²½ì€ ê¸‰ê²©ížˆ 진화하고 있고, ëª¨ë˜ ë¸Œë¼ìš°ì €ë“¤ì€ ì´ë¯¸ 충분히 ë§Žì€ DOM/BOM APIë“¤ì„ êµ¬í˜„í–ˆìŠµë‹ˆë‹¤. 우리는 jQuery를 DOM 처리나 ì´ë²¤íŠ¸ë¥¼ 위해 처ìŒë¶€í„° 배울 필요가 없습니다. React, Angular, Vueê°™ì€ í”„ë¡ íŠ¸ì—”ë“œ ë¼ì´ë¸ŒëŸ¬ë¦¬ë“¤ì´ 주ë„ê¶Œì„ ì°¨ì§€í•˜ëŠ” ë™ì•ˆ DOMì„ ë°”ë¡œ 처리하는 ê²ƒì€ ì•ˆí‹°íŒ¨í„´ì´ ë˜ì—ˆê³ , jQueryì˜ ì¤‘ìš”ì„±ì€ ì¤„ì–´ë“¤ì—ˆìŠµë‹ˆë‹¤. ì´ í”„ë¡œì íŠ¸ëŠ” ëŒ€ë¶€ë¶„ì˜ jQuery ë©”ì†Œë“œì˜ ëŒ€ì•ˆì„ IE 10+ ì´ìƒì„ ì§€ì›í•˜ëŠ” 네ì´í‹°ë¸Œ 구현으로 소개합니다. + +## 목차 + +1. [Query Selector](#query-selector) +1. [CSS & Style](#css--style) +1. [DOM ì¡°ìž‘](#dom-ì¡°ìž‘) +1. [Ajax](#ajax) +1. [ì´ë²¤íЏ](#ì´ë²¤íЏ) +1. [유틸리티](#유틸리티) +1. [대안방법](#대안방법) +1. [번역](#번역) +1. [브ë¼ìš°ì € ì§€ì›](#브ë¼ìš°ì €-ì§€ì›) + +## Query Selector + +í‰ë²”한 class, id, attributeê°™ì€ selecotor는 `document.querySelector`나 `document.querySelectorAll`으로 대체할 수 있습니다. +* `document.querySelector`는 ì²˜ìŒ ë§¤ì¹­ëœ ì—˜ë¦¬ë¨¼íŠ¸ë¥¼ 반환합니다. +* `document.querySelectorAll`는 모든 ë§¤ì¹­ëœ ì—˜ë¦¬ë¨¼íŠ¸ë¥¼ NodeList로 반환합니다. `[].slice.call`ì„ ì‚¬ìš©í•´ì„œ Array로 변환할 수 있습니다. +* 만약 ë§¤ì¹­ëœ ì—˜ë¦¬ë©˜íŠ¸ê°€ 없으면 jQuery는 `[]` 를 반환하지만 DOM API는 `null`ì„ ë°˜í™˜í•©ë‹ˆë‹¤. Null Pointer Exceptionì— ì£¼ì˜í•˜ì„¸ìš”. + +> 안내: `document.querySelector`와 `document.querySelectorAll`는 꽤 **ëŠë¦½ë‹ˆë‹¤**, `getElementById`나 `document.getElementsByClassName`, `document.getElementsByTagName`를 사용하면 í¼í¬ë¨¼ìŠ¤ê°€ í–¥ìƒì„ 기대할 수 있습니다. + +- [1.0](#1.0) selector로 찾기 + + ```js + // jQuery + $('selector'); + + // Native + document.querySelectorAll('selector'); + ``` + +- [1.1](#1.1) class로 찾기 + + ```js + // jQuery + $('.class'); + + // Native + document.querySelectorAll('.class'); + + // or + document.getElementsByClassName('class'); + ``` + +- [1.2](#1.2) id로 찾기 + + ```js + // jQuery + $('#id'); + + // Native + document.querySelector('#id'); + + // or + document.getElementById('id'); + ``` + +- [1.3](#1.3) ì†ì„±(attribute)으로 찾기 + + ```js + // jQuery + $('a[target=_blank]'); + + // Native + document.querySelectorAll('a[target=_blank]'); + ``` + +- [1.4](#1.4) ìžì‹ì—서 찾기 + + ```js + // jQuery + $el.find('li'); + + // Native + el.querySelectorAll('li'); + ``` + +- [1.5](#1.5) 형제/ì´ì „/ë‹¤ìŒ ì—˜ë¦¬ë¨¼íŠ¸ 찾기 + + + 형제 엘리먼트 + + ```js + // jQuery + $el.siblings(); + + // Native + [].filter.call(el.parentNode.children, function(child) { + return child !== el; + }); + ``` + + + ì´ì „ 엘리먼트 + + ```js + // jQuery + $el.prev(); + + // Native + el.previousElementSibling; + ``` + + + ë‹¤ìŒ ì—˜ë¦¬ë¨¼íŠ¸ + + ```js + // jQuery + $el.next(); + + // Native + el.nextElementSibling; + ``` + +- [1.6](#1.6) Closest + + 현재 엘리먼트부터 document로 ì´ë™í•˜ë©´ì„œ 주어진 셀렉터와 ì¼ì¹˜í•˜ëŠ” 가장 가까운 엘리먼트를 반환합니다. + + ```js + // jQuery + $el.closest(selector); + + // Native - 최신 브ë¼ìš°ì €ë§Œ, IE는 ë¯¸ì§€ì› + el.closest(selector); + + // Native - IE10 ì´ìƒ + function closest(el, selector) { + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + while (el) { + if (matchesSelector.call(el, selector)) { + return el; + } else { + el = el.parentElement; + } + } + return null; + } + ``` + +- [1.7](#1.7) Parents Until + + 주어진 ì…€ë ‰í„°ì— ë§¤ì¹­ë˜ëŠ” 엘리먼트를 찾기까지 부모 íƒœê·¸ë“¤ì„ ìœ„ë¡œ 올ë¼ê°€ë©° íƒìƒ‰í•˜ì—¬ 저장해ë‘었다가 DOM 노드 ë˜ëŠ” jQuery object로 반환합니다. + + ```js + // jQuery + $el.parentsUntil(selector, filter); + + // Native + function parentsUntil(el, selector, filter) { + const result = []; + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + // match start from parent + el = el.parentElement; + while (el && !matchesSelector.call(el, selector)) { + if (!filter) { + result.push(el); + } else { + if (matchesSelector.call(el, filter)) { + result.push(el); + } + } + el = el.parentElement; + } + return result; + } + ``` + +- [1.8](#1.8) Form + + + Input/Textarea + + ```js + // jQuery + $('#my-input').val(); + + // Native + document.querySelector('#my-input').value; + ``` + + + e.currentTargetì´ `.radio`ì˜ ëª‡ë²ˆì§¸ì¸ì§€ 구하기 + + ```js + // jQuery + $(e.currentTarget).index('.radio'); + + // Native + [].indexOf.call(document.querySelectAll('.radio'), e.currentTarget); + ``` + +- [1.9](#1.9) Iframe Contents + + `$('iframe').contents()`는 iframeì— í•œì •í•´ì„œ `contentDocument`를 반환합니다. + + + Iframe contents + + ```js + // jQuery + $iframe.contents(); + + // Native + iframe.contentDocument; + ``` + + + Iframeì—서 찾기 + + ```js + // jQuery + $iframe.contents().find('.css'); + + // Native + iframe.contentDocument.querySelectorAll('.css'); + ``` + +- [1.10](#1.10) body 얻기 + + ```js + // jQuery + $('body'); + + // Native + document.body; + ``` + +- [1.11](#1.11) ì†ì„± 얻기 ë° ì„¤ì • + + + ì†ì„± 얻기 + + ```js + // jQuery + $el.attr('foo'); + + // Native + el.getAttribute('foo'); + ``` + + ì†ì„± 설정하기 + + ```js + // jQuery, DOM 변형 ì—†ì´ ë©”ëª¨ë¦¬ì—서 ìž‘ë™ë©ë‹ˆë‹¤. + $el.attr('foo', 'bar'); + + // Native + el.setAttribute('foo', 'bar'); + ``` + + + `data-` ì†ì„± 얻기 + + ```js + // jQuery + $el.data('foo'); + + // Native (`getAttribute` 사용) + el.getAttribute('data-foo'); + // Native (IE 11 ì´ìƒì˜ ì§€ì›ë§Œ 필요하다면 `dataset`ì„ ì‚¬ìš©) + el.dataset['foo']; + ``` + +**[⬆ 목차로 ëŒì•„가기](#목차)** + +## CSS & Style + +- [2.1](#2.1) CSS + + + styleê°’ 얻기 + + ```js + // jQuery + $el.css("color"); + + // Native + // NOTE: 알려진 버그로, styleê°’ì´ 'auto'ì´ë©´ 'auto'를 반환합니다. + const win = el.ownerDocument.defaultView; + // nullì€ ê°€ìƒ ìŠ¤íƒ€ì¼ì€ 반환하지 않ìŒì„ ì˜ë¯¸í•©ë‹ˆë‹¤. + win.getComputedStyle(el, null).color; + ``` + + + styleê°’ 설정하기 + + ```js + // jQuery + $el.css({ color: "#ff0011" }); + + // Native + el.style.color = '#ff0011'; + ``` + + + Styleê°’ë“¤ì„ ë™ì‹œì— 얻거나 설정하기 + + 만약 í•œë²ˆì— ì—¬ëŸ¬ styleê°’ì„ ë°”ê¾¸ê³  싶다면 oui-dom-utils íŒ¨í‚¤ì§€ì˜ [setStyles](https://github.com/oneuijs/oui-dom-utils/blob/master/src/index.js#L194)를 사용해보세요. + + + + class 추가하기 + + ```js + // jQuery + $el.addClass(className); + + // Native + el.classList.add(className); + ``` + + + class 제거하기 + + ```js + // jQuery + $el.removeClass(className); + + // Native + el.classList.remove(className); + ``` + + + class를 í¬í•¨í•˜ê³  있는지 검사하기 + + ```js + // jQuery + $el.hasClass(className); + + // Native + el.classList.contains(className); + ``` + + + class 토글하기 + + ```js + // jQuery + $el.toggleClass(className); + + // Native + el.classList.toggle(className); + ``` + +- [2.2](#2.2) í­ê³¼ ë†’ì´ + + í­ê³¼ 높ì´ëŠ” ì´ë¡ ìƒ ë™ì¼í•©ë‹ˆë‹¤. 높ì´ë¡œ 예를 들겠습니다. + + + Windowì˜ ë†’ì´ + + ```js + // window ë†’ì´ + $(window).height(); + // jQuery처럼 스í¬ë¡¤ë°”를 제외하기 + window.document.documentElement.clientHeight; + // 스í¬ë¡¤ë°” í¬í•¨ + window.innerHeight; + ``` + + + 문서 ë†’ì´ + + ```js + // jQuery + $(document).height(); + + // Native + document.documentElement.scrollHeight; + ``` + + + Element ë†’ì´ + + ```js + // jQuery + $el.height(); + + // Native + function getHeight(el) { + const styles = this.getComputedStyles(el); + const height = el.offsetHeight; + const borderTopWidth = parseFloat(styles.borderTopWidth); + const borderBottomWidth = parseFloat(styles.borderBottomWidth); + const paddingTop = parseFloat(styles.paddingTop); + const paddingBottom = parseFloat(styles.paddingBottom); + return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom; + } + // 정수로 정확하게(`border-box`ì¼ ë•Œ ì´ ê°’ì€ `height`ì´ê³ , `content-box`ì¼ ë•Œ, ì´ ê°’ì€ `height + padding + border`) + el.clientHeight; + // 실수로 정확하게(`border-box`ì¼ ë•Œ ì´ ê°’ì€ `height`ì´ê³ , `content-box`ì¼ ë•Œ, ì´ ê°’ì€ `height + padding + border`) + el.getBoundingClientRect().height; + ``` + +- [2.3](#2.3) Position & Offset + + + Position + + ```js + // jQuery + $el.position(); + + // Native + { left: el.offsetLeft, top: el.offsetTop } + ``` + + + Offset + + ```js + // jQuery + $el.offset(); + + // Native + function getOffset (el) { + const box = el.getBoundingClientRect(); + + return { + top: box.top + window.pageYOffset - document.documentElement.clientTop, + left: box.left + window.pageXOffset - document.documentElement.clientLeft + } + } + ``` + +- [2.4](#2.4) Scroll Top + + ```js + // jQuery + $(window).scrollTop(); + + // Native + (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop; + ``` + +**[⬆ 목차로 ëŒì•„가기](#목차)** + +## DOM ì¡°ìž‘ + +- [3.1](#3.1) 제거 + ```js + // jQuery + $el.remove(); + + // Native + el.parentNode.removeChild(el); + ``` + +- [3.2](#3.2) Text + + + text 가져오기 + + ```js + // jQuery + $el.text(); + + // Native + el.textContent; + ``` + + + text 설정하기 + + ```js + // jQuery + $el.text(string); + + // Native + el.textContent = string; + ``` + +- [3.3](#3.3) HTML + + + HTML 가져오기 + + ```js + // jQuery + $el.html(); + + // Native + el.innerHTML; + ``` + + + HTML 설정하기 + + ```js + // jQuery + $el.html(htmlString); + + // Native + el.innerHTML = htmlString; + ``` + +- [3.4](#3.4) 해당 ì—˜ë¦¬ë¨¼íŠ¸ì˜ ìžì‹ë“¤ ë’¤ì— ë„£ê¸°(Append) + + 부모 ì—˜ë¦¬ë¨¼íŠ¸ì˜ ë§ˆì§€ë§‰ ìžì‹ìœ¼ë¡œ 엘리먼트를 추가합니다. + + ```js + // jQuery + $el.append("
hello
"); + + // Native + el.insertAdjacentHTML("beforeend","
hello
"); + ``` + +- [3.5](#3.5) 해당 ì—˜ë¦¬ë¨¼íŠ¸ì˜ ìžì‹ë“¤ ì•žì— ë„£ê¸°(Prepend) + + ```js + // jQuery + $el.prepend("
hello
"); + + // Native +el.insertAdjacentHTML("afterbegin","
hello
"); + ``` + +- [3.6](#3.6) 해당 엘리먼트 ì•žì— ë„£ê¸°(insertBefore) + + 새 노드를 ì„ íƒí•œ 엘리먼트 ì•žì— ë„£ìŠµë‹ˆë‹¤. + + ```js + // jQuery + $newEl.insertBefore(queryString); + + // Native + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target); + ``` + +- [3.7](#3.7) 해당 엘리먼트 ë’¤ì— ë„£ê¸°(insertAfter) + + 새 노드를 ì„ íƒí•œ 엘리먼트 ë’¤ì— ë„£ìŠµë‹ˆë‹¤. + + ```js + // jQuery + $newEl.insertAfter(queryString); + + // Native + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target.nextSibling); + ``` +- [3.8](#3.8) is + + query selector와 ì¼ì¹˜í•˜ë©´ `true` 를 반환합니다. + + ```js + // jQuery + $el.is(selector); + + // Native + el.matches(selector); + ``` + +- [3.9](#3.9) clone + + ì—˜ë¦¬ë¨¼íŠ¸ì˜ ë³µì œë³¸ì„ ë§Œë“­ë‹ˆë‹¤. + + ```js + // jQuery + $el.clone(); + + // Native + el.cloneNode(); + + // Deep cloneì€ íŒŒë¼ë¯¸í„°ë¥¼ `true` 로 설정하세요. + ``` + +- [3.10](#3.10) empty + + 모든 ìžì‹ 노드를 제거합니다. + + ```js + // jQuery + $el.empty(); + + // Native + el.innerHTML = ''; + ``` + +**[⬆ 목차로 ëŒì•„가기](#목차)** + +## Ajax + +[Fetch API](https://fetch.spec.whatwg.org/) 는 XMLHttpRequest를 ajax로 대체하는 새로운 표준 입니다. Chromeê³¼ Firefoxì—서 ìž‘ë™í•˜ë©°, polyfillì„ ì´ìš©í•´ì„œ 구형 브ë¼ìš°ì €ì—서 ìž‘ë™ë˜ë„ë¡ ë§Œë“¤ ìˆ˜ë„ ìžˆìŠµë‹ˆë‹¤. + +IE9 ì´ìƒì—서 ì§€ì›í•˜ëŠ” [github/fetch](http://github.com/github/fetch) í˜¹ì€ IE8 ì´ìƒì—서 ì§€ì›í•˜ëŠ” [fetch-ie8](https://github.com/camsong/fetch-ie8/), JSONP ìš”ì²­ì„ ë§Œë“œëŠ” [fetch-jsonp](https://github.com/camsong/fetch-jsonp)를 ì´ìš©í•´ë³´ì„¸ìš”. + +**[⬆ 목차로 ëŒì•„가기](#목차)** + +## ì´ë²¤íЏ + +namespace와 delegationì„ í¬í•¨í•´ì„œ 완전히 갈아 엎길 ì›í•˜ì‹œë©´ https://github.com/oneuijs/oui-dom-events 를 고려해보세요. + +- [5.1](#5.1) ì´ë²¤íЏ Bind 걸기 + + ```js + // jQuery + $el.on(eventName, eventHandler); + + // Native + el.addEventListener(eventName, eventHandler); + ``` + +- [5.2](#5.2) ì´ë²¤íЏ Bind 풀기 + + ```js + // jQuery + $el.off(eventName, eventHandler); + + // Native + el.removeEventListener(eventName, eventHandler); + ``` + +- [5.3](#5.3) ì´ë²¤íЏ ë°œìƒì‹œí‚¤ê¸°(Trigger) + + ```js + // jQuery + $(el).trigger('custom-event', {key1: 'data'}); + + // Native + if (window.CustomEvent) { + const event = new CustomEvent('custom-event', {detail: {key1: 'data'}}); + } else { + const event = document.createEvent('CustomEvent'); + event.initCustomEvent('custom-event', true, true, {key1: 'data'}); + } + + el.dispatchEvent(event); + ``` + +**[⬆ 목차로 ëŒì•„가기](#목차)** + +## 유틸리티 + +- [6.1](#6.1) ë°°ì—´ì¸ì§€ 검사(isArray) + + ```js + // jQuery + $.isArray(range); + + // Native + Array.isArray(range); + ``` + +- [6.2](#6.2) 앞뒤 공백 지우기(Trim) + + ```js + // jQuery + $.trim(string); + + // Native + string.trim(); + ``` + +- [6.3](#6.3) Object Assign + + 사용하려면 object.assign polyfillì„ ì‚¬ìš©í•˜ì„¸ìš”. https://github.com/ljharb/object.assign + + ```js + // jQuery + $.extend({}, defaultOpts, opts); + + // Native + Object.assign({}, defaultOpts, opts); + ``` + +- [6.4](#6.4) Contains + + ```js + // jQuery + $.contains(el, child); + + // Native + el !== child && el.contains(child); + ``` + +- [6.5](#6.5) inArray + + ```js + // jQuery + $.inArray(item, array); + + // Native + array.indexOf(item); + ``` + +- [6.6](#6.6) map + + ```js + // jQuery + $.map(array, function(value, index) { + }); + + // Native + Array.map(function(value, index) { + }); + ``` + +**[⬆ 목차로 ëŒì•„가기](#목차)** + +## 대안방법 + +* [You Might Not Need jQuery](http://youmightnotneedjquery.com/) - ì¼ë°˜ ìžë°”스í¬ë¦½íŠ¸ë¡œ 공통ì´ë²¤íЏ, 엘리먼트, ajax ë“±ì„ ë‹¤ë£¨ëŠ” 방법 예제. +* [npm-dom](http://github.com/npm-dom) ê³¼ [webmodules](http://github.com/webmodules) - 개별 DOMëª¨ë“ˆì„ NPMì—서 ì°¾ì„ ìˆ˜ 있습니다. + +## 번역 + +* [한국어](./README.ko-KR.md) +* [简体中文](./README.zh-CN.md) +* [Bahasa Melayu](./README-my.md) +* [Bahasa Indonesia](./README-id.md) +* [Português(PT-BR)](./README.pt-BR.md) +* [Tiếng Việt Nam](./README-vi.md) +* [Español](./README-es.md) +* [РуÑÑкий](./README-ru.md) +* [Türkçe](./README-tr.md) +* [Italian](./README-it.md) + +## 브ë¼ìš°ì € ì§€ì› + +![Chrome](https://raw.github.com/alrra/browser-logos/master/chrome/chrome_48x48.png) | ![Firefox](https://raw.github.com/alrra/browser-logos/master/firefox/firefox_48x48.png) | ![IE](https://raw.github.com/alrra/browser-logos/master/internet-explorer/internet-explorer_48x48.png) | ![Opera](https://raw.github.com/alrra/browser-logos/master/opera/opera_48x48.png) | ![Safari](https://raw.github.com/alrra/browser-logos/master/safari/safari_48x48.png) +--- | --- | --- | --- | --- | +Latest ✔ | Latest ✔ | 10+ ✔ | Latest ✔ | 6.1+ ✔ | + +# License + +MIT diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README.md b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README.md new file mode 100644 index 000000000..6476f5034 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README.md @@ -0,0 +1,1190 @@ +## You Don't Need jQuery [![Build Status](https://travis-ci.org/oneuijs/You-Dont-Need-jQuery.svg)](https://travis-ci.org/oneuijs/You-Dont-Need-jQuery) + +Frontend environments evolve rapidly nowadays, modern browsers have already implemented a great deal of DOM/BOM APIs which are good enough. We don't have to learn jQuery from scratch for DOM manipulation or events. In the meantime, thanks to the prevailment of frontend libraries such as React, Angular and Vue, manipulating DOM directly becomes anti-pattern, jQuery has never been less important. This project summarizes most of the jQuery method alternatives in native implementation, with IE 10+ support. + +## Table of Contents + +1. [Query Selector](#query-selector) +1. [CSS & Style](#css--style) +1. [DOM Manipulation](#dom-manipulation) +1. [Ajax](#ajax) +1. [Events](#events) +1. [Utilities](#utilities) +1. [Promises](#promises) +1. [Animation](#animation) +1. [Alternatives](#alternatives) +1. [Translations](#translations) +1. [Browser Support](#browser-support) + +## Query Selector + +In place of common selectors like class, id or attribute we can use `document.querySelector` or `document.querySelectorAll` for substitution. The differences lie in: +* `document.querySelector` returns the first matched element +* `document.querySelectorAll` returns all matched elements as NodeList. It can be converted to Array using `[].slice.call(document.querySelectorAll(selector) || []);` +* If no elements matched, jQuery would return `[]` whereas the DOM API will return `null`. Pay attention to Null Pointer Exception. You can also use `||` to set default value if not found, like `document.querySelectorAll(selector) || []` + +> Notice: `document.querySelector` and `document.querySelectorAll` are quite **SLOW**, try to use `getElementById`, `document.getElementsByClassName` or `document.getElementsByTagName` if you want to get a performance bonus. + +- [1.0](#1.0) Query by selector + + ```js + // jQuery + $('selector'); + + // Native + document.querySelectorAll('selector'); + ``` + +- [1.1](#1.1) Query by class + + ```js + // jQuery + $('.class'); + + // Native + document.querySelectorAll('.class'); + + // or + document.getElementsByClassName('class'); + ``` + +- [1.2](#1.2) Query by id + + ```js + // jQuery + $('#id'); + + // Native + document.querySelector('#id'); + + // or + document.getElementById('id'); + ``` + +- [1.3](#1.3) Query by attribute + + ```js + // jQuery + $('a[target=_blank]'); + + // Native + document.querySelectorAll('a[target=_blank]'); + ``` + +- [1.4](#1.4) Query in descendents + + ```js + // jQuery + $el.find('li'); + + // Native + el.querySelectorAll('li'); + ``` + +- [1.5](#1.5) Sibling/Previous/Next Elements + + + Sibling elements + + ```js + // jQuery + $el.siblings(); + + // Native + [].filter.call(el.parentNode.children, function(child) { + return child !== el; + }); + ``` + + + Previous elements + + ```js + // jQuery + $el.prev(); + + // Native + el.previousElementSibling; + ``` + + + Next elements + + ```js + // jQuery + $el.next(); + + // Native + el.nextElementSibling; + ``` + +- [1.6](#1.6) Closest + + Return the first matched element by provided selector, traversing from current element to document. + + ```js + // jQuery + $el.closest(selector); + + // Native - Only latest, NO IE + el.closest(selector); + + // Native - IE10+ + function closest(el, selector) { + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + while (el) { + if (matchesSelector.call(el, selector)) { + return el; + } else { + el = el.parentElement; + } + } + return null; + } + ``` + +- [1.7](#1.7) Parents Until + + Get the ancestors of each element in the current set of matched elements, up to but not including the element matched by the selector, DOM node, or jQuery object. + + ```js + // jQuery + $el.parentsUntil(selector, filter); + + // Native + function parentsUntil(el, selector, filter) { + const result = []; + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + // match start from parent + el = el.parentElement; + while (el && !matchesSelector.call(el, selector)) { + if (!filter) { + result.push(el); + } else { + if (matchesSelector.call(el, filter)) { + result.push(el); + } + } + el = el.parentElement; + } + return result; + } + ``` + +- [1.8](#1.8) Form + + + Input/Textarea + + ```js + // jQuery + $('#my-input').val(); + + // Native + document.querySelector('#my-input').value; + ``` + + + Get index of e.currentTarget between `.radio` + + ```js + // jQuery + $(e.currentTarget).index('.radio'); + + // Native + [].indexOf.call(document.querySelectAll('.radio'), e.currentTarget); + ``` + +- [1.9](#1.9) Iframe Contents + + `$('iframe').contents()` returns `contentDocument` for this specific iframe + + + Iframe contents + + ```js + // jQuery + $iframe.contents(); + + // Native + iframe.contentDocument; + ``` + + + Iframe Query + + ```js + // jQuery + $iframe.contents().find('.css'); + + // Native + iframe.contentDocument.querySelectorAll('.css'); + ``` + +- [1.10](#1.10) Get body + + ```js + // jQuery + $('body'); + + // Native + document.body; + ``` + +- [1.11](#1.11) Attribute getter and setter + + + Get an attribute + + ```js + // jQuery + $el.attr('foo'); + + // Native + el.getAttribute('foo'); + ``` + + Set an attribute + + ```js + // jQuery, note that this works in memory without change the DOM + $el.attr('foo', 'bar'); + + // Native + el.setAttribute('foo', 'bar'); + ``` + + + Get a `data-` attribute + + ```js + // jQuery + $el.data('foo'); + + // Native (use `getAttribute`) + el.getAttribute('data-foo'); + // Native (use `dataset` if only need to support IE 11+) + el.dataset['foo']; + ``` + +**[⬆ back to top](#table-of-contents)** + +## CSS & Style + +- [2.1](#2.1) CSS + + + Get style + + ```js + // jQuery + $el.css("color"); + + // Native + // NOTE: Known bug, will return 'auto' if style value is 'auto' + const win = el.ownerDocument.defaultView; + // null means not return pseudo styles + win.getComputedStyle(el, null).color; + ``` + + + Set style + + ```js + // jQuery + $el.css({ color: "#ff0011" }); + + // Native + el.style.color = '#ff0011'; + ``` + + + Get/Set Styles + + Note that if you want to set multiple styles once, you could refer to [setStyles](https://github.com/oneuijs/oui-dom-utils/blob/master/src/index.js#L194) method in oui-dom-utils package. + + + + Add class + + ```js + // jQuery + $el.addClass(className); + + // Native + el.classList.add(className); + ``` + + + Remove class + + ```js + // jQuery + $el.removeClass(className); + + // Native + el.classList.remove(className); + ``` + + + has class + + ```js + // jQuery + $el.hasClass(className); + + // Native + el.classList.contains(className); + ``` + + + Toggle class + + ```js + // jQuery + $el.toggleClass(className); + + // Native + el.classList.toggle(className); + ``` + +- [2.2](#2.2) Width & Height + + Width and Height are theoretically identical, take Height as example: + + + Window height + + ```js + // window height + $(window).height(); + // without scrollbar, behaves like jQuery + window.document.documentElement.clientHeight; + // with scrollbar + window.innerHeight; + ``` + + + Document height + + ```js + // jQuery + $(document).height(); + + // Native + document.documentElement.scrollHeight; + ``` + + + Element height + + ```js + // jQuery + $el.height(); + + // Native + function getHeight(el) { + const styles = this.getComputedStyles(el); + const height = el.offsetHeight; + const borderTopWidth = parseFloat(styles.borderTopWidth); + const borderBottomWidth = parseFloat(styles.borderBottomWidth); + const paddingTop = parseFloat(styles.paddingTop); + const paddingBottom = parseFloat(styles.paddingBottom); + return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom; + } + // accurate to integer(when `border-box`, it's `height`; when `content-box`, it's `height + padding + border`) + el.clientHeight; + // accurate to decimal(when `border-box`, it's `height`; when `content-box`, it's `height + padding + border`) + el.getBoundingClientRect().height; + ``` + +- [2.3](#2.3) Position & Offset + + + Position + + ```js + // jQuery + $el.position(); + + // Native + { left: el.offsetLeft, top: el.offsetTop } + ``` + + + Offset + + ```js + // jQuery + $el.offset(); + + // Native + function getOffset (el) { + const box = el.getBoundingClientRect(); + + return { + top: box.top + window.pageYOffset - document.documentElement.clientTop, + left: box.left + window.pageXOffset - document.documentElement.clientLeft + } + } + ``` + +- [2.4](#2.4) Scroll Top + + ```js + // jQuery + $(window).scrollTop(); + + // Native + (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop; + ``` + +**[⬆ back to top](#table-of-contents)** + +## DOM Manipulation + +- [3.1](#3.1) Remove + ```js + // jQuery + $el.remove(); + + // Native + el.parentNode.removeChild(el); + ``` + +- [3.2](#3.2) Text + + + Get text + + ```js + // jQuery + $el.text(); + + // Native + el.textContent; + ``` + + + Set text + + ```js + // jQuery + $el.text(string); + + // Native + el.textContent = string; + ``` + +- [3.3](#3.3) HTML + + + Get HTML + + ```js + // jQuery + $el.html(); + + // Native + el.innerHTML; + ``` + + + Set HTML + + ```js + // jQuery + $el.html(htmlString); + + // Native + el.innerHTML = htmlString; + ``` + +- [3.4](#3.4) Append + + Append child element after the last child of parent element + + ```js + // jQuery + $el.append("
hello
"); + + // Native + el.insertAdjacentHTML("beforeend","
hello
"); + ``` + +- [3.5](#3.5) Prepend + + ```js + // jQuery + $el.prepend("
hello
"); + + // Native + el.insertAdjacentHTML("afterbegin","
hello
"); + ``` + +- [3.6](#3.6) insertBefore + + Insert a new node before the selected elements + + ```js + // jQuery + $newEl.insertBefore(queryString); + + // Native + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target); + ``` + +- [3.7](#3.7) insertAfter + + Insert a new node after the selected elements + + ```js + // jQuery + $newEl.insertAfter(queryString); + + // Native + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target.nextSibling); + ``` + +- [3.8](#3.8) is + + Return `true` if it matches the query selector + + ```js + // jQuery - Notice `is` also work with `function` or `elements` which is not concerned here + $el.is(selector); + + // Native + el.matches(selector); + ``` +- [3.9](#3.9) clone + + Create a deep copy of that element + + ```js + // jQuery + $el.clone(); + + // Native + el.cloneNode(); + + // For Deep clone , set param as `true` + ``` + +- [3.10](#3.10) empty + + Remove all child nodes + + ```js + // jQuery + $el.empty(); + + // Native + el.innerHTML = ''; + ``` + +- [3.11](#3.11) wrap + + Wrap an HTML structure around each element + + ```js + // jQuery + $('.inner').wrap('
'); + + // Native + [].slice.call(document.querySelectorAll('.inner')).forEach(function(el){ + var wrapper = document.createElement('div'); + wrapper.className = 'wrapper'; + el.parentNode.insertBefore(wrapper, el); + el.parentNode.removeChild(el); + wrapper.appendChild(el); + }); + ``` + +- [3.12](#3.12) unwrap + + Remove the parents of the set of matched elements from the DOM + + ```js + // jQuery + $('.inner').unwrap(); + + // Native + [].slice.call(document.querySelectorAll('.inner')).forEach(function(el){ + [].slice.call(el.childNodes).forEach(function(child){ + el.parentNode.insertBefore(child, el); + }); + el.parentNode.removeChild(el); + }); + ``` + + - [3.13](#3.13) replaceWith + + Replace each element in the set of matched elements with the provided new content + + ```js + // jQuery + $('.inner').replaceWith('
'); + + // Native + [].slice.call(document.querySelectorAll('.inner')).forEach(function(el){ + var outer = document.createElement('div'); + outer.className = 'outer'; + el.parentNode.insertBefore(outer, el); + el.parentNode.removeChild(el); + }); + ``` + + +**[⬆ back to top](#table-of-contents)** + +## Ajax + +[Fetch API](https://fetch.spec.whatwg.org/) is the new standard to replace XMLHttpRequest to do ajax. It works on Chrome and Firefox, you can use polyfills to make it work on legacy browsers. + +Try [github/fetch](http://github.com/github/fetch) on IE9+ or [fetch-ie8](https://github.com/camsong/fetch-ie8/) on IE8+, [fetch-jsonp](https://github.com/camsong/fetch-jsonp) to make JSONP requests. + +**[⬆ back to top](#table-of-contents)** + +## Events + +For a complete replacement with namespace and delegation, refer to https://github.com/oneuijs/oui-dom-events + +- [5.1](#5.1) Bind an event with on + + ```js + // jQuery + $el.on(eventName, eventHandler); + + // Native + el.addEventListener(eventName, eventHandler); + ``` + +- [5.2](#5.2) Unbind an event with off + + ```js + // jQuery + $el.off(eventName, eventHandler); + + // Native + el.removeEventListener(eventName, eventHandler); + ``` + +- [5.3](#5.3) Trigger + + ```js + // jQuery + $(el).trigger('custom-event', {key1: 'data'}); + + // Native + if (window.CustomEvent) { + const event = new CustomEvent('custom-event', {detail: {key1: 'data'}}); + } else { + const event = document.createEvent('CustomEvent'); + event.initCustomEvent('custom-event', true, true, {key1: 'data'}); + } + + el.dispatchEvent(event); + ``` + +**[⬆ back to top](#table-of-contents)** + +## Utilities + +Most of utilities are found by native API. Others advanced functions could be choosed better utilities library focus on consistency and performance. Recommend [lodash](https://lodash.com) to replace. + +- [6.1](#6.1) Basic utilities + + + isArray + + Determine whether the argument is an array. + + ```js + // jQuery + $.isArray(array); + + // Native + Array.isArray(array); + ``` + + + isWindow + + Determine whether the argument is a window. + + ```js + // jQuery + $.isArray(obj); + + // Native + function isWindow(obj) { + return obj != null && obj === obj.window; + } + ``` + + + inArray + + Search for a specified value within an array and return its index (or -1 if not found). + + ```js + // jQuery + $.inArray(item, array); + + // Native + Array.indexOf(item); + ``` + + + isNumbic + + Determines whether its argument is a number. + Use `typeof` to decide type. if necessary to use library, sometimes `typeof` isn't accurate. + + ```js + // jQuery + $.isNumbic(item); + + // Native + function isNumbic(item) { + return typeof value === 'number'; + } + ``` + + + isFunction + + Determine if the argument passed is a JavaScript function object. + + ```js + // jQuery + $.isFunction(item); + + // Native + function isFunction(item) { + return typeof value === 'function'; + } + ``` + + + isEmptyObject + + Check to see if an object is empty (contains no enumerable properties). + + ```js + // jQuery + $.isEmptyObject(obj); + + // Native + function isEmptyObject(obj) { + for (let key in obj) { + return false; + } + return true; + } + ``` + + + isPlanObject + + Check to see if an object is a plain object (created using “{}†or “new Objectâ€). + + ```js + // jQuery + $.isPlanObject(obj); + + // Native + function isPlainObject(obj) { + if (typeof (obj) !== 'object' || obj.nodeType || obj != null && obj === obj.window) { + return false; + } + + if (obj.constructor && + !{}.hasOwnPropert.call(obj.constructor.prototype, 'isPrototypeOf')) { + return false; + } + + return true; + } + ``` + + + extend + + Merge the contents of two or more objects together into the first object. + object.assign is ES6 API, and you could use [polyfill](https://github.com/ljharb/object.assign) also. + + ```js + // jQuery + $.extend({}, defaultOpts, opts); + + // Native + Object.assign({}, defaultOpts, opts); + ``` + + + trim + + Remove the whitespace from the beginning and end of a string. + + ```js + // jQuery + $.trim(string); + + // Native + string.trim(); + ``` + + + map + + Translate all items in an array or object to new array of items. + + ```js + // jQuery + $.map(array, function(value, index) { + }); + + // Native + array.map(function(value, index) { + }); + ``` + + + each + + A generic iterator function, which can be used to seamlessly iterate over both objects and arrays. + + ```js + // jQuery + $.each(array, function(value, index) { + }); + + // Native + array.forEach(function(value, index) { + }); + ``` + + + grep + + Finds the elements of an array which satisfy a filter function. + + ```js + // jQuery + $.grep(array, function(value, index) { + }); + + // Native + array.filter(function(value, index) { + }); + ``` + + + type + + Determine the internal JavaScript [[Class]] of an object. + + ```js + // jQuery + $.type(obj); + + // Native + Object.prototype.toString.call(obj).replace(/^\[object (.+)\]$/, '$1').toLowerCase(); + ``` + + + merge + + Merge the contents of two arrays together into the first array. + + ```js + // jQuery + $.merge(array1, array2); + + // Native + // But concat function don't remove duplicate items. + function merge() { + return Array.prototype.concat.apply([], arguments) + } + ``` + + + now + + Return a number representing the current time. + + ```js + // jQuery + $.now(); + + // Native + Date.now(); + ``` + + + proxy + + Takes a function and returns a new one that will always have a particular context. + + ```js + // jQuery + $.proxy(fn, context); + + // Native + fn.bind(context); + ``` + + + makeArray + + Convert an array-like object into a true JavaScript array. + + ```js + // jQuery + $.makeArray(array); + + // Native + [].slice.call(array); + ``` + +- [6.2](#6.2) DOM utilities + + + unique + + Sorts an array of DOM elements, in place, with the duplicates removed. Note that this only works on arrays of DOM elements, not strings or numbers. + + Sizzle's API + + + contains + + Check to see if a DOM element is a descendant of another DOM element. + + ```js + // jQuery + $.contains(el, child); + + // Native + el !== child && el.contains(child); + ``` + +- [6.3](#6.3) Globaleval + + ```js + // jQuery + $.globaleval(code); + + // Native + function Globaleval(code) { + let script = document.createElement('script'); + script.text = code; + + document.head.appendChild(script).parentNode.removeChild(script); + } + + // Use eval, but context of eval is current, context of $.Globaleval is global. + eval(code); + ``` + +- [6.4](#6.4) parse + + + parseHTML + + Parses a string into an array of DOM nodes. + + ```js + // jQuery + $.parseHTML(htmlString); + + // Native + function parseHTML(string) { + const tmp = document.implementation.createHTMLDocument(); + tmp.body.innerHTML = string; + return tmp.body.children; + } + ``` + + + parseJSON + + Takes a well-formed JSON string and returns the resulting JavaScript value. + + ```js + // jQuery + $.parseJSON(str); + + // Native + JSON.parse(str); + ``` + +**[⬆ back to top](#table-of-contents)** + +## Promises + +A promise represents the eventual result of an asynchronous operation. jQuery has its own way to handle promises. Native JavaScript implements a thin and minimal API to handle promises according to the [Promises/A+](http://promises-aplus.github.io/promises-spec/) specification. + +- [7.1](#7.1) done, fail, always + + `done` is called when promise is resolved, `fail` is called when promise is rejected, `always` is called when promise is either resolved or rejected. + + ```js + // jQuery + $promise.done(doneCallback).fail(failCallback).always(alwaysCallback) + + // Native + promise.then(doneCallback, failCallback).then(alwaysCallback, alwaysCallback) + ``` + +- [7.2](#7.2) when + + `when` is used to handle multiple promises. It will resolve when all promises are resolved, and reject if either one is rejected. + + ```js + // jQuery + $.when($promise1, $promise2).done((promise1Result, promise2Result) => {}) + + // Native + Promise.all([$promise1, $promise2]).then([promise1Result, promise2Result] => {}); + ``` + +- [7.3](#7.3) Deferred + + Deferred is a way to create promises. + + ```js + // jQuery + function asyncFunc() { + var d = new $.Deferred(); + setTimeout(function() { + if(true) { + d.resolve('some_value_compute_asynchronously'); + } else { + d.reject('failed'); + } + }, 1000); + return d.promise(); + } + + // Native + function asyncFunc() { + return new Promise((resolve, reject) => { + setTimeout(function() { + if (true) { + resolve('some_value_compute_asynchronously'); + } else { + reject('failed'); + } + }, 1000); + }); + } + ``` + +**[⬆ back to top](#table-of-contents)** + +## Animation + +- [8.1](#8.1) Show & Hide + + ```js + // jQuery + $el.show(); + $el.hide(); + + // Native + // More detail about show method, please refer to https://github.com/oneuijs/oui-dom-utils/blob/master/src/index.js#L363 + el.style.display = ''|'inline'|'inline-block'|'inline-table'|'block'; + el.style.display = 'none'; + ``` + +- [8.2](#8.2) Toggle + + ```js + // jQuery + $el.toggle(); + + // Native + if (el.ownerDocument.defaultView.getComputedStyle(el, null).display === 'none') { + el.style.display = ''|'inline'|'inline-block'|'inline-table'|'block'; + } + else { + el.style.display = 'none'; + } + ``` + +- [8.3](#8.3) FadeIn & FadeOut + + ```js + // jQuery + $el.fadeIn(3000); + $el.fadeOut(3000); + + // Native + el.style.transition = 'opacity 3s'; + // fadeIn + el.style.opacity = '1'; + // fadeOut + el.style.opacity = '0'; + ``` + +- [8.4](#8.4) FadeTo + + ```js + // jQuery + $el.fadeTo('slow',0.15); + // Native + el.style.transition = 'opacity 3s'; // assume 'slow' equals 3 seconds + el.style.opacity = '0.15'; + ``` + +- [8.5](#8.5) FadeToggle + + ```js + // jQuery + $el.fadeToggle(); + + // Native + el.style.transition = 'opacity 3s'; + let { opacity } = el.ownerDocument.defaultView.getComputedStyle(el, null); + if (opacity === '1') { + el.style.opacity = '0'; + } + else { + el.style.opacity = '1'; + } + ``` + +- [8.6](#8.6) SlideUp & SlideDown + + ```js + // jQuery + $el.slideUp(); + $el.slideDown(); + + // Native + let originHeight = '100px'; + el.style.transition = 'height 3s'; + // slideUp + el.style.height = '0px'; + // slideDown + el.style.height = originHeight; + ``` + +- [8.7](#8.7) SlideToggle + + ```js + // jQuery + $el.slideToggle(); + + // Native + let originHeight = '100px'; + el.style.transition = 'height 3s'; + let { height } = el.ownerDocument.defaultView.getComputedStyle(el, null); + if (parseInt(height, 10) === 0) { + el.style.height = originHeight; + } + else { + el.style.height = '0px'; + } + ``` + +- [8.8](#8.8) Animate + + ```js + // jQuery + $el.animate({params}, speed); + + // Native + el.style.transition = 'all' + speed; + Object.keys(params).forEach(function(key) { + el.style[key] = params[key]; + }) + ``` + +## Alternatives + +* [You Might Not Need jQuery](http://youmightnotneedjquery.com/) - Examples of how to do common event, element, ajax etc with plain javascript. +* [npm-dom](http://github.com/npm-dom) and [webmodules](http://github.com/webmodules) - Organizations you can find individual DOM modules on NPM + +## Translations + +* [한국어](./README.ko-KR.md) +* [简体中文](./README.zh-CN.md) +* [Bahasa Melayu](./README-my.md) +* [Bahasa Indonesia](./README-id.md) +* [Português(PT-BR)](./README.pt-BR.md) +* [Tiếng Việt Nam](./README-vi.md) +* [Español](./README-es.md) +* [РуÑÑкий](./README-ru.md) +* [Türkçe](./README-tr.md) +* [Italian](./README-it.md) + +## Browser Support + +![Chrome](https://raw.github.com/alrra/browser-logos/master/chrome/chrome_48x48.png) | ![Firefox](https://raw.github.com/alrra/browser-logos/master/firefox/firefox_48x48.png) | ![IE](https://raw.github.com/alrra/browser-logos/master/internet-explorer/internet-explorer_48x48.png) | ![Opera](https://raw.github.com/alrra/browser-logos/master/opera/opera_48x48.png) | ![Safari](https://raw.github.com/alrra/browser-logos/master/safari/safari_48x48.png) +--- | --- | --- | --- | --- | +Latest ✔ | Latest ✔ | 10+ ✔ | Latest ✔ | 6.1+ ✔ | + +# License + +MIT diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README.pt-BR.md b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README.pt-BR.md new file mode 100644 index 000000000..e576f0ec4 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README.pt-BR.md @@ -0,0 +1,628 @@ +> #### You Don't Need jQuery + +Você não precisa de jQuery +--- + +Ambientes Frontend evoluem rapidamente nos dias de hoje, navegadores modernos já implementaram uma grande parte das APIs DOM/BOM que são boas o suficiente. Nós não temos que aprender jQuery a partir do zero para manipulação do DOM ou eventos. Nesse meio tempo, graças a bibliotecas frontend como React, Angular e Vue, a manipulação direta do DOM torna-se um anti-padrão, jQuery é menos importante do que nunca. Este projeto resume a maioria das alternativas dos métodos jQuery em implementação nativa, com suporte ao IE 10+. + +## Tabela de conteúdos + +1. [Query Selector](#query-selector) +1. [CSS & Estilo](#css--estilo) +1. [Manipulação do DOM](#manipulação-do-dom) +1. [Ajax](#ajax) +1. [Eventos](#eventos) +1. [Utilitários](#utilitários) +1. [Suporte dos Navegadores](#suporte-dos-navegadores) + +## Query Selector + +No lugar de seletores comuns como classe, id ou atributo podemos usar `document.querySelector` ou `document.querySelectorAll` para substituição. As diferenças são: +* `document.querySelector` retorna o primeiro elemento correspondente +* `document.querySelectorAll` retorna todos os elementos correspondentes como NodeList. Pode ser convertido para Array usando `[].slice.call(document.querySelectorAll(selector) || []);` +* Se não tiver elementos correspondentes, jQuery retornaria `[]` considerando que a DOM API irá retornar `null`. Preste atenção ao Null Pointer Exception. Você também pode usar `||` para definir um valor padrão caso nenhum elemento seja encontrado, como `document.querySelectorAll(selector) || []` + +> Aviso: `document.querySelector` e `document.querySelectorAll` são bastante **LENTOS**, tente usar `getElementById`, `document.getElementsByClassName` ou `document.getElementsByTagName` se você quer ter uma maior performance. + +- [1.0](#1.0) Query por seletor + + ```js + // jQuery + $('selector'); + + // Nativo + document.querySelectorAll('selector'); + ``` + +- [1.1](#1.1) Query por classe + + ```js + // jQuery + $('.class'); + + // Nativo + document.querySelectorAll('.class'); + + // ou + document.getElementsByClassName('class'); + ``` + +- [1.2](#1.2) Query por id + + ```js + // jQuery + $('#id'); + + // Nativo + document.querySelector('#id'); + + // ou + document.getElementById('id'); + ``` + +- [1.3](#1.3) Query por atributo + + ```js + // jQuery + $('a[target=_blank]'); + + // Nativo + document.querySelectorAll('a[target=_blank]'); + ``` + +- [1.4](#1.4) Find sth. + + + Busca por nós + + ```js + // jQuery + $el.find('li'); + + // Nativo + el.querySelectorAll('li'); + ``` + + + Buscar `body` + + ```js + // jQuery + $('body'); + + // Nativo + document.body; + ``` + + + Buscar atributos + + ```js + // jQuery + $el.attr('foo'); + + // Nativo + e.getAttribute('foo'); + ``` + + + Buscar atributos `data-` + + ```js + // jQuery + $el.data('foo'); + + // Nativo + // usando getAttribute + el.getAttribute('data-foo'); + // você também pode usar `dataset` se você precisar suportar apenas IE 11+ + el.dataset['foo']; + ``` + +- [1.5](#1.5) Sibling/Previous/Next Elements + + + Sibling elements + + ```js + // jQuery + $el.siblings(); + + // Nativo + [].filter.call(el.parentNode.children, function(child) { + return child !== el; + }); + ``` + + + Previous elements + + ```js + // jQuery + $el.prev(); + + // Nativo + el.previousElementSibling; + + ``` + + + Next elements + + ```js + // jQuery + $el.next(); + + // Nativo + el.nextElementSibling; + ``` + +- [1.6](#1.6) Closest + + Retorna o primeiro elemento que corresponda ao seletor, partindo do elemento atual para o document. + + ```js + // jQuery + $el.closest(queryString); + + // Nativo + function closest(el, selector) { + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + while (el) { + if (matchesSelector.call(el, selector)) { + return el; + } else { + el = el.parentElement; + } + } + return null; + } + ``` + +- [1.7](#1.7) Parents Until + + Obtém os ancestrais de cada elemento no atual conjunto de elementos combinados, mas não inclui o elemento correspondente pelo seletor, nó do DOM, ou objeto jQuery. + + ```js + // jQuery + $el.parentsUntil(selector, filter); + + // Nativo + function parentsUntil(el, selector, filter) { + const result = []; + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + // match start from parent + el = el.parentElement; + while (el && !matchesSelector.call(el, selector)) { + if (!filter) { + result.push(el); + } else { + if (matchesSelector.call(el, filter)) { + result.push(el); + } + } + el = el.parentElement; + } + return result; + } + ``` + +- [1.8](#1.8) Form + + + Input/Textarea + + ```js + // jQuery + $('#my-input').val(); + + // Nativo + document.querySelector('#my-input').value; + ``` + + + Obter o índice do e.currentTarget entre `.radio` + + ```js + // jQuery + $(e.currentTarget).index('.radio'); + + // Nativo + [].indexOf.call(document.querySelectAll('.radio'), e.currentTarget); + ``` + +- [1.9](#1.9) Iframe Contents + + `$('iframe').contents()` retorna `contentDocument` para este iframe específico + + + Iframe contents + + ```js + // jQuery + $iframe.contents(); + + // Nativo + iframe.contentDocument; + ``` + + + Iframe Query + + ```js + // jQuery + $iframe.contents().find('.css'); + + // Nativo + iframe.contentDocument.querySelectorAll('.css'); + ``` + +**[⬆ ir para o topo](#tabela-de-conteúdos)** + + +## CSS & Estilo + +- [2.1](#2.1) CSS + + + Obter estilo + + ```js + // jQuery + $el.css("color"); + + // Nativo + // AVISO: Bug conhecido, irá retornar 'auto' se o valor do estilo for 'auto' + const win = el.ownerDocument.defaultView; + // null significa não retornar estilos + win.getComputedStyle(el, null).color; + ``` + + + Definir Estilo + + ```js + // jQuery + $el.css({ color: "#ff0011" }); + + // Nativo + el.style.color = '#ff0011'; + ``` + + + Get/Set Styles + + Observe que se você deseja setar vários estilos de uma vez, você pode optar por [setStyles](https://github.com/oneuijs/oui-dom-utils/blob/master/src/index.js#L194) método no pacote oui-dom-utils. + + + + Adicionar classe + + ```js + // jQuery + $el.addClass(className); + + // Nativo + el.classList.add(className); + ``` + + + Remover classe + + ```js + // jQuery + $el.removeClass(className); + + // Nativo + el.classList.remove(className); + ``` + + + Verificar classe + + ```js + // jQuery + $el.hasClass(className); + + // Nativo + el.classList.contains(className); + ``` + + + Toggle class + + ```js + // jQuery + $el.toggleClass(className); + + // Nativo + el.classList.toggle(className); + ``` + +- [2.2](#2.2) Largura e Altura + + `width` e `height` são teoricamente idênticos, vamos pegar `height` como exemplo: + + + Altura da janela + + ```jsc + // window height + $(window).height(); + // sem scrollbar, se comporta como jQuery + window.document.documentElement.clientHeight; + // com scrollbar + window.innerHeight; + ``` + + + Altura do Documento + + ```js + // jQuery + $(document).height(); + + // Nativo + document.documentElement.scrollHeight; + ``` + + + Altura do Elemento + + ```js + // jQuery + $el.height(); + + // Nativo + function getHeight(el) { + const styles = this.getComputedStyles(el); + const height = el.offsetHeight; + const borderTopWidth = parseFloat(styles.borderTopWidth); + const borderBottomWidth = parseFloat(styles.borderBottomWidth); + const paddingTop = parseFloat(styles.paddingTop); + const paddingBottom = parseFloat(styles.paddingBottom); + return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom; + } + // preciso para inteiro(quando `border-box`, é `height`; quando `content-box`, é `height + padding + border`) + el.clientHeight; + // preciso para decimal(quando `border-box`, é `height`; quando `content-box`, é `height + padding + border`) + el.getBoundingClientRect().height; + ``` + +- [2.3](#2.3) Position & Offset + + + Position + + ```js + // jQuery + $el.position(); + + // Nativo + { left: el.offsetLeft, top: el.offsetTop } + ``` + + + Offset + + ```js + // jQuery + $el.offset(); + + // Nativo + function getOffset (el) { + const box = el.getBoundingClientRect(); + + return { + top: box.top + window.pageYOffset - document.documentElement.clientTop, + left: box.left + window.pageXOffset - document.documentElement.clientLeft + } + } + ``` + +- [2.4](#2.4) Rolar para o topo + + ```js + // jQuery + $(window).scrollTop(); + + // Nativo + (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop; + ``` + +**[⬆ ir para o topo](#tabela-de-conteúdos)** + +## Manipulação do Dom + +- [3.1](#3.1) Remover + ```js + // jQuery + $el.remove(); + + // Nativo + el.parentNode.removeChild(el); + ``` + +- [3.2](#3.2) Texto + + + Obter texto + + ```js + // jQuery + $el.text(); + + // Nativo + el.textContent; + ``` + + + Definir texto + + ```js + // jQuery + $el.text(string); + + // Nativo + el.textContent = string; + ``` + +- [3.3](#3.3) HTML + + + Obter HTML + + ```js + // jQuery + $el.html(); + + // Nativo + el.innerHTML; + ``` + + + Definir HTML + + ```js + // jQuery + $el.html(htmlString); + + // Nativo + el.innerHTML = htmlString; + ``` + +- [3.4](#3.4) Append + + Incluir elemento filho após o último filho do elemento pai. + + ```js + // jQuery + $el.append("
hello
"); + + // Nativo + let newEl = document.createElement('div'); + newEl.setAttribute('id', 'container'); + newEl.innerHTML = 'hello'; + el.appendChild(newEl); + ``` + +- [3.5](#3.5) Prepend + + ```js + // jQuery + $el.prepend("
hello
"); + + // Nativo + let newEl = document.createElement('div'); + newEl.setAttribute('id', 'container'); + newEl.innerHTML = 'hello'; + el.insertBefore(newEl, el.firstChild); + ``` + +- [3.6](#3.6) insertBefore + + Insere um novo nó antes dos elementos selecionados. + + ```js + // jQuery + $newEl.insertBefore(queryString); + + // Nativo + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target); + ``` + +- [3.7](#3.7) insertAfter + + Insere um novo nó após os elementos selecionados. + + ```js + // jQuery + $newEl.insertAfter(queryString); + + // Nativo + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target.nextSibling); + ``` + +**[⬆ ir para o topo](#tabela-de-conteúdos)** + +## Ajax + +Substitua por [fetch](https://github.com/camsong/fetch-ie8) e [fetch-jsonp](https://github.com/camsong/fetch-jsonp) + +**[⬆ ir para o topo](#tabela-de-conteúdos)** + +## Eventos + +Para uma substituição completa com namespace e delegation, consulte https://github.com/oneuijs/oui-dom-events + +- [5.1](#5.1) `Bind` num evento com `on` + + ```js + // jQuery + $el.on(eventName, eventHandler); + + // Nativo + el.addEventListener(eventName, eventHandler); + ``` + +- [5.2](#5.2) `Unbind` num evento com `off` + + ```js + // jQuery + $el.off(eventName, eventHandler); + + // Nativo + el.removeEventListener(eventName, eventHandler); + ``` + +- [5.3](#5.3) Trigger + + ```js + // jQuery + $(el).trigger('custom-event', {key1: 'data'}); + + // Nativo + if (window.CustomEvent) { + const event = new CustomEvent('custom-event', {detail: {key1: 'data'}}); + } else { + const event = document.createEvent('CustomEvent'); + event.initCustomEvent('custom-event', true, true, {key1: 'data'}); + } + + el.dispatchEvent(event); + ``` + +**[⬆ ir para o topo](#tabela-de-conteúdos)** + +## Utilitários + +- [6.1](#6.1) isArray + + ```js + // jQuery + $.isArray(range); + + // Nativo + Array.isArray(range); + ``` + +- [6.2](#6.2) Trim + + ```js + // jQuery + $.trim(string); + + // Nativo + string.trim(); + ``` + +- [6.3](#6.3) Object Assign + + Use o polyfill `object.assign` para eetender um Object: https://github.com/ljharb/object.assign + + ```js + // jQuery + $.extend({}, defaultOpts, opts); + + // Nativo + Object.assign({}, defaultOpts, opts); + ``` + +- [6.4](#6.4) Contains + + ```js + // jQuery + $.contains(el, child); + + // Nativo + el !== child && el.contains(child); + ``` + +**[⬆ ir para o topo](#tabela-de-conteúdos)** + +## Suporte dos Navegadores + +![Chrome](https://raw.github.com/alrra/browser-logos/master/chrome/chrome_48x48.png) | ![Firefox](https://raw.github.com/alrra/browser-logos/master/firefox/firefox_48x48.png) | ![IE](https://raw.github.com/alrra/browser-logos/master/internet-explorer/internet-explorer_48x48.png) | ![Opera](https://raw.github.com/alrra/browser-logos/master/opera/opera_48x48.png) | ![Safari](https://raw.github.com/alrra/browser-logos/master/safari/safari_48x48.png) +--- | --- | --- | --- | --- | +Latest ✔ | Latest ✔ | 10+ ✔ | Latest ✔ | 6.1+ ✔ | + +# Licença + +MIT diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README.zh-CN.md b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README.zh-CN.md new file mode 100644 index 000000000..24d6f8795 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README.zh-CN.md @@ -0,0 +1,656 @@ +## You Don't Need jQuery + +å‰ç«¯å‘展很快,现代æµè§ˆå™¨åŽŸç”Ÿ API å·²ç»è¶³å¤Ÿå¥½ç”¨ã€‚我们并ä¸éœ€è¦ä¸ºäº†æ“作 DOMã€Event ç­‰å†å­¦ä¹ ä¸€ä¸‹ jQuery çš„ APIã€‚åŒæ—¶ç”±äºŽ Reactã€Angularã€Vue 等框架的æµè¡Œï¼Œç›´æŽ¥æ“作 DOM ä¸å†æ˜¯å¥½çš„æ¨¡å¼ï¼ŒjQuery 使用场景大大å‡å°‘。本项目总结了大部分 jQuery API æ›¿ä»£çš„æ–¹æ³•ï¼Œæš‚æ—¶åªæ”¯æŒ IE10+ 以上æµè§ˆå™¨ã€‚ + +## 目录 + +1. [Query Selector](#query-selector) +1. [CSS & Style](#css--style) +1. [DOM Manipulation](#dom-manipulation) +1. [Ajax](#ajax) +1. [Events](#events) +1. [Utilities](#utilities) +1. [Alternatives](#alternatives) +1. [Translations](#translations) +1. [Browser Support](#browser-support) + +## Query Selector + +常用的 classã€idã€å±žæ€§ 选择器都å¯ä»¥ä½¿ç”¨ `document.querySelector` 或 `document.querySelectorAll` 替代。区别是 +* `document.querySelector` 返回第一个匹é…çš„ Element +* `document.querySelectorAll` 返回所有匹é…çš„ Element 组æˆçš„ NodeList。它å¯ä»¥é€šè¿‡ `[].slice.call()` æŠŠå®ƒè½¬æˆ Array +* 如果匹é…ä¸åˆ°ä»»ä½• Element,jQuery 返回空数组 `[]`,但 `document.querySelector` 返回 `null`,注æ„空指针异常。当找ä¸åˆ°æ—¶ï¼Œä¹Ÿå¯ä»¥ä½¿ç”¨ `||` 设置默认的值,如 `document.querySelectorAll(selector) || []` + +> 注æ„:`document.querySelector` å’Œ `document.querySelectorAll` 性能很**å·®**。如果想æé«˜æ€§èƒ½ï¼Œå°½é‡ä½¿ç”¨ `document.getElementById`ã€`document.getElementsByClassName` 或 `document.getElementsByTagName`。 + +- [1.0](#1.0) Query by selector + + ```js + // jQuery + $('selector'); + + // Native + document.querySelectorAll('selector'); + ``` + +- [1.1](#1.1) Query by class + + ```js + // jQuery + $('.css'); + + // Native + document.querySelectorAll('.css'); + + // or + document.getElementsByClassName('css'); + ``` + +- [1.2](#1.2) Query by id + + ```js + // jQuery + $('#id'); + + // Native + document.querySelector('#id'); + + // or + document.getElementById('id'); + ``` + +- [1.3](#1.3) Query by attribute + + ```js + // jQuery + $('a[target=_blank]'); + + // Native + document.querySelectorAll('a[target=_blank]'); + ``` + +- [1.4](#1.4) Find sth. + + + Find nodes + + ```js + // jQuery + $el.find('li'); + + // Native + el.querySelectorAll('li'); + ``` + + + Find body + + ```js + // jQuery + $('body'); + + // Native + document.body; + ``` + + + Find Attribute + + ```js + // jQuery + $el.attr('foo'); + + // Native + e.getAttribute('foo'); + ``` + + + Find data attribute + + ```js + // jQuery + $el.data('foo'); + + // Native + // using getAttribute + el.getAttribute('data-foo'); + // you can also use `dataset` if only need to support IE 11+ + el.dataset['foo']; + ``` + +- [1.5](#1.5) Sibling/Previous/Next Elements + + + Sibling elements + + ```js + // jQuery + $el.siblings(); + + // Native + [].filter.call(el.parentNode.children, function(child) { + return child !== el; + }); + ``` + + + Previous elements + + ```js + // jQuery + $el.prev(); + + // Native + el.previousElementSibling; + + ``` + + + Next elements + + ```js + // next + $el.next(); + el.nextElementSibling; + ``` + +- [1.6](#1.6) Closest + + Closest 获得匹é…选择器的第一个祖先元素,从当å‰å…ƒç´ å¼€å§‹æ²¿ DOM æ ‘å‘上。 + + ```js + // jQuery + $el.closest(queryString); + + // Native + function closest(el, selector) { + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + while (el) { + if (matchesSelector.call(el, selector)) { + return el; + } else { + el = el.parentElement; + } + } + return null; + } + ``` + +- [1.7](#1.7) Parents Until + + 获å–当剿¯ä¸€ä¸ªåŒ¹é…元素集的祖先,ä¸åŒ…括匹é…元素的本身。 + + ```js + // jQuery + $el.parentsUntil(selector, filter); + + // Native + function parentsUntil(el, selector, filter) { + const result = []; + const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; + + // match start from parent + el = el.parentElement; + while (el && !matchesSelector.call(el, selector)) { + if (!filter) { + result.push(el); + } else { + if (matchesSelector.call(el, filter)) { + result.push(el); + } + } + el = el.parentElement; + } + return result; + } + ``` + +- [1.8](#1.8) Form + + + Input/Textarea + + ```js + // jQuery + $('#my-input').val(); + + // Native + document.querySelector('#my-input').value; + ``` + + + Get index of e.currentTarget between `.radio` + + ```js + // jQuery + $(e.currentTarget).index('.radio'); + + // Native + [].indexOf.call(document.querySelectAll('.radio'), e.currentTarget); + ``` + +- [1.9](#1.9) Iframe Contents + + jQuery 对象的 iframe `contents()` 返回的是 iframe 内的 `document` + + + Iframe contents + + ```js + // jQuery + $iframe.contents(); + + // Native + iframe.contentDocument; + ``` + + + Iframe Query + + ```js + // jQuery + $iframe.contents().find('.css'); + + // Native + iframe.contentDocument.querySelectorAll('.css'); + ``` + +**[⬆ 回到顶部](#目录)** + +## CSS & Style + +- [2.1](#2.1) CSS + + + Get style + + ```js + // jQuery + $el.css("color"); + + // Native + // 注æ„:此处为了解决当 style 值为 auto 时,返回 auto 的问题 + const win = el.ownerDocument.defaultView; + // null çš„æ„æ€æ˜¯ä¸è¿”回伪类元素 + win.getComputedStyle(el, null).color; + ``` + + + Set style + + ```js + // jQuery + $el.css({ color: "#ff0011" }); + + // Native + el.style.color = '#ff0011'; + ``` + + + Get/Set Styles + + 注æ„,如果想一次设置多个 style,å¯ä»¥å‚考 oui-dom-utils 中 [setStyles](https://github.com/oneuijs/oui-dom-utils/blob/master/src/index.js#L194) 方法 + + + Add class + + ```js + // jQuery + $el.addClass(className); + + // Native + el.classList.add(className); + ``` + + + Remove class + + ```js + // jQuery + $el.removeClass(className); + + // Native + el.classList.remove(className); + ``` + + + has class + + ```js + // jQuery + $el.hasClass(className); + + // Native + el.classList.contains(className); + ``` + + + Toggle class + + ```js + // jQuery + $el.toggleClass(className); + + // Native + el.classList.toggle(className); + ``` + +- [2.2](#2.2) Width & Height + + Width 与 Height èŽ·å–æ–¹æ³•相åŒï¼Œä¸‹é¢ä»¥ Height 为例: + + + Window height + + ```js + // jQuery + $(window).height(); + + // Native + // ä¸å« scrollbar,与 jQuery 行为一致 + window.document.documentElement.clientHeight; + // å« scrollbar + window.innerHeight; + ``` + + + Document height + + ```js + // jQuery + $(document).height(); + + // Native + document.documentElement.scrollHeight; + ``` + + + Element height + + ```js + // jQuery + $el.height(); + + // Native + // 与 jQuery 一致(一直为 content 区域的高度) + function getHeight(el) { + const styles = this.getComputedStyles(el); + const height = el.offsetHeight; + const borderTopWidth = parseFloat(styles.borderTopWidth); + const borderBottomWidth = parseFloat(styles.borderBottomWidth); + const paddingTop = parseFloat(styles.paddingTop); + const paddingBottom = parseFloat(styles.paddingBottom); + return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom; + } + // 精确到整数(border-box 时为 height 值,content-box 时为 height + padding + border 值) + el.clientHeight; + // ç²¾ç¡®åˆ°å°æ•°ï¼ˆborder-box 时为 height 值,content-box 时为 height + padding + border 值) + el.getBoundingClientRect().height; + ``` + + + Iframe height + + $iframe .contents() 方法返回 iframe çš„ contentDocument + + ```js + // jQuery + $('iframe').contents().height(); + + // Native + iframe.contentDocument.documentElement.scrollHeight; + ``` + +- [2.3](#2.3) Position & Offset + + + Position + + ```js + // jQuery + $el.position(); + + // Native + { left: el.offsetLeft, top: el.offsetTop } + ``` + + + Offset + + ```js + // jQuery + $el.offset(); + + // Native + function getOffset (el) { + const box = el.getBoundingClientRect(); + + return { + top: box.top + window.pageYOffset - document.documentElement.clientTop, + left: box.left + window.pageXOffset - document.documentElement.clientLeft + } + } + ``` + +- [2.4](#2.4) Scroll Top + + ```js + // jQuery + $(window).scrollTop(); + + // Native + (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop; + ``` + +**[⬆ 回到顶部](#目录)** + +## DOM Manipulation + +- [3.1](#3.1) Remove + ```js + // jQuery + $el.remove(); + + // Native + el.parentNode.removeChild(el); + ``` + +- [3.2](#3.2) Text + + + Get text + + ```js + // jQuery + $el.text(); + + // Native + el.textContent; + ``` + + + Set text + + ```js + // jQuery + $el.text(string); + + // Native + el.textContent = string; + ``` + +- [3.3](#3.3) HTML + + + Get HTML + + ```js + // jQuery + $el.html(); + + // Native + el.innerHTML; + ``` + + + Set HTML + + ```js + // jQuery + $el.html(htmlString); + + // Native + el.innerHTML = htmlString; + ``` + +- [3.4](#3.4) Append + + Append æ’入到å­èŠ‚ç‚¹çš„æœ«å°¾ + + ```js + // jQuery + $el.append("
hello
"); + + // Native + let newEl = document.createElement('div'); + newEl.setAttribute('id', 'container'); + newEl.innerHTML = 'hello'; + el.appendChild(newEl); + ``` + +- [3.5](#3.5) Prepend + + ```js + // jQuery + $el.prepend("
hello
"); + + // Native + let newEl = document.createElement('div'); + newEl.setAttribute('id', 'container'); + newEl.innerHTML = 'hello'; + el.insertBefore(newEl, el.firstChild); + ``` + +- [3.6](#3.6) insertBefore + + åœ¨é€‰ä¸­å…ƒç´ å‰æ’入新节点 + + ```js + // jQuery + $newEl.insertBefore(queryString); + + // Native + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target); + ``` + +- [3.7](#3.7) insertAfter + + åœ¨é€‰ä¸­å…ƒç´ åŽæ’入新节点 + + ```js + // jQuery + $newEl.insertAfter(queryString); + + // Native + const target = document.querySelector(queryString); + target.parentNode.insertBefore(newEl, target.nextSibling); + ``` + +**[⬆ 回到顶部](#目录)** + +## Ajax + +用 [fetch](https://github.com/camsong/fetch-ie8) å’Œ [fetch-jsonp](https://github.com/camsong/fetch-jsonp) 替代 + +**[⬆ 回到顶部](#目录)** + +## Events + +完整地替代命å空间和事件代ç†ï¼Œé“¾æŽ¥åˆ° https://github.com/oneuijs/oui-dom-events + +- [5.1](#5.1) Bind an event with on + + ```js + // jQuery + $el.on(eventName, eventHandler); + + // Native + el.addEventListener(eventName, eventHandler); + ``` + +- [5.2](#5.2) Unbind an event with off + + ```js + // jQuery + $el.off(eventName, eventHandler); + + // Native + el.removeEventListener(eventName, eventHandler); + ``` + +- [5.3](#5.3) Trigger + + ```js + // jQuery + $(el).trigger('custom-event', {key1: 'data'}); + + // Native + if (window.CustomEvent) { + const event = new CustomEvent('custom-event', {detail: {key1: 'data'}}); + } else { + const event = document.createEvent('CustomEvent'); + event.initCustomEvent('custom-event', true, true, {key1: 'data'}); + } + + el.dispatchEvent(event); + ``` + +**[⬆ 回到顶部](#目录)** + +## Utilities + +- [6.1](#6.1) isArray + + ```js + // jQuery + $.isArray(range); + + // Native + Array.isArray(range); + ``` + +- [6.2](#6.2) Trim + + ```js + // jQuery + $.trim(string); + + // Native + string.trim(); + ``` + +- [6.3](#6.3) Object Assign + + 继承,使用 object.assign polyfill https://github.com/ljharb/object.assign + + ```js + // jQuery + $.extend({}, defaultOpts, opts); + + // Native + Object.assign({}, defaultOpts, opts); + ``` + +- [6.4](#6.4) Contains + + ```js + // jQuery + $.contains(el, child); + + // Native + el !== child && el.contains(child); + ``` + +**[⬆ 回到顶部](#目录)** + +## Alternatives + +* [ä½ å¯èƒ½ä¸éœ€è¦ jQuery (You Might Not Need jQuery)](http://youmightnotneedjquery.com/) - 如何使用原生 JavaScript 实现通用事件,元素,ajax 等用法。 +* [npm-dom](http://github.com/npm-dom) ä»¥åŠ [webmodules](http://github.com/webmodules) - 在 NPM 上æä¾›ç‹¬ç«‹ DOM 模å—的组织 + +## Translations + +* [한국어](./README.ko-KR.md) +* [简体中文](./README.zh-CN.md) +* [Bahasa Melayu](./README-my.md) +* [Bahasa Indonesia](./README-id.md) +* [Português(PT-BR)](./README.pt-BR.md) +* [Tiếng Việt Nam](./README-vi.md) +* [Español](./README-es.md) +* [РуÑÑкий](./README-ru.md) +* [Türkçe](./README-tr.md) +* [Italian](./README-it.md) + +## Browser Support + +![Chrome](https://raw.github.com/alrra/browser-logos/master/chrome/chrome_48x48.png) | ![Firefox](https://raw.github.com/alrra/browser-logos/master/firefox/firefox_48x48.png) | ![IE](https://raw.github.com/alrra/browser-logos/master/internet-explorer/internet-explorer_48x48.png) | ![Opera](https://raw.github.com/alrra/browser-logos/master/opera/opera_48x48.png) | ![Safari](https://raw.github.com/alrra/browser-logos/master/safari/safari_48x48.png) +--- | --- | --- | --- | --- | +Latest ✔ | Latest ✔ | 10+ ✔ | Latest ✔ | 6.1+ ✔ | + +# License + +MIT diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/git.git b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/git.git new file mode 100644 index 000000000..c115085f7 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/git.git @@ -0,0 +1 @@ +gitdir: ../.git/modules/You-Dont-Need-jQuery diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/karma.conf.js b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/karma.conf.js new file mode 100644 index 000000000..f935ae405 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/karma.conf.js @@ -0,0 +1,92 @@ +// Karma configuration +// Generated on Sun Nov 22 2015 22:10:47 GMT+0800 (CST) +require('babel-core/register'); + +module.exports = function(config) { + config.set({ + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: '.', + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ['mocha'], + + // list of files / patterns to load in the browser + files: [ + './test/**/*.spec.js' + ], + + // list of files to exclude + exclude: [ + ], + + // preprocess matching files before serving them to the browser + // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor + preprocessors: { + 'test/**/*.spec.js': ['webpack', 'sourcemap'] + }, + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ['progress'], + + coverageReporter: { + reporters: [ + {type: 'text'}, + {type: 'html', dir: 'coverage'}, + ] + }, + + webpackMiddleware: { + stats: 'minimal' + }, + + webpack: { + cache: true, + devtool: 'inline-source-map', + module: { + loaders: [{ + test: /\.jsx?$/, + loader: 'babel-loader', + exclude: /node_modules/ + }], + postLoaders: [{ + test: /\.js/, + exclude: /(test|node_modules)/, + loader: 'istanbul-instrumenter' + }], + }, + resolve: { + extensions: ['', '.js', '.jsx'] + } + }, + + // web server port + port: 9876, + + // enable / disable colors in the output (reporters and logs) + colors: true, + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: true, + + // start these browsers + // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher + browsers: ['Firefox'], + + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + // singleRun: false, + + // Concurrency level + // how many browser should be started simultanous + // concurrency: Infinity, + + // plugins: ['karma-phantomjs-launcher', 'karma-sourcemap-loader', 'karma-webpack'] + }) +} diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/package.json b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/package.json new file mode 100644 index 000000000..734e6ca90 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/package.json @@ -0,0 +1,53 @@ +{ + "name": "You-Dont-Need-jQuery", + "version": "1.0.0", + "description": "Examples of how to do query, style, dom, ajax, event etc like jQuery with plain javascript.", + "scripts": { + "test": "karma start --single-run", + "tdd": "karma start --auto-watch --no-single-run", + "test-cov": "karma start --auto-watch --single-run --reporters progress,coverage", + "lint": "eslint src test" + }, + "dependencies": {}, + "devDependencies": { + "babel-cli": "^6.2.0", + "babel-core": "^6.1.21", + "babel-eslint": "^4.1.5", + "babel-loader": "^6.2.0", + "babel-preset-es2015": "^6.1.18", + "babel-preset-stage-0": "^6.1.18", + "chai": "^3.4.1", + "eslint": "^1.9.0", + "eslint-config-airbnb": "^1.0.0", + "eslint-plugin-react": "^3.10.0", + "isparta": "^4.0.0", + "istanbul-instrumenter-loader": "^0.1.3", + "jquery": "^2.1.4", + "karma": "^0.13.15", + "karma-coverage": "^0.5.3", + "karma-firefox-launcher": "^0.1.7", + "karma-mocha": "^0.2.1", + "karma-sourcemap-loader": "^0.3.6", + "karma-webpack": "^1.7.0", + "mocha": "^2.3.4", + "webpack": "^1.12.9" + }, + "repository": { + "type": "git", + "url": "https://github.com/oneuijs/You-Dont-Need-jQuery.git" + }, + "keywords": [ + "convertion guide", + "jQuery", + "es6", + "es2015", + "babel", + "OneUI Group" + ], + "author": "OneUI Group", + "license": "MIT", + "bugs": { + "url": "https://github.com/oneuijs/You-Dont-Need-jQuery/issues" + }, + "homepage": "https://github.com/oneuijs/You-Dont-Need-jQuery" +} diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/README.md b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/README.md new file mode 100644 index 000000000..f0f376887 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/README.md @@ -0,0 +1,13 @@ +# Test cases for all the tips + +## Usage + +run all tests once +``` +npm run test +``` + +run tests on TDD(Test Driven Development) mode +``` +npm run tdd +``` diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/css.spec.js b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/css.spec.js new file mode 100644 index 000000000..99248f2bb --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/css.spec.js @@ -0,0 +1 @@ +// test for CSS related \ No newline at end of file diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/dom.spec.js b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/dom.spec.js new file mode 100644 index 000000000..5bfc287e9 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/dom.spec.js @@ -0,0 +1 @@ +// test for CSS and style related \ No newline at end of file diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/query.spec.js b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/query.spec.js new file mode 100644 index 000000000..f6b9c300e --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/query.spec.js @@ -0,0 +1,71 @@ +// tests for Query Selector related +import { expect } from 'chai'; +import $ from 'jquery'; + +describe('query selector', () => { + describe('basic', () => { + beforeEach(() => { + document.body.innerHTML = ` +
    +
  • I
  • +
  • II
  • +
  • III
  • +
  • +
      +
    • III.I
    • +
    • III.II
    • +
    +
  • +
+ `; + }); + + afterEach(() => { + const el = document.querySelector('#query-selector-test1'); + el.parentNode.removeChild(el); + }); + + it('1.0 Query by selector', () => { + const $els = $('li.item[data-role="red"]'); + const els = document.querySelectorAll('li.item[data-role="red"]'); + + expect($els.length).to.equal(2); + [].forEach.call($els, function($el, i) { + expect($el).to.equal(els[i]); + }); + }); + + it('1.1 Query by class', () => { + const $els = $('.item'); + const els = document.getElementsByClassName('item'); + + [].forEach.call($els, function($el, i) { + expect($el).to.equal(els[i]); + }); + }); + + it('1.2 Query by id', () => { + expect($('#nested-ul')[0]).to.equal(document.getElementById('nested-ul')); + }); + + it('1.3 Query by attribute', () => { + const $els = $('[data-role="blue"]'); + const els = document.querySelectorAll('[data-role="blue"]'); + + expect($els.length).to.equal(2); + [].forEach.call($els, function($el, i) { + expect($el).to.equal(els[i]); + }); + }); + + it('1.4 Query in descendents', () => { + const $els = $('#query-selector-test1').find('.item'); + const els = document.getElementById('query-selector-test1').querySelectorAll('.item'); + + expect($els.length).to.equal(4); + [].forEach.call($els, function($el, i) { + expect($el).to.equal(els[i]); + }); + }); + }); +}); \ No newline at end of file diff --git a/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/utilities.spec.js b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/utilities.spec.js new file mode 100644 index 000000000..d68e723c5 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/utilities.spec.js @@ -0,0 +1 @@ +// test for Utilities related \ No newline at end of file diff --git a/spec/fixtures/git/repo-with-submodules/git.git/COMMIT_EDITMSG b/spec/fixtures/git/repo-with-submodules/git.git/COMMIT_EDITMSG new file mode 100644 index 000000000..196384738 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/COMMIT_EDITMSG @@ -0,0 +1,9 @@ +submodules +# Please enter the commit message for your changes. Lines starting +# with '#' will be ignored, and an empty message aborts the commit. +# On branch master +# Changes to be committed: +# new file: .gitmodules +# new file: You-Dont-Need-jQuery +# new file: jstips +# diff --git a/spec/fixtures/git/repo-with-submodules/git.git/HEAD b/spec/fixtures/git/repo-with-submodules/git.git/HEAD new file mode 100644 index 000000000..cb089cd89 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/HEAD @@ -0,0 +1 @@ +ref: refs/heads/master diff --git a/spec/fixtures/git/repo-with-submodules/git.git/config b/spec/fixtures/git/repo-with-submodules/git.git/config new file mode 100644 index 000000000..ab57cc5f1 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/config @@ -0,0 +1,11 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = false + logallrefupdates = true + ignorecase = true + precomposeunicode = true +[submodule "jstips"] + url = https://github.com/loverajoel/jstips +[submodule "You-Dont-Need-jQuery"] + url = https://github.com/oneuijs/You-Dont-Need-jQuery diff --git a/spec/fixtures/git/repo-with-submodules/git.git/description b/spec/fixtures/git/repo-with-submodules/git.git/description new file mode 100644 index 000000000..498b267a8 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/spec/fixtures/git/repo-with-submodules/git.git/hooks/applypatch-msg.sample b/spec/fixtures/git/repo-with-submodules/git.git/hooks/applypatch-msg.sample new file mode 100755 index 000000000..a5d7b84a6 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/hooks/applypatch-msg.sample @@ -0,0 +1,15 @@ +#!/bin/sh +# +# An example hook script to check the commit log message taken by +# applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. The hook is +# allowed to edit the commit message file. +# +# To enable this hook, rename this file to "applypatch-msg". + +. git-sh-setup +commitmsg="$(git rev-parse --git-path hooks/commit-msg)" +test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"} +: diff --git a/spec/fixtures/git/repo-with-submodules/git.git/hooks/commit-msg.sample b/spec/fixtures/git/repo-with-submodules/git.git/hooks/commit-msg.sample new file mode 100755 index 000000000..b58d1184a --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/hooks/commit-msg.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to check the commit log message. +# Called by "git commit" with one argument, the name of the file +# that has the commit message. The hook should exit with non-zero +# status after issuing an appropriate message if it wants to stop the +# commit. The hook is allowed to edit the commit message file. +# +# To enable this hook, rename this file to "commit-msg". + +# Uncomment the below to add a Signed-off-by line to the message. +# Doing this in a hook is a bad idea in general, but the prepare-commit-msg +# hook is more suited to it. +# +# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" + +# This example catches duplicate Signed-off-by lines. + +test "" = "$(grep '^Signed-off-by: ' "$1" | + sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { + echo >&2 Duplicate Signed-off-by lines. + exit 1 +} diff --git a/spec/fixtures/git/repo-with-submodules/git.git/hooks/post-update.sample b/spec/fixtures/git/repo-with-submodules/git.git/hooks/post-update.sample new file mode 100755 index 000000000..ec17ec193 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/hooks/post-update.sample @@ -0,0 +1,8 @@ +#!/bin/sh +# +# An example hook script to prepare a packed repository for use over +# dumb transports. +# +# To enable this hook, rename this file to "post-update". + +exec git update-server-info diff --git a/spec/fixtures/git/repo-with-submodules/git.git/hooks/pre-applypatch.sample b/spec/fixtures/git/repo-with-submodules/git.git/hooks/pre-applypatch.sample new file mode 100755 index 000000000..4142082bc --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/hooks/pre-applypatch.sample @@ -0,0 +1,14 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed +# by applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-applypatch". + +. git-sh-setup +precommit="$(git rev-parse --git-path hooks/pre-commit)" +test -x "$precommit" && exec "$precommit" ${1+"$@"} +: diff --git a/spec/fixtures/git/repo-with-submodules/git.git/hooks/pre-commit.sample b/spec/fixtures/git/repo-with-submodules/git.git/hooks/pre-commit.sample new file mode 100755 index 000000000..68d62d544 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/hooks/pre-commit.sample @@ -0,0 +1,49 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git commit" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message if +# it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-commit". + +if git rev-parse --verify HEAD >/dev/null 2>&1 +then + against=HEAD +else + # Initial commit: diff against an empty tree object + against=4b825dc642cb6eb9a060e54bf8d69288fbee4904 +fi + +# If you want to allow non-ASCII filenames set this variable to true. +allownonascii=$(git config --bool hooks.allownonascii) + +# Redirect output to stderr. +exec 1>&2 + +# Cross platform projects tend to avoid non-ASCII filenames; prevent +# them from being added to the repository. We exploit the fact that the +# printable range starts at the space character and ends with tilde. +if [ "$allownonascii" != "true" ] && + # Note that the use of brackets around a tr range is ok here, (it's + # even required, for portability to Solaris 10's /usr/bin/tr), since + # the square bracket bytes happen to fall in the designated range. + test $(git diff --cached --name-only --diff-filter=A -z $against | + LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 +then + cat <<\EOF +Error: Attempt to add a non-ASCII file name. + +This can cause problems if you want to work with people on other platforms. + +To be portable it is advisable to rename the file. + +If you know what you are doing you can disable this check using: + + git config hooks.allownonascii true +EOF + exit 1 +fi + +# If there are whitespace errors, print the offending file names and fail. +exec git diff-index --check --cached $against -- diff --git a/spec/fixtures/git/repo-with-submodules/git.git/hooks/pre-push.sample b/spec/fixtures/git/repo-with-submodules/git.git/hooks/pre-push.sample new file mode 100755 index 000000000..6187dbf43 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/hooks/pre-push.sample @@ -0,0 +1,53 @@ +#!/bin/sh + +# An example hook script to verify what is about to be pushed. Called by "git +# push" after it has checked the remote status, but before anything has been +# pushed. If this script exits with a non-zero status nothing will be pushed. +# +# This hook is called with the following parameters: +# +# $1 -- Name of the remote to which the push is being done +# $2 -- URL to which the push is being done +# +# If pushing without using a named remote those arguments will be equal. +# +# Information about the commits which are being pushed is supplied as lines to +# the standard input in the form: +# +# +# +# This sample shows how to prevent push of commits where the log message starts +# with "WIP" (work in progress). + +remote="$1" +url="$2" + +z40=0000000000000000000000000000000000000000 + +while read local_ref local_sha remote_ref remote_sha +do + if [ "$local_sha" = $z40 ] + then + # Handle delete + : + else + if [ "$remote_sha" = $z40 ] + then + # New branch, examine all commits + range="$local_sha" + else + # Update to existing branch, examine new commits + range="$remote_sha..$local_sha" + fi + + # Check for WIP commit + commit=`git rev-list -n 1 --grep '^WIP' "$range"` + if [ -n "$commit" ] + then + echo >&2 "Found WIP commit in $local_ref, not pushing" + exit 1 + fi + fi +done + +exit 0 diff --git a/spec/fixtures/git/repo-with-submodules/git.git/hooks/pre-rebase.sample b/spec/fixtures/git/repo-with-submodules/git.git/hooks/pre-rebase.sample new file mode 100755 index 000000000..9773ed4cb --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/hooks/pre-rebase.sample @@ -0,0 +1,169 @@ +#!/bin/sh +# +# Copyright (c) 2006, 2008 Junio C Hamano +# +# The "pre-rebase" hook is run just before "git rebase" starts doing +# its job, and can prevent the command from running by exiting with +# non-zero status. +# +# The hook is called with the following parameters: +# +# $1 -- the upstream the series was forked from. +# $2 -- the branch being rebased (or empty when rebasing the current branch). +# +# This sample shows how to prevent topic branches that are already +# merged to 'next' branch from getting rebased, because allowing it +# would result in rebasing already published history. + +publish=next +basebranch="$1" +if test "$#" = 2 +then + topic="refs/heads/$2" +else + topic=`git symbolic-ref HEAD` || + exit 0 ;# we do not interrupt rebasing detached HEAD +fi + +case "$topic" in +refs/heads/??/*) + ;; +*) + exit 0 ;# we do not interrupt others. + ;; +esac + +# Now we are dealing with a topic branch being rebased +# on top of master. Is it OK to rebase it? + +# Does the topic really exist? +git show-ref -q "$topic" || { + echo >&2 "No such branch $topic" + exit 1 +} + +# Is topic fully merged to master? +not_in_master=`git rev-list --pretty=oneline ^master "$topic"` +if test -z "$not_in_master" +then + echo >&2 "$topic is fully merged to master; better remove it." + exit 1 ;# we could allow it, but there is no point. +fi + +# Is topic ever merged to next? If so you should not be rebasing it. +only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` +only_next_2=`git rev-list ^master ${publish} | sort` +if test "$only_next_1" = "$only_next_2" +then + not_in_topic=`git rev-list "^$topic" master` + if test -z "$not_in_topic" + then + echo >&2 "$topic is already up-to-date with master" + exit 1 ;# we could allow it, but there is no point. + else + exit 0 + fi +else + not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` + /usr/bin/perl -e ' + my $topic = $ARGV[0]; + my $msg = "* $topic has commits already merged to public branch:\n"; + my (%not_in_next) = map { + /^([0-9a-f]+) /; + ($1 => 1); + } split(/\n/, $ARGV[1]); + for my $elem (map { + /^([0-9a-f]+) (.*)$/; + [$1 => $2]; + } split(/\n/, $ARGV[2])) { + if (!exists $not_in_next{$elem->[0]}) { + if ($msg) { + print STDERR $msg; + undef $msg; + } + print STDERR " $elem->[1]\n"; + } + } + ' "$topic" "$not_in_next" "$not_in_master" + exit 1 +fi + +exit 0 + +################################################################ + +This sample hook safeguards topic branches that have been +published from being rewound. + +The workflow assumed here is: + + * Once a topic branch forks from "master", "master" is never + merged into it again (either directly or indirectly). + + * Once a topic branch is fully cooked and merged into "master", + it is deleted. If you need to build on top of it to correct + earlier mistakes, a new topic branch is created by forking at + the tip of the "master". This is not strictly necessary, but + it makes it easier to keep your history simple. + + * Whenever you need to test or publish your changes to topic + branches, merge them into "next" branch. + +The script, being an example, hardcodes the publish branch name +to be "next", but it is trivial to make it configurable via +$GIT_DIR/config mechanism. + +With this workflow, you would want to know: + +(1) ... if a topic branch has ever been merged to "next". Young + topic branches can have stupid mistakes you would rather + clean up before publishing, and things that have not been + merged into other branches can be easily rebased without + affecting other people. But once it is published, you would + not want to rewind it. + +(2) ... if a topic branch has been fully merged to "master". + Then you can delete it. More importantly, you should not + build on top of it -- other people may already want to + change things related to the topic as patches against your + "master", so if you need further changes, it is better to + fork the topic (perhaps with the same name) afresh from the + tip of "master". + +Let's look at this example: + + o---o---o---o---o---o---o---o---o---o "next" + / / / / + / a---a---b A / / + / / / / + / / c---c---c---c B / + / / / \ / + / / / b---b C \ / + / / / / \ / + ---o---o---o---o---o---o---o---o---o---o---o "master" + + +A, B and C are topic branches. + + * A has one fix since it was merged up to "next". + + * B has finished. It has been fully merged up to "master" and "next", + and is ready to be deleted. + + * C has not merged to "next" at all. + +We would want to allow C to be rebased, refuse A, and encourage +B to be deleted. + +To compute (1): + + git rev-list ^master ^topic next + git rev-list ^master next + + if these match, topic has not merged in next at all. + +To compute (2): + + git rev-list master..topic + + if this is empty, it is fully merged to "master". diff --git a/spec/fixtures/git/repo-with-submodules/git.git/hooks/prepare-commit-msg.sample b/spec/fixtures/git/repo-with-submodules/git.git/hooks/prepare-commit-msg.sample new file mode 100755 index 000000000..f093a02ec --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/hooks/prepare-commit-msg.sample @@ -0,0 +1,36 @@ +#!/bin/sh +# +# An example hook script to prepare the commit log message. +# Called by "git commit" with the name of the file that has the +# commit message, followed by the description of the commit +# message's source. The hook's purpose is to edit the commit +# message file. If the hook fails with a non-zero status, +# the commit is aborted. +# +# To enable this hook, rename this file to "prepare-commit-msg". + +# This hook includes three examples. The first comments out the +# "Conflicts:" part of a merge commit. +# +# The second includes the output of "git diff --name-status -r" +# into the message, just before the "git status" output. It is +# commented because it doesn't cope with --amend or with squashed +# commits. +# +# The third example adds a Signed-off-by line to the message, that can +# still be edited. This is rarely a good idea. + +case "$2,$3" in + merge,) + /usr/bin/perl -i.bak -ne 's/^/# /, s/^# #/#/ if /^Conflicts/ .. /#/; print' "$1" ;; + +# ,|template,) +# /usr/bin/perl -i.bak -pe ' +# print "\n" . `git diff --cached --name-status -r` +# if /^#/ && $first++ == 0' "$1" ;; + + *) ;; +esac + +# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" diff --git a/spec/fixtures/git/repo-with-submodules/git.git/hooks/update.sample b/spec/fixtures/git/repo-with-submodules/git.git/hooks/update.sample new file mode 100755 index 000000000..d84758373 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/hooks/update.sample @@ -0,0 +1,128 @@ +#!/bin/sh +# +# An example hook script to blocks unannotated tags from entering. +# Called by "git receive-pack" with arguments: refname sha1-old sha1-new +# +# To enable this hook, rename this file to "update". +# +# Config +# ------ +# hooks.allowunannotated +# This boolean sets whether unannotated tags will be allowed into the +# repository. By default they won't be. +# hooks.allowdeletetag +# This boolean sets whether deleting tags will be allowed in the +# repository. By default they won't be. +# hooks.allowmodifytag +# This boolean sets whether a tag may be modified after creation. By default +# it won't be. +# hooks.allowdeletebranch +# This boolean sets whether deleting branches will be allowed in the +# repository. By default they won't be. +# hooks.denycreatebranch +# This boolean sets whether remotely creating branches will be denied +# in the repository. By default this is allowed. +# + +# --- Command line +refname="$1" +oldrev="$2" +newrev="$3" + +# --- Safety check +if [ -z "$GIT_DIR" ]; then + echo "Don't run this script from the command line." >&2 + echo " (if you want, you could supply GIT_DIR then run" >&2 + echo " $0 )" >&2 + exit 1 +fi + +if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then + echo "usage: $0 " >&2 + exit 1 +fi + +# --- Config +allowunannotated=$(git config --bool hooks.allowunannotated) +allowdeletebranch=$(git config --bool hooks.allowdeletebranch) +denycreatebranch=$(git config --bool hooks.denycreatebranch) +allowdeletetag=$(git config --bool hooks.allowdeletetag) +allowmodifytag=$(git config --bool hooks.allowmodifytag) + +# check for no description +projectdesc=$(sed -e '1q' "$GIT_DIR/description") +case "$projectdesc" in +"Unnamed repository"* | "") + echo "*** Project description file hasn't been set" >&2 + exit 1 + ;; +esac + +# --- Check types +# if $newrev is 0000...0000, it's a commit to delete a ref. +zero="0000000000000000000000000000000000000000" +if [ "$newrev" = "$zero" ]; then + newrev_type=delete +else + newrev_type=$(git cat-file -t $newrev) +fi + +case "$refname","$newrev_type" in + refs/tags/*,commit) + # un-annotated tag + short_refname=${refname##refs/tags/} + if [ "$allowunannotated" != "true" ]; then + echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2 + echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 + exit 1 + fi + ;; + refs/tags/*,delete) + # delete tag + if [ "$allowdeletetag" != "true" ]; then + echo "*** Deleting a tag is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/tags/*,tag) + # annotated tag + if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 + then + echo "*** Tag '$refname' already exists." >&2 + echo "*** Modifying a tag is not allowed in this repository." >&2 + exit 1 + fi + ;; + refs/heads/*,commit) + # branch + if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then + echo "*** Creating a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/heads/*,delete) + # delete branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/remotes/*,commit) + # tracking branch + ;; + refs/remotes/*,delete) + # delete tracking branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a tracking branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + *) + # Anything else (is there anything else?) + echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 + exit 1 + ;; +esac + +# --- Finished +exit 0 diff --git a/spec/fixtures/git/repo-with-submodules/git.git/index b/spec/fixtures/git/repo-with-submodules/git.git/index new file mode 100644 index 0000000000000000000000000000000000000000..20f2cdc44cc7953336ba167ea622ac57807d3a5d GIT binary patch literal 377 zcmZ?q402{*U|<4bmax8AH9(pHMl%A%*d;cuU|?um!oa}z6(}VF#K#@JJfEqyBGBZ@ zN!AN~1>x3dVxRXgaO5bm(J>}Q<6<)jPt&P???hu`{ zb0-&rNMwGgu1kJiiLPI2YKm@FU}ebFyOM&fAm8sf7SNR@4UHPMTSp4?G3rl Zbbj)_S+51vScSe`S>5#c_E~B3?*J0haH#+Q literal 0 HcmV?d00001 diff --git a/spec/fixtures/git/repo-with-submodules/git.git/info/exclude b/spec/fixtures/git/repo-with-submodules/git.git/info/exclude new file mode 100644 index 000000000..a5196d1be --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/info/exclude @@ -0,0 +1,6 @@ +# git ls-files --others --exclude-from=.git/info/exclude +# Lines that start with '#' are comments. +# For a project mostly in C, the following would be a good set of +# exclude patterns (uncomment them if you want to use them): +# *.[oa] +# *~ diff --git a/spec/fixtures/git/repo-with-submodules/git.git/logs/HEAD b/spec/fixtures/git/repo-with-submodules/git.git/logs/HEAD new file mode 100644 index 000000000..d41c1a106 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/logs/HEAD @@ -0,0 +1,2 @@ +0000000000000000000000000000000000000000 d3e073baf592c56614c68ead9e2cd0a3880140cd joshaber 1452185922 -0500 commit (initial): first +d3e073baf592c56614c68ead9e2cd0a3880140cd d2b0ad9cbc6f6c4372e8956e5cc5af771b2342e5 joshaber 1452186239 -0500 commit: submodules diff --git a/spec/fixtures/git/repo-with-submodules/git.git/logs/refs/heads/master b/spec/fixtures/git/repo-with-submodules/git.git/logs/refs/heads/master new file mode 100644 index 000000000..d41c1a106 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/logs/refs/heads/master @@ -0,0 +1,2 @@ +0000000000000000000000000000000000000000 d3e073baf592c56614c68ead9e2cd0a3880140cd joshaber 1452185922 -0500 commit (initial): first +d3e073baf592c56614c68ead9e2cd0a3880140cd d2b0ad9cbc6f6c4372e8956e5cc5af771b2342e5 joshaber 1452186239 -0500 commit: submodules diff --git a/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/HEAD b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/HEAD new file mode 100644 index 000000000..cb089cd89 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/HEAD @@ -0,0 +1 @@ +ref: refs/heads/master diff --git a/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/ORIG_HEAD b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/ORIG_HEAD new file mode 100644 index 000000000..b7cdcfd53 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/ORIG_HEAD @@ -0,0 +1 @@ +2e9bbc77d60f20eb462ead5b2ac7405b62b9b90a diff --git a/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/config b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/config new file mode 100644 index 000000000..6ce8d21dd --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/config @@ -0,0 +1,14 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = false + logallrefupdates = true + worktree = ../../../You-Dont-Need-jQuery + ignorecase = true + precomposeunicode = true +[remote "origin"] + url = https://github.com/oneuijs/You-Dont-Need-jQuery + fetch = +refs/heads/*:refs/remotes/origin/* +[branch "master"] + remote = origin + merge = refs/heads/master diff --git a/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/description b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/description new file mode 100644 index 000000000..498b267a8 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/gitdir b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/gitdir new file mode 100644 index 000000000..6b8710a71 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/gitdir @@ -0,0 +1 @@ +.git diff --git a/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/applypatch-msg.sample b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/applypatch-msg.sample new file mode 100755 index 000000000..a5d7b84a6 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/applypatch-msg.sample @@ -0,0 +1,15 @@ +#!/bin/sh +# +# An example hook script to check the commit log message taken by +# applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. The hook is +# allowed to edit the commit message file. +# +# To enable this hook, rename this file to "applypatch-msg". + +. git-sh-setup +commitmsg="$(git rev-parse --git-path hooks/commit-msg)" +test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"} +: diff --git a/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/commit-msg.sample b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/commit-msg.sample new file mode 100755 index 000000000..b58d1184a --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/commit-msg.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to check the commit log message. +# Called by "git commit" with one argument, the name of the file +# that has the commit message. The hook should exit with non-zero +# status after issuing an appropriate message if it wants to stop the +# commit. The hook is allowed to edit the commit message file. +# +# To enable this hook, rename this file to "commit-msg". + +# Uncomment the below to add a Signed-off-by line to the message. +# Doing this in a hook is a bad idea in general, but the prepare-commit-msg +# hook is more suited to it. +# +# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" + +# This example catches duplicate Signed-off-by lines. + +test "" = "$(grep '^Signed-off-by: ' "$1" | + sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { + echo >&2 Duplicate Signed-off-by lines. + exit 1 +} diff --git a/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/post-update.sample b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/post-update.sample new file mode 100755 index 000000000..ec17ec193 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/post-update.sample @@ -0,0 +1,8 @@ +#!/bin/sh +# +# An example hook script to prepare a packed repository for use over +# dumb transports. +# +# To enable this hook, rename this file to "post-update". + +exec git update-server-info diff --git a/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/pre-applypatch.sample b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/pre-applypatch.sample new file mode 100755 index 000000000..4142082bc --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/pre-applypatch.sample @@ -0,0 +1,14 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed +# by applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-applypatch". + +. git-sh-setup +precommit="$(git rev-parse --git-path hooks/pre-commit)" +test -x "$precommit" && exec "$precommit" ${1+"$@"} +: diff --git a/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/pre-commit.sample b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/pre-commit.sample new file mode 100755 index 000000000..68d62d544 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/pre-commit.sample @@ -0,0 +1,49 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git commit" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message if +# it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-commit". + +if git rev-parse --verify HEAD >/dev/null 2>&1 +then + against=HEAD +else + # Initial commit: diff against an empty tree object + against=4b825dc642cb6eb9a060e54bf8d69288fbee4904 +fi + +# If you want to allow non-ASCII filenames set this variable to true. +allownonascii=$(git config --bool hooks.allownonascii) + +# Redirect output to stderr. +exec 1>&2 + +# Cross platform projects tend to avoid non-ASCII filenames; prevent +# them from being added to the repository. We exploit the fact that the +# printable range starts at the space character and ends with tilde. +if [ "$allownonascii" != "true" ] && + # Note that the use of brackets around a tr range is ok here, (it's + # even required, for portability to Solaris 10's /usr/bin/tr), since + # the square bracket bytes happen to fall in the designated range. + test $(git diff --cached --name-only --diff-filter=A -z $against | + LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 +then + cat <<\EOF +Error: Attempt to add a non-ASCII file name. + +This can cause problems if you want to work with people on other platforms. + +To be portable it is advisable to rename the file. + +If you know what you are doing you can disable this check using: + + git config hooks.allownonascii true +EOF + exit 1 +fi + +# If there are whitespace errors, print the offending file names and fail. +exec git diff-index --check --cached $against -- diff --git a/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/pre-push.sample b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/pre-push.sample new file mode 100755 index 000000000..6187dbf43 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/pre-push.sample @@ -0,0 +1,53 @@ +#!/bin/sh + +# An example hook script to verify what is about to be pushed. Called by "git +# push" after it has checked the remote status, but before anything has been +# pushed. If this script exits with a non-zero status nothing will be pushed. +# +# This hook is called with the following parameters: +# +# $1 -- Name of the remote to which the push is being done +# $2 -- URL to which the push is being done +# +# If pushing without using a named remote those arguments will be equal. +# +# Information about the commits which are being pushed is supplied as lines to +# the standard input in the form: +# +# +# +# This sample shows how to prevent push of commits where the log message starts +# with "WIP" (work in progress). + +remote="$1" +url="$2" + +z40=0000000000000000000000000000000000000000 + +while read local_ref local_sha remote_ref remote_sha +do + if [ "$local_sha" = $z40 ] + then + # Handle delete + : + else + if [ "$remote_sha" = $z40 ] + then + # New branch, examine all commits + range="$local_sha" + else + # Update to existing branch, examine new commits + range="$remote_sha..$local_sha" + fi + + # Check for WIP commit + commit=`git rev-list -n 1 --grep '^WIP' "$range"` + if [ -n "$commit" ] + then + echo >&2 "Found WIP commit in $local_ref, not pushing" + exit 1 + fi + fi +done + +exit 0 diff --git a/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/pre-rebase.sample b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/pre-rebase.sample new file mode 100755 index 000000000..9773ed4cb --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/pre-rebase.sample @@ -0,0 +1,169 @@ +#!/bin/sh +# +# Copyright (c) 2006, 2008 Junio C Hamano +# +# The "pre-rebase" hook is run just before "git rebase" starts doing +# its job, and can prevent the command from running by exiting with +# non-zero status. +# +# The hook is called with the following parameters: +# +# $1 -- the upstream the series was forked from. +# $2 -- the branch being rebased (or empty when rebasing the current branch). +# +# This sample shows how to prevent topic branches that are already +# merged to 'next' branch from getting rebased, because allowing it +# would result in rebasing already published history. + +publish=next +basebranch="$1" +if test "$#" = 2 +then + topic="refs/heads/$2" +else + topic=`git symbolic-ref HEAD` || + exit 0 ;# we do not interrupt rebasing detached HEAD +fi + +case "$topic" in +refs/heads/??/*) + ;; +*) + exit 0 ;# we do not interrupt others. + ;; +esac + +# Now we are dealing with a topic branch being rebased +# on top of master. Is it OK to rebase it? + +# Does the topic really exist? +git show-ref -q "$topic" || { + echo >&2 "No such branch $topic" + exit 1 +} + +# Is topic fully merged to master? +not_in_master=`git rev-list --pretty=oneline ^master "$topic"` +if test -z "$not_in_master" +then + echo >&2 "$topic is fully merged to master; better remove it." + exit 1 ;# we could allow it, but there is no point. +fi + +# Is topic ever merged to next? If so you should not be rebasing it. +only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` +only_next_2=`git rev-list ^master ${publish} | sort` +if test "$only_next_1" = "$only_next_2" +then + not_in_topic=`git rev-list "^$topic" master` + if test -z "$not_in_topic" + then + echo >&2 "$topic is already up-to-date with master" + exit 1 ;# we could allow it, but there is no point. + else + exit 0 + fi +else + not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` + /usr/bin/perl -e ' + my $topic = $ARGV[0]; + my $msg = "* $topic has commits already merged to public branch:\n"; + my (%not_in_next) = map { + /^([0-9a-f]+) /; + ($1 => 1); + } split(/\n/, $ARGV[1]); + for my $elem (map { + /^([0-9a-f]+) (.*)$/; + [$1 => $2]; + } split(/\n/, $ARGV[2])) { + if (!exists $not_in_next{$elem->[0]}) { + if ($msg) { + print STDERR $msg; + undef $msg; + } + print STDERR " $elem->[1]\n"; + } + } + ' "$topic" "$not_in_next" "$not_in_master" + exit 1 +fi + +exit 0 + +################################################################ + +This sample hook safeguards topic branches that have been +published from being rewound. + +The workflow assumed here is: + + * Once a topic branch forks from "master", "master" is never + merged into it again (either directly or indirectly). + + * Once a topic branch is fully cooked and merged into "master", + it is deleted. If you need to build on top of it to correct + earlier mistakes, a new topic branch is created by forking at + the tip of the "master". This is not strictly necessary, but + it makes it easier to keep your history simple. + + * Whenever you need to test or publish your changes to topic + branches, merge them into "next" branch. + +The script, being an example, hardcodes the publish branch name +to be "next", but it is trivial to make it configurable via +$GIT_DIR/config mechanism. + +With this workflow, you would want to know: + +(1) ... if a topic branch has ever been merged to "next". Young + topic branches can have stupid mistakes you would rather + clean up before publishing, and things that have not been + merged into other branches can be easily rebased without + affecting other people. But once it is published, you would + not want to rewind it. + +(2) ... if a topic branch has been fully merged to "master". + Then you can delete it. More importantly, you should not + build on top of it -- other people may already want to + change things related to the topic as patches against your + "master", so if you need further changes, it is better to + fork the topic (perhaps with the same name) afresh from the + tip of "master". + +Let's look at this example: + + o---o---o---o---o---o---o---o---o---o "next" + / / / / + / a---a---b A / / + / / / / + / / c---c---c---c B / + / / / \ / + / / / b---b C \ / + / / / / \ / + ---o---o---o---o---o---o---o---o---o---o---o "master" + + +A, B and C are topic branches. + + * A has one fix since it was merged up to "next". + + * B has finished. It has been fully merged up to "master" and "next", + and is ready to be deleted. + + * C has not merged to "next" at all. + +We would want to allow C to be rebased, refuse A, and encourage +B to be deleted. + +To compute (1): + + git rev-list ^master ^topic next + git rev-list ^master next + + if these match, topic has not merged in next at all. + +To compute (2): + + git rev-list master..topic + + if this is empty, it is fully merged to "master". diff --git a/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/prepare-commit-msg.sample b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/prepare-commit-msg.sample new file mode 100755 index 000000000..f093a02ec --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/prepare-commit-msg.sample @@ -0,0 +1,36 @@ +#!/bin/sh +# +# An example hook script to prepare the commit log message. +# Called by "git commit" with the name of the file that has the +# commit message, followed by the description of the commit +# message's source. The hook's purpose is to edit the commit +# message file. If the hook fails with a non-zero status, +# the commit is aborted. +# +# To enable this hook, rename this file to "prepare-commit-msg". + +# This hook includes three examples. The first comments out the +# "Conflicts:" part of a merge commit. +# +# The second includes the output of "git diff --name-status -r" +# into the message, just before the "git status" output. It is +# commented because it doesn't cope with --amend or with squashed +# commits. +# +# The third example adds a Signed-off-by line to the message, that can +# still be edited. This is rarely a good idea. + +case "$2,$3" in + merge,) + /usr/bin/perl -i.bak -ne 's/^/# /, s/^# #/#/ if /^Conflicts/ .. /#/; print' "$1" ;; + +# ,|template,) +# /usr/bin/perl -i.bak -pe ' +# print "\n" . `git diff --cached --name-status -r` +# if /^#/ && $first++ == 0' "$1" ;; + + *) ;; +esac + +# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" diff --git a/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/update.sample b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/update.sample new file mode 100755 index 000000000..d84758373 --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/update.sample @@ -0,0 +1,128 @@ +#!/bin/sh +# +# An example hook script to blocks unannotated tags from entering. +# Called by "git receive-pack" with arguments: refname sha1-old sha1-new +# +# To enable this hook, rename this file to "update". +# +# Config +# ------ +# hooks.allowunannotated +# This boolean sets whether unannotated tags will be allowed into the +# repository. By default they won't be. +# hooks.allowdeletetag +# This boolean sets whether deleting tags will be allowed in the +# repository. By default they won't be. +# hooks.allowmodifytag +# This boolean sets whether a tag may be modified after creation. By default +# it won't be. +# hooks.allowdeletebranch +# This boolean sets whether deleting branches will be allowed in the +# repository. By default they won't be. +# hooks.denycreatebranch +# This boolean sets whether remotely creating branches will be denied +# in the repository. By default this is allowed. +# + +# --- Command line +refname="$1" +oldrev="$2" +newrev="$3" + +# --- Safety check +if [ -z "$GIT_DIR" ]; then + echo "Don't run this script from the command line." >&2 + echo " (if you want, you could supply GIT_DIR then run" >&2 + echo " $0 )" >&2 + exit 1 +fi + +if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then + echo "usage: $0 " >&2 + exit 1 +fi + +# --- Config +allowunannotated=$(git config --bool hooks.allowunannotated) +allowdeletebranch=$(git config --bool hooks.allowdeletebranch) +denycreatebranch=$(git config --bool hooks.denycreatebranch) +allowdeletetag=$(git config --bool hooks.allowdeletetag) +allowmodifytag=$(git config --bool hooks.allowmodifytag) + +# check for no description +projectdesc=$(sed -e '1q' "$GIT_DIR/description") +case "$projectdesc" in +"Unnamed repository"* | "") + echo "*** Project description file hasn't been set" >&2 + exit 1 + ;; +esac + +# --- Check types +# if $newrev is 0000...0000, it's a commit to delete a ref. +zero="0000000000000000000000000000000000000000" +if [ "$newrev" = "$zero" ]; then + newrev_type=delete +else + newrev_type=$(git cat-file -t $newrev) +fi + +case "$refname","$newrev_type" in + refs/tags/*,commit) + # un-annotated tag + short_refname=${refname##refs/tags/} + if [ "$allowunannotated" != "true" ]; then + echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2 + echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 + exit 1 + fi + ;; + refs/tags/*,delete) + # delete tag + if [ "$allowdeletetag" != "true" ]; then + echo "*** Deleting a tag is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/tags/*,tag) + # annotated tag + if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 + then + echo "*** Tag '$refname' already exists." >&2 + echo "*** Modifying a tag is not allowed in this repository." >&2 + exit 1 + fi + ;; + refs/heads/*,commit) + # branch + if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then + echo "*** Creating a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/heads/*,delete) + # delete branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/remotes/*,commit) + # tracking branch + ;; + refs/remotes/*,delete) + # delete tracking branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a tracking branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + *) + # Anything else (is there anything else?) + echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 + exit 1 + ;; +esac + +# --- Finished +exit 0 diff --git a/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/index b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/index new file mode 100644 index 0000000000000000000000000000000000000000..202c1f9ceb11e5e0d6143722d77602bbb12dd012 GIT binary patch literal 1919 zcmZ?q402{*U|<4b@vy#GH9(pHMl&)nu&_&P>}6nRT*AP>_!TH60>rA1+_iqB@0ulQ z%(ym3<(_T)&qdSEFmUK4B_^fj6eT0{qnXo(Y7V27+ppzEzdsPx6m;%ziaI~NlxwQS zHU>_;)Z(1Xyb_>6F#RwZ>Yjd7a~QOPr!}4ayY;<$z3q&JZe?3l&j%d4&A_FXo>`Ka zo|j*g3i1;Oz|4WsQ1d3BnKvu*yjbM|tI3QTUeC0Z`MopCWzV6j4BUDpMTupZ#d?*w zIS}K3<}G>xF&9EZ%sY1u)jSsC`-Y{Hy15_Uk=wJlB}psmN6W&1Vg_~}PiI%ZU{~Dk znuKbOMRBU4ifVL)z?6Awa&n~EYuS39Jo0AX337FG@paWrE!N9TfjA82KNt=5=VVm# z%nF51m2Nx!BH~3_`09OM_=S%Dx_Rmv+`P;bLgr0DHP1pp>&GOfwQuUIC-w{G{y6AZ z$GG^%L48Moa`obP7!!_6zpBxK%9RP*e9hqwlcJ`LQy zvS_tk@H1oErT>DWOu;c9EACfMr$NCB%o?fT69By7!hOV<8UVqL-HIH-2Pt$b{VVS)F z{hDQ>y$uy>zTdj?oPjqxu_!lDFF8LiO)sk$oY7$Zgwas{&Ox8-oDYyyW6y zz2bt@WU!ZE=D=vEd5h4@Q;hzjQ8%YeS3mZM?d;Dt7Ek$o=lMsdc`5n11kGEFY95Ev zx1EO>-rh@ow6%N+>(RYyUM)CleV;)H?7qU%)S^m+iFm^iScjq1W3t!!ifkfV_dnU& zs?F`#oy;k6tLPJh7}(s>lFS@n^#LqY;l5{J2nlip71!nrM#c(;T&wjK?Mt=eNxOY3 zH1TPxSk8+xre+|$45kVOT#lMsU! 1452186236 -0500 clone: from https://github.com/oneuijs/You-Dont-Need-jQuery diff --git a/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/logs/refs/heads/master b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/logs/refs/heads/master new file mode 100644 index 000000000..a76a6d29f --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/logs/refs/heads/master @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 2e9bbc77d60f20eb462ead5b2ac7405b62b9b90a joshaber 1452186236 -0500 clone: from https://github.com/oneuijs/You-Dont-Need-jQuery diff --git a/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/logs/refs/remotes/origin/HEAD b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/logs/refs/remotes/origin/HEAD new file mode 100644 index 000000000..a76a6d29f --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/logs/refs/remotes/origin/HEAD @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 2e9bbc77d60f20eb462ead5b2ac7405b62b9b90a joshaber 1452186236 -0500 clone: from https://github.com/oneuijs/You-Dont-Need-jQuery diff --git a/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/objects/pack/pack-d38b3bc339acd655e8dae9c0dcea8bb2ec174d16.idx b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/objects/pack/pack-d38b3bc339acd655e8dae9c0dcea8bb2ec174d16.idx new file mode 100644 index 0000000000000000000000000000000000000000..cb4b0b44e5f8cf06c01d8ac8368ec2008a8eb09a GIT binary patch literal 18152 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 literal 0 HcmV?d00001 diff --git a/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/objects/pack/pack-d38b3bc339acd655e8dae9c0dcea8bb2ec174d16.pack b/spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/objects/pack/pack-d38b3bc339acd655e8dae9c0dcea8bb2ec174d16.pack new file mode 100644 index 0000000000000000000000000000000000000000..89a46ce6f10d13fb52dd189c2c05d6e9bd192155 GIT binary patch literal 202626 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