From 84e6c04af2b718ca317c8c2b6e6886875930223e Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Tue, 10 Jul 2012 15:04:02 -0700 Subject: [PATCH 01/97] Add atom-debug-test scheme which automatically runs tests when atom starts --- Atom.xcodeproj/project.pbxproj | 2 + .../xcschemes/atom-debug-test.xcscheme | 92 +++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 Atom.xcodeproj/xcshareddata/xcschemes/atom-debug-test.xcscheme diff --git a/Atom.xcodeproj/project.pbxproj b/Atom.xcodeproj/project.pbxproj index 908ad9349..bac99d777 100644 --- a/Atom.xcodeproj/project.pbxproj +++ b/Atom.xcodeproj/project.pbxproj @@ -1142,6 +1142,7 @@ LIBRARY_SEARCH_PATHS = ( "$(SDKROOT)/System/Library/Frameworks", "\"$(SRCROOT)/frameworks\"", + "\"$(SDKROOT)/usr/lib/system\"", ); OTHER_CFLAGS = ( "-fno-strict-aliasing", @@ -1188,6 +1189,7 @@ LIBRARY_SEARCH_PATHS = ( "$(SDKROOT)/System/Library/Frameworks", "\"$(SRCROOT)/frameworks\"", + "\"$(SDKROOT)/usr/lib/system\"", ); OTHER_CFLAGS = "-fno-strict-aliasing"; OTHER_LDFLAGS = ( diff --git a/Atom.xcodeproj/xcshareddata/xcschemes/atom-debug-test.xcscheme b/Atom.xcodeproj/xcshareddata/xcschemes/atom-debug-test.xcscheme new file mode 100644 index 000000000..0e7d1aa86 --- /dev/null +++ b/Atom.xcodeproj/xcshareddata/xcschemes/atom-debug-test.xcscheme @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 5b2781aec278ee4229e342cab995669f82f4cf47 Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Tue, 10 Jul 2012 15:05:29 -0700 Subject: [PATCH 02/97] waitsForPromise can wait for calls to fail --- spec/spec-helper.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 5ff852fcc..b6d232993 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -90,7 +90,8 @@ window.mousemoveEvent = (properties={}) -> window.waitsForPromise = (fn) -> window.waitsFor (moveOn) -> - fn().done(moveOn) + promise = fn() + promise.then promise.done(moveOn), promise.fail(moveOn) window.resetTimeouts = -> window.now = 0 From 2afec5cf5316b80415e6d952d53c0f29851eba1d Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Tue, 10 Jul 2012 15:07:28 -0700 Subject: [PATCH 03/97] Add ChildProcess.exec(cmd, [options]) Uses promises for failure and success states. Takes optional stderr and stdout callbacks for incremental reading. --- Atom/src/native_handler.mm | 84 ++++++++++++++++++++++++++- spec/stdlib/child-process-spec.coffee | 47 +++++++++++++++ src/stdlib/child-process.coffee | 54 ++++------------- 3 files changed, 142 insertions(+), 43 deletions(-) create mode 100644 spec/stdlib/child-process-spec.coffee diff --git a/Atom/src/native_handler.mm b/Atom/src/native_handler.mm index 68e8a687e..1460cbb7e 100644 --- a/Atom/src/native_handler.mm +++ b/Atom/src/native_handler.mm @@ -4,6 +4,7 @@ #import "AtomController.h" #import "client_handler.h" #import "PathWatcher.h" +#import #import #import @@ -18,7 +19,7 @@ NSString *stringFromCefV8Value(const CefRefPtr& value) { NativeHandler::NativeHandler() : CefV8Handler() { std::string extensionCode = "var $native = {}; (function() {"; - const char *functionNames[] = {"exists", "alert", "read", "write", "absolute", "list", "isFile", "isDirectory", "remove", "asyncList", "open", "openDialog", "quit", "writeToPasteboard", "readFromPasteboard", "showDevTools", "toggleDevTools", "newWindow", "saveDialog", "exit", "watchPath", "unwatchPath", "makeDirectory", "move", "moveToTrash", "reload", "lastModified", "md5ForPath"}; + const char *functionNames[] = {"exists", "alert", "read", "write", "absolute", "list", "isFile", "isDirectory", "remove", "asyncList", "open", "openDialog", "quit", "writeToPasteboard", "readFromPasteboard", "showDevTools", "toggleDevTools", "newWindow", "saveDialog", "exit", "watchPath", "unwatchPath", "makeDirectory", "move", "moveToTrash", "reload", "lastModified", "md5ForPath", "exec"}; NSUInteger arrayLength = sizeof(functionNames) / sizeof(const char *); for (NSUInteger i = 0; i < arrayLength; i++) { std::string functionName = std::string(functionNames[i]); @@ -423,5 +424,86 @@ bool NativeHandler::Execute(const CefString& name, retval = CefV8Value::CreateString([hash UTF8String]); return true; } + else if (name == "exec") { + NSString *command = stringFromCefV8Value(arguments[0]); + CefRefPtr options = arguments[1]; + CefRefPtr callback = arguments[2]; + + NSTask *task = [[NSTask alloc] init]; + [task setLaunchPath:@"/bin/sh"]; + [task setStandardInput:[NSFileHandle fileHandleWithNullDevice]]; + [task setArguments:[NSArray arrayWithObjects:@"-l", @"-c", command, nil]]; + + NSPipe *stdout = [NSPipe pipe]; + NSPipe *stderr = [NSPipe pipe]; + [task setStandardOutput:stdout]; + [task setStandardError:stderr]; + + CefRefPtr context = CefV8Context::GetCurrentContext(); + void (^outputHandle)(NSFileHandle *fileHandle, CefRefPtr function) = nil; + void (^taskTerminatedHandle)() = nil; + + outputHandle = ^(NSFileHandle *fileHandle, CefRefPtr function) { + context->Enter(); + + NSData *data = [fileHandle availableData]; + NSString *contents = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + + CefV8ValueList args; + CefRefPtr retval = CefV8Value::CreateBool(YES); + CefRefPtr e; + args.push_back(CefV8Value::CreateString([contents UTF8String])); + + function->ExecuteFunction(function, args, retval, e, true); + [contents release]; + context->Exit(); + }; + + taskTerminatedHandle = ^() { + context->Enter(); + NSString *output = [[NSString alloc] initWithData:[[stdout fileHandleForReading] readDataToEndOfFile] encoding:NSUTF8StringEncoding]; + NSString *errorOutput = [[NSString alloc] initWithData:[[task.standardError fileHandleForReading] readDataToEndOfFile] encoding:NSUTF8StringEncoding]; + + CefV8ValueList args; + CefRefPtr retval = CefV8Value::CreateBool(YES); + CefRefPtr e; + + args.push_back(CefV8Value::CreateInt([task terminationStatus])); + args.push_back(CefV8Value::CreateString([output UTF8String])); + args.push_back(CefV8Value::CreateString([errorOutput UTF8String])); + + callback->ExecuteFunction(callback, args, retval, e, true); + context->Exit(); + + stdout.fileHandleForReading.writeabilityHandler = nil; + stderr.fileHandleForReading.writeabilityHandler = nil; + }; + + task.terminationHandler = ^(NSTask *) { + dispatch_sync(dispatch_get_main_queue(), taskTerminatedHandle); + }; + + CefRefPtr stdoutFunction = options->GetValue("stdout"); + if (stdoutFunction->IsFunction()) { + stdout.fileHandleForReading.writeabilityHandler = ^(NSFileHandle *fileHandle) { + dispatch_sync(dispatch_get_main_queue(), ^() { + outputHandle(fileHandle, stdoutFunction); + }); + }; + } + + CefRefPtr stderrFunction = options->GetValue("stderr"); + if (stderrFunction->IsFunction()) { + stderr.fileHandleForReading.writeabilityHandler = ^(NSFileHandle *fileHandle) { + dispatch_sync(dispatch_get_main_queue(), ^() { + outputHandle(fileHandle, stderrFunction); + }); + }; + } + + [task launch]; + + return true; + } return false; }; \ No newline at end of file diff --git a/spec/stdlib/child-process-spec.coffee b/spec/stdlib/child-process-spec.coffee new file mode 100644 index 000000000..74c106e06 --- /dev/null +++ b/spec/stdlib/child-process-spec.coffee @@ -0,0 +1,47 @@ +ChildProcess = require 'child-process' + +fdescribe 'Child Processes', -> + describe ".exec(command, options)", -> + it "returns a promise that resolves to stdout and stderr", -> + waitsForPromise -> + cmd = "echo 'good' && echo 'bad' >&2" + ChildProcess.exec(cmd).done (stdout, stderr) -> + expect(stdout).toBe 'good\n' + expect(stderr).toBe 'bad\n' + + describe "when `options` contains a stdout function", -> + it "calls the stdout function when new data is received", -> + stderrHandler = jasmine.createSpy "stderrHandler" + + cmd = "echo '1111' >&2 && sleep .1 && echo '2222' >&2" + ChildProcess.exec(cmd, stderr: stderrHandler) + + waitsFor -> + stderrHandler.callCount > 1 + + runs -> + expect(stderrHandler.argsForCall[0][0]).toBe "1111\n" + expect(stderrHandler.argsForCall[1][0]).toBe "2222\n" + + it "calls the stderr function when new data is received", -> + stdoutHandler = jasmine.createSpy "stdoutHandler" + + cmd = "echo 'first' && sleep .1 && echo 'second' && sleep .1 && echo 'third'" + ChildProcess.exec(cmd, stdout: stdoutHandler) + + waitsFor -> + stdoutHandler.callCount > 2 + + runs -> + expect(stdoutHandler.argsForCall[0][0]).toBe "first\n" + expect(stdoutHandler.argsForCall[1][0]).toBe "second\n" + expect(stdoutHandler.argsForCall[2][0]).toBe "third\n" + + describe "when the command fails", -> + it "executes the callback with error set to the exit status", -> + waitsForPromise -> + cmd = "exit 2" + ChildProcess.exec(cmd).fail (error) -> + expect(error.exitStatus).toBe 2 + + diff --git a/src/stdlib/child-process.coffee b/src/stdlib/child-process.coffee index a724439e0..18977c41e 100644 --- a/src/stdlib/child-process.coffee +++ b/src/stdlib/child-process.coffee @@ -1,50 +1,20 @@ # node.js child-process # http://nodejs.org/docs/v0.6.3/api/child_processes.html +$ = require 'jquery' _ = require 'underscore' module.exports = - exec: (command, options, callback) -> - callback = options if _.isFunction options +class ChildProccess + @exec: (command, options={}) -> + deferred = $.Deferred() + $native.exec command, options, (exitStatus, stdout, stdin) -> + if error != 0 + error = new Error("Exec failed (#{exitStatus}) command '#{command}'") + error.exitStatus = exitStatus + deferred.reject(error) + else + deferred.resolve(stdout, stdin) - # make a task - task = OSX.NSTask.alloc.init + deferred - # try to use their login shell - task.setLaunchPath "/bin/bash" - - # set stdin to /dev/null - task.setStandardInput OSX.NSFileHandle.fileHandleWithNullDevice - - # -l = login shell, -c = command - args = ["-l", "-c", command] - task.setArguments args - - # setup stdout and stderr - task.setStandardOutput stdout = OSX.NSPipe.pipe - task.setStandardError stderr = OSX.NSPipe.pipe - stdoutHandle = stdout.fileHandleForReading - stderrHandle = stderr.fileHandleForReading - - # begin - task.launch - - # read pipes - err = @readHandle stderrHandle - out = @readHandle stdoutHandle - - # check for a dirty exit - if not task.isRunning - code = task.terminationStatus - if code > 0 - error = new Error - error.code = code - - # call callback - callback error, out, err - - readHandle: (handle) -> - OSX.NSString. - alloc. - initWithData_encoding(handle.readDataToEndOfFile, OSX.NSUTF8StringEncoding). - toString() From 8f5746c8feb5fe0d1af7e57207a9cc54a02ef85c Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 11 Jul 2012 16:30:50 -0600 Subject: [PATCH 04/97] Add bufferLines option to ChildProcess.exec It ensures that stdout and stderr callbacks are triggered with whole lines --- spec/stdlib/child-process-spec.coffee | 86 ++++++++++++++++++++------- src/stdlib/child-process.coffee | 13 ++++ 2 files changed, 79 insertions(+), 20 deletions(-) diff --git a/spec/stdlib/child-process-spec.coffee b/spec/stdlib/child-process-spec.coffee index 74c106e06..6c8b7e7d8 100644 --- a/spec/stdlib/child-process-spec.coffee +++ b/spec/stdlib/child-process-spec.coffee @@ -1,7 +1,13 @@ ChildProcess = require 'child-process' -fdescribe 'Child Processes', -> +describe 'Child Processes', -> describe ".exec(command, options)", -> + [stderrHandler, stdoutHandler] = [] + + beforeEach -> + stderrHandler = jasmine.createSpy "stderrHandler" + stdoutHandler = jasmine.createSpy "stdoutHandler" + it "returns a promise that resolves to stdout and stderr", -> waitsForPromise -> cmd = "echo 'good' && echo 'bad' >&2" @@ -9,23 +15,8 @@ fdescribe 'Child Processes', -> expect(stdout).toBe 'good\n' expect(stderr).toBe 'bad\n' - describe "when `options` contains a stdout function", -> - it "calls the stdout function when new data is received", -> - stderrHandler = jasmine.createSpy "stderrHandler" - - cmd = "echo '1111' >&2 && sleep .1 && echo '2222' >&2" - ChildProcess.exec(cmd, stderr: stderrHandler) - - waitsFor -> - stderrHandler.callCount > 1 - - runs -> - expect(stderrHandler.argsForCall[0][0]).toBe "1111\n" - expect(stderrHandler.argsForCall[1][0]).toBe "2222\n" - - it "calls the stderr function when new data is received", -> - stdoutHandler = jasmine.createSpy "stdoutHandler" - + describe "when `options` contains stdout/stderror callbacks", -> + it "calls the stdout callback when new data is received on stdout", -> cmd = "echo 'first' && sleep .1 && echo 'second' && sleep .1 && echo 'third'" ChildProcess.exec(cmd, stdout: stdoutHandler) @@ -37,11 +28,66 @@ fdescribe 'Child Processes', -> expect(stdoutHandler.argsForCall[1][0]).toBe "second\n" expect(stdoutHandler.argsForCall[2][0]).toBe "third\n" + it "calls the stderr callback when new data is received on stderr", -> + cmd = "echo '1111' >&2 && sleep .1 && echo '2222' >&2" + ChildProcess.exec(cmd, stderr: stderrHandler) + + waitsFor -> + stderrHandler.callCount > 1 + + runs -> + expect(stderrHandler.argsForCall[0][0]).toBe "1111\n" + expect(stderrHandler.argsForCall[1][0]).toBe "2222\n" + + describe "when the `bufferLines` option is true ", -> + [simulateStdout, simulateStderr] = [] + + beforeEach -> + spyOn($native, 'exec') + ChildProcess.exec("print_the_things", bufferLines: true, stdout: stdoutHandler, stderr: stderrHandler) + { stdout, stderr } = $native.exec.argsForCall[0][1] + simulateStdout = stdout + simulateStderr = stderr + + it "only triggers stdout callbacks with complete lines", -> + simulateStdout """ + I am a full line + I am part of """ + + expect(stdoutHandler).toHaveBeenCalledWith("I am a full line\n") + stdoutHandler.reset() + + simulateStdout """ + a line + I am another full line\n + """ + + expect(stdoutHandler).toHaveBeenCalledWith """ + I am part of a line + I am another full line\n + """ + + it "only triggers stderr callbacks with complete lines", -> + simulateStderr """ + I am a full line + I am part of """ + + expect(stderrHandler).toHaveBeenCalledWith("I am a full line\n") + stdoutHandler.reset() + + simulateStderr """ + a line + I am another full line\n + """ + + expect(stderrHandler).toHaveBeenCalledWith """ + I am part of a line + I am another full line\n + """ + describe "when the command fails", -> it "executes the callback with error set to the exit status", -> waitsForPromise -> cmd = "exit 2" ChildProcess.exec(cmd).fail (error) -> expect(error.exitStatus).toBe 2 - - diff --git a/src/stdlib/child-process.coffee b/src/stdlib/child-process.coffee index 18977c41e..164fd1c13 100644 --- a/src/stdlib/child-process.coffee +++ b/src/stdlib/child-process.coffee @@ -8,6 +8,11 @@ module.exports = class ChildProccess @exec: (command, options={}) -> deferred = $.Deferred() + + if options.bufferLines + options.stdout = @bufferLines(options.stdout) if options.stdout + options.stderr = @bufferLines(options.stderr) if options.stderr + $native.exec command, options, (exitStatus, stdout, stdin) -> if error != 0 error = new Error("Exec failed (#{exitStatus}) command '#{command}'") @@ -18,3 +23,11 @@ class ChildProccess deferred + @bufferLines: (callback) -> + buffered = "" + (data) -> + buffered += data + lastNewlineIndex = buffered.lastIndexOf('\n') + if lastNewlineIndex >= 0 + callback(buffered.substring(0, lastNewlineIndex + 1)) + buffered = buffered.substring(lastNewlineIndex + 1) From 92f4519db29ab166776ae8eb3c90c2402c8cdc7e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 11 Jul 2012 18:45:57 -0600 Subject: [PATCH 05/97] Allow null characters in child process output This is needed because grep uses a null character as a delimiter after the file path. --- Atom/src/native_handler.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Atom/src/native_handler.mm b/Atom/src/native_handler.mm index 1460cbb7e..3c07fd35d 100644 --- a/Atom/src/native_handler.mm +++ b/Atom/src/native_handler.mm @@ -452,7 +452,7 @@ bool NativeHandler::Execute(const CefString& name, CefV8ValueList args; CefRefPtr retval = CefV8Value::CreateBool(YES); CefRefPtr e; - args.push_back(CefV8Value::CreateString([contents UTF8String])); + args.push_back(CefV8Value::CreateString(std::string([contents UTF8String], [contents length]))); function->ExecuteFunction(function, args, retval, e, true); [contents release]; From db6692b2f69de37413803dea949c8e4678997ee7 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 11 Jul 2012 18:48:49 -0600 Subject: [PATCH 06/97] Fix the $native.exec callback in ChildProcess.exec It's stderr, not stdin. Also we have to look at exitStatus, not error to see if the process failed. None of this is tested, which is bad. --- src/stdlib/child-process.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/stdlib/child-process.coffee b/src/stdlib/child-process.coffee index 164fd1c13..e8885a7a2 100644 --- a/src/stdlib/child-process.coffee +++ b/src/stdlib/child-process.coffee @@ -13,13 +13,13 @@ class ChildProccess options.stdout = @bufferLines(options.stdout) if options.stdout options.stderr = @bufferLines(options.stderr) if options.stderr - $native.exec command, options, (exitStatus, stdout, stdin) -> - if error != 0 + $native.exec command, options, (exitStatus, stdout, stderr) -> + if exitStatus != 0 error = new Error("Exec failed (#{exitStatus}) command '#{command}'") error.exitStatus = exitStatus deferred.reject(error) else - deferred.resolve(stdout, stdin) + deferred.resolve(stdout, stderr) deferred From 3854f73c8404f856b8b4d13b196a9d8903463d5f Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 11 Jul 2012 18:50:38 -0600 Subject: [PATCH 07/97] WIP: Start on Project.prototype.scan Right now it calls out to grep and parses the output, but doesn't do anything with it just yet. --- spec/app/project-spec.coffee | 12 ++++++++++++ spec/fixtures/dir/a | 2 ++ spec/fixtures/dir/b | 1 + src/app/project.coffee | 13 +++++++++++++ src/stdlib/child-process.coffee | 1 + 5 files changed, 29 insertions(+) diff --git a/spec/app/project-spec.coffee b/spec/app/project-spec.coffee index 15ef5e7b5..97bace5eb 100644 --- a/spec/app/project-spec.coffee +++ b/spec/app/project-spec.coffee @@ -106,3 +106,15 @@ describe "Project", -> project.getFilePaths().done (paths) -> expect(paths).not.toContain('a') expect(paths).toContain('b') + + describe ".scan(options, callback)", -> + describe "when called with a regex", -> + fit "calls the callback with all regex matches in all files in the project", -> + matches = [] + project.scan regex: /a+/, ({path, match, range}) -> + matches.push({path, match, range}) + + expect(matches[0]).toEqual + path: project.resolve('a') + match: 'aaa' + range: [[0, 0], [0, 3]] diff --git a/spec/fixtures/dir/a b/spec/fixtures/dir/a index e69de29bb..9bda1cbd0 100644 --- a/spec/fixtures/dir/a +++ b/spec/fixtures/dir/a @@ -0,0 +1,2 @@ +aaa bbb +cc aa cc diff --git a/spec/fixtures/dir/b b/spec/fixtures/dir/b index e69de29bb..f2c4fd35c 100644 --- a/spec/fixtures/dir/b +++ b/spec/fixtures/dir/b @@ -0,0 +1 @@ +aaa ccc diff --git a/src/app/project.coffee b/src/app/project.coffee index 40ddfc523..689a1beab 100644 --- a/src/app/project.coffee +++ b/src/app/project.coffee @@ -6,6 +6,7 @@ Buffer = require 'buffer' EditSession = require 'edit-session' EventEmitter = require 'event-emitter' Directory = require 'directory' +ChildProcess = require 'child-process' module.exports = class Project @@ -126,4 +127,16 @@ class Project bufferWithPath: (path) -> return editSession.buffer for editSession in @editSessions when editSession.buffer.getPath() == path + scan: ({regex}, callback) -> + command = "grep --null --perl-regexp --with-filename --line-number --recursive --regexp=#{regex.source} #{@getPath()}" + ChildProcess.exec command, bufferLines: false, stdout: (data) -> + for grepLine in data.split('\n') when grepLine.length + nullCharIndex = grepLine.indexOf('\0') + colonIndex = grepLine.indexOf(':') + path = grepLine.substring(0, nullCharIndex) + row = parseInt(grepLine.substring(nullCharIndex + 1, colonIndex)) - 1 + line = grepLine.substring(colonIndex + 1) + + console.log path, row, line + _.extend Project.prototype, EventEmitter diff --git a/src/stdlib/child-process.coffee b/src/stdlib/child-process.coffee index e8885a7a2..f22cc09a9 100644 --- a/src/stdlib/child-process.coffee +++ b/src/stdlib/child-process.coffee @@ -14,6 +14,7 @@ class ChildProccess options.stderr = @bufferLines(options.stderr) if options.stderr $native.exec command, options, (exitStatus, stdout, stderr) -> + console.log exitStatus if exitStatus != 0 error = new Error("Exec failed (#{exitStatus}) command '#{command}'") error.exitStatus = exitStatus From 76d71c82d0ae7c04ecd5730c4634743a6ce0ec7c Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 12 Jul 2012 11:10:58 -0600 Subject: [PATCH 08/97] waitsForPromise takes a expectRejection option If we expect rejection, we expect the promises's `fail` callback to be invoked and throw an exception if it isn't. Vice versa is also true. --- spec/spec-helper.coffee | 18 ++++++++++++++++-- spec/stdlib/child-process-spec.coffee | 2 +- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index b6d232993..6c69dafb1 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -88,10 +88,24 @@ window.mousedownEvent = (properties={}) -> window.mousemoveEvent = (properties={}) -> window.mouseEvent('mousemove', properties) -window.waitsForPromise = (fn) -> +window.waitsForPromise = (args...) -> + if args.length > 1 + { shouldReject } = args[0] + else + shouldReject = false + fn = _.last(args) + window.waitsFor (moveOn) -> promise = fn() - promise.then promise.done(moveOn), promise.fail(moveOn) + if shouldReject + promise.fail(moveOn) + promise.done -> throw new Error("Expected promise to be rejected, but it was resolved") + else + promise.done(moveOn) + promise.fail -> + debugger + throw new Error("Expected promise to be resolved, but it was rejected") + window.resetTimeouts = -> window.now = 0 diff --git a/spec/stdlib/child-process-spec.coffee b/spec/stdlib/child-process-spec.coffee index 6c8b7e7d8..0bf6192e4 100644 --- a/spec/stdlib/child-process-spec.coffee +++ b/spec/stdlib/child-process-spec.coffee @@ -87,7 +87,7 @@ describe 'Child Processes', -> describe "when the command fails", -> it "executes the callback with error set to the exit status", -> - waitsForPromise -> + waitsForPromise shouldReject: true, -> cmd = "exit 2" ChildProcess.exec(cmd).fail (error) -> expect(error.exitStatus).toBe 2 From b62ddcfa013f62d37b000ec1983611b171293ea4 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 12 Jul 2012 11:11:34 -0600 Subject: [PATCH 09/97] Assign autocompleter to editor so we can inspect it when it f's up --- src/extensions/autocomplete/autocomplete.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extensions/autocomplete/autocomplete.coffee b/src/extensions/autocomplete/autocomplete.coffee index 5706482a2..5a8ea7845 100644 --- a/src/extensions/autocomplete/autocomplete.coffee +++ b/src/extensions/autocomplete/autocomplete.coffee @@ -27,7 +27,7 @@ class Autocomplete extends View @activate: (rootView) -> new Autocomplete(editor) for editor in rootView.getEditors() rootView.on 'editor-open', (e, editor) -> - new Autocomplete(editor) unless editor.is('.autocomplete .mini') + editor.autoComplete = new Autocomplete(editor) unless editor.is('.autocomplete .mini') initialize: (@editor) -> requireStylesheet 'autocomplete.css' From 2fe56ba077a65f247c6b466b1a515d3dd8c1dc4a Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 12 Jul 2012 11:11:45 -0600 Subject: [PATCH 10/97] Remove logging --- src/stdlib/child-process.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/src/stdlib/child-process.coffee b/src/stdlib/child-process.coffee index f22cc09a9..e8885a7a2 100644 --- a/src/stdlib/child-process.coffee +++ b/src/stdlib/child-process.coffee @@ -14,7 +14,6 @@ class ChildProccess options.stderr = @bufferLines(options.stderr) if options.stderr $native.exec command, options, (exitStatus, stdout, stderr) -> - console.log exitStatus if exitStatus != 0 error = new Error("Exec failed (#{exitStatus}) command '#{command}'") error.exitStatus = exitStatus From d2d6efdcb5aa9a33b245f71b6a2aab721b282db5 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 12 Jul 2012 11:12:31 -0600 Subject: [PATCH 11/97] Project.scan invokes the callback with matches --- spec/app/project-spec.coffee | 25 ++++++++++++++++++------- src/app/project.coffee | 24 +++++++++++++----------- 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/spec/app/project-spec.coffee b/spec/app/project-spec.coffee index 97bace5eb..f05712fe3 100644 --- a/spec/app/project-spec.coffee +++ b/spec/app/project-spec.coffee @@ -109,12 +109,23 @@ describe "Project", -> describe ".scan(options, callback)", -> describe "when called with a regex", -> - fit "calls the callback with all regex matches in all files in the project", -> + it "calls the callback with all regex matches in all files in the project", -> matches = [] - project.scan regex: /a+/, ({path, match, range}) -> - matches.push({path, match, range}) - expect(matches[0]).toEqual - path: project.resolve('a') - match: 'aaa' - range: [[0, 0], [0, 3]] + waitsForPromise -> + project.scan /a+/, ({path, match, range}) -> + console.log "ITERATOR", path, match, range + matches.push({path, match, range}) + + runs -> + expect(matches[0]).toEqual + path: project.resolve('a') + match: ['aaa'] + range: [[0, 0], [0, 3]] + + expect(matches[1]).toEqual + path: project.resolve('a') + match: ['aa'] + range: [[1, 3], [1, 5]] + + diff --git a/src/app/project.coffee b/src/app/project.coffee index 689a1beab..d10110582 100644 --- a/src/app/project.coffee +++ b/src/app/project.coffee @@ -1,7 +1,7 @@ fs = require 'fs' _ = require 'underscore' $ = require 'jquery' - +Range = require 'range' Buffer = require 'buffer' EditSession = require 'edit-session' EventEmitter = require 'event-emitter' @@ -127,16 +127,18 @@ class Project bufferWithPath: (path) -> return editSession.buffer for editSession in @editSessions when editSession.buffer.getPath() == path - scan: ({regex}, callback) -> + scan: (regex, iterator) -> + regex = new RegExp(regex.source, 'g') command = "grep --null --perl-regexp --with-filename --line-number --recursive --regexp=#{regex.source} #{@getPath()}" - ChildProcess.exec command, bufferLines: false, stdout: (data) -> - for grepLine in data.split('\n') when grepLine.length - nullCharIndex = grepLine.indexOf('\0') - colonIndex = grepLine.indexOf(':') - path = grepLine.substring(0, nullCharIndex) - row = parseInt(grepLine.substring(nullCharIndex + 1, colonIndex)) - 1 - line = grepLine.substring(colonIndex + 1) - - console.log path, row, line + ChildProcess.exec command, bufferLines: true, stdout: (data) -> + for grepLine in data.split('\n') when grepLine.length + nullCharIndex = grepLine.indexOf('\0') + colonIndex = grepLine.indexOf(':') + path = grepLine.substring(0, nullCharIndex) + row = parseInt(grepLine.substring(nullCharIndex + 1, colonIndex)) - 1 + line = grepLine.substring(colonIndex + 1) + while match = regex.exec(line) + range = new Range([row, match.index], [row, match.index + match[0].length]) + iterator({path, match, range}) _.extend Project.prototype, EventEmitter From ba2e85a548d9072d501326d2f8374d21f4ef8622 Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Thu, 12 Jul 2012 08:51:04 -0700 Subject: [PATCH 12/97] :lipstick --- spec/stdlib/child-process-spec.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/stdlib/child-process-spec.coffee b/spec/stdlib/child-process-spec.coffee index 0bf6192e4..afd9baaa7 100644 --- a/spec/stdlib/child-process-spec.coffee +++ b/spec/stdlib/child-process-spec.coffee @@ -15,8 +15,8 @@ describe 'Child Processes', -> expect(stdout).toBe 'good\n' expect(stderr).toBe 'bad\n' - describe "when `options` contains stdout/stderror callbacks", -> - it "calls the stdout callback when new data is received on stdout", -> + describe "when options are given", -> + it "calls the options.stdout callback when new data is received on stdout", -> cmd = "echo 'first' && sleep .1 && echo 'second' && sleep .1 && echo 'third'" ChildProcess.exec(cmd, stdout: stdoutHandler) @@ -28,7 +28,7 @@ describe 'Child Processes', -> expect(stdoutHandler.argsForCall[1][0]).toBe "second\n" expect(stdoutHandler.argsForCall[2][0]).toBe "third\n" - it "calls the stderr callback when new data is received on stderr", -> + it "calls the options.stderr callback when new data is received on stderr", -> cmd = "echo '1111' >&2 && sleep .1 && echo '2222' >&2" ChildProcess.exec(cmd, stderr: stderrHandler) From 0d50066dad622d3829378b08d7d34107ada7573e Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 12 Jul 2012 11:44:07 -0700 Subject: [PATCH 13/97] Add window.originalSetTimeout Signed-off-by: Corey Johnson & Nathan Sobo --- spec/spec-helper.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 6c69dafb1..6c6472894 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -112,6 +112,7 @@ window.resetTimeouts = -> window.timeoutCount = 0 window.timeouts = [] +window.originalSetTimeout = window.setTimeout window.setTimeout = (callback, ms) -> id = ++window.timeoutCount window.timeouts.push([id, window.now + ms, callback]) From 1331cfb74994d5c395964c23772153ffdbc3ab90 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 12 Jul 2012 11:45:44 -0700 Subject: [PATCH 14/97] When promise resolution throws an exception, waitsForPromise is halted --- spec/spec-helper.coffee | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 6c6472894..8fc7fcbd0 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -99,13 +99,14 @@ window.waitsForPromise = (args...) -> promise = fn() if shouldReject promise.fail(moveOn) - promise.done -> throw new Error("Expected promise to be rejected, but it was resolved") + promise.done -> + jasmine.getEnv().currentSpec.fail("Expected promise to be rejected, but it was resolved") + moveOn() else promise.done(moveOn) promise.fail -> - debugger - throw new Error("Expected promise to be resolved, but it was rejected") - + jasmine.getEnv().currentSpec.fail("Expected promise to be resolved, but it was rejected") + moveOn() window.resetTimeouts = -> window.now = 0 From 0d35d5613a7fe015ac2c47439544326acc1a7ddd Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 12 Jul 2012 12:07:23 -0700 Subject: [PATCH 15/97] Non-zero exit codes reject promise with a detailed message --- src/stdlib/child-process.coffee | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/stdlib/child-process.coffee b/src/stdlib/child-process.coffee index e8885a7a2..c7b9a9174 100644 --- a/src/stdlib/child-process.coffee +++ b/src/stdlib/child-process.coffee @@ -15,9 +15,7 @@ class ChildProccess $native.exec command, options, (exitStatus, stdout, stderr) -> if exitStatus != 0 - error = new Error("Exec failed (#{exitStatus}) command '#{command}'") - error.exitStatus = exitStatus - deferred.reject(error) + deferred.reject("Command '#{command}' failed with exit status #{exitStatus} and stderr '#{stderr}'") else deferred.resolve(stdout, stderr) From 075b2e4304374f1b5aea404061f84caa47a7bca6 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 12 Jul 2012 12:10:37 -0700 Subject: [PATCH 16/97] Don't rethrow execeptions in exec callbacks. Since the callbacks are executed asynchronously, no v8 code is above them on the stack to which to rethrow. It was causing crashes. --- Atom/src/native_handler.mm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Atom/src/native_handler.mm b/Atom/src/native_handler.mm index 3c07fd35d..7b8103898 100644 --- a/Atom/src/native_handler.mm +++ b/Atom/src/native_handler.mm @@ -454,7 +454,7 @@ bool NativeHandler::Execute(const CefString& name, CefRefPtr e; args.push_back(CefV8Value::CreateString(std::string([contents UTF8String], [contents length]))); - function->ExecuteFunction(function, args, retval, e, true); + function->ExecuteFunction(function, args, retval, e, false); [contents release]; context->Exit(); }; @@ -472,7 +472,7 @@ bool NativeHandler::Execute(const CefString& name, args.push_back(CefV8Value::CreateString([output UTF8String])); args.push_back(CefV8Value::CreateString([errorOutput UTF8String])); - callback->ExecuteFunction(callback, args, retval, e, true); + callback->ExecuteFunction(callback, args, retval, e, false); context->Exit(); stdout.fileHandleForReading.writeabilityHandler = nil; From 64a9c245e7d9b80371d2f10068da1532baf149a1 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 12 Jul 2012 12:11:00 -0700 Subject: [PATCH 17/97] Quote regex passed to grep --- src/app/project.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/project.coffee b/src/app/project.coffee index d10110582..610592708 100644 --- a/src/app/project.coffee +++ b/src/app/project.coffee @@ -129,7 +129,7 @@ class Project scan: (regex, iterator) -> regex = new RegExp(regex.source, 'g') - command = "grep --null --perl-regexp --with-filename --line-number --recursive --regexp=#{regex.source} #{@getPath()}" + command = "grep --null --perl-regexp --with-filename --line-number --recursive --regexp=\"#{regex.source}\" #{@getPath()}" ChildProcess.exec command, bufferLines: true, stdout: (data) -> for grepLine in data.split('\n') when grepLine.length nullCharIndex = grepLine.indexOf('\0') From ded4ac7fa1185160de441a4d12d3a6416e33a776 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 12 Jul 2012 12:11:48 -0700 Subject: [PATCH 18/97] WaitsForPromise outputs reject callback arguments --- spec/spec-helper.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 8fc7fcbd0..62c51b39d 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -104,8 +104,8 @@ window.waitsForPromise = (args...) -> moveOn() else promise.done(moveOn) - promise.fail -> - jasmine.getEnv().currentSpec.fail("Expected promise to be resolved, but it was rejected") + promise.fail (error) -> + jasmine.getEnv().currentSpec.fail("Expected promise to be resolved, but it was rejected with #{jasmine.pp(error)}") moveOn() window.resetTimeouts = -> From 86adf09267e3d76ecadf1298f82845672d0c9897 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 12 Jul 2012 12:12:44 -0700 Subject: [PATCH 19/97] Test that project scan executes callback with match data --- spec/app/project-spec.coffee | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/spec/app/project-spec.coffee b/spec/app/project-spec.coffee index f05712fe3..f9b930acd 100644 --- a/spec/app/project-spec.coffee +++ b/spec/app/project-spec.coffee @@ -109,23 +109,21 @@ describe "Project", -> describe ".scan(options, callback)", -> describe "when called with a regex", -> - it "calls the callback with all regex matches in all files in the project", -> + fit "calls the callback with all regex matches in all files in the project", -> matches = [] - waitsForPromise -> - project.scan /a+/, ({path, match, range}) -> - console.log "ITERATOR", path, match, range + project.scan /(a)+/, ({path, match, range}) -> matches.push({path, match, range}) runs -> expect(matches[0]).toEqual path: project.resolve('a') - match: ['aaa'] + match: ['aaa', 'a'] range: [[0, 0], [0, 3]] expect(matches[1]).toEqual path: project.resolve('a') - match: ['aa'] + match: ['aa', 'a'] range: [[1, 3], [1, 5]] From e59d15868dad006c47ffb3eb38b2fc883a356e8f Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 12 Jul 2012 12:18:24 -0700 Subject: [PATCH 20/97] un-f --- spec/app/project-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/app/project-spec.coffee b/spec/app/project-spec.coffee index f9b930acd..b9203b1a5 100644 --- a/spec/app/project-spec.coffee +++ b/spec/app/project-spec.coffee @@ -109,7 +109,7 @@ describe "Project", -> describe ".scan(options, callback)", -> describe "when called with a regex", -> - fit "calls the callback with all regex matches in all files in the project", -> + it "calls the callback with all regex matches in all files in the project", -> matches = [] waitsForPromise -> project.scan /(a)+/, ({path, match, range}) -> From 47a9d1284a050ba6c7306693e59bd2fdb9109c40 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 12 Jul 2012 12:18:53 -0700 Subject: [PATCH 21/97] Reject exec promise with hash instead of string --- spec/stdlib/child-process-spec.coffee | 3 ++- src/stdlib/child-process.coffee | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/spec/stdlib/child-process-spec.coffee b/spec/stdlib/child-process-spec.coffee index afd9baaa7..7d57a21cb 100644 --- a/spec/stdlib/child-process-spec.coffee +++ b/spec/stdlib/child-process-spec.coffee @@ -88,6 +88,7 @@ describe 'Child Processes', -> describe "when the command fails", -> it "executes the callback with error set to the exit status", -> waitsForPromise shouldReject: true, -> - cmd = "exit 2" + cmd = "echo 'bad' >&2 && exit 2" ChildProcess.exec(cmd).fail (error) -> expect(error.exitStatus).toBe 2 + expect(error.stderr).toBe "bad\n" diff --git a/src/stdlib/child-process.coffee b/src/stdlib/child-process.coffee index c7b9a9174..4f8db76bf 100644 --- a/src/stdlib/child-process.coffee +++ b/src/stdlib/child-process.coffee @@ -15,7 +15,7 @@ class ChildProccess $native.exec command, options, (exitStatus, stdout, stderr) -> if exitStatus != 0 - deferred.reject("Command '#{command}' failed with exit status #{exitStatus} and stderr '#{stderr}'") + deferred.reject({command, exitStatus, stderr}) else deferred.resolve(stdout, stderr) From bee6bf0ff567c493b15443a36ef3fc5d09272a01 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 12 Jul 2012 17:56:23 -0600 Subject: [PATCH 22/97] Add explicit ZeroAddress object to command language AST --- src/extensions/command-panel/commands.pegjs | 8 +++++--- .../command-panel/commands/zero-address.coffee | 9 +++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 src/extensions/command-panel/commands/zero-address.coffee diff --git a/src/extensions/command-panel/commands.pegjs b/src/extensions/command-panel/commands.pegjs index 1a3b92307..982a206fe 100644 --- a/src/extensions/command-panel/commands.pegjs +++ b/src/extensions/command-panel/commands.pegjs @@ -1,9 +1,10 @@ { var CompositeCommand = require('command-panel/commands/composite-command') var Substitution = require('command-panel/commands/substitution'); + var ZeroAddress = require('command-panel/commands/zero-address'); + var EofAddress = require('command-panel/commands/eof-address'); var LineAddress = require('command-panel/commands/line-address'); var AddressRange = require('command-panel/commands/address-range'); - var EofAddress = require('command-panel/commands/eof-address'); var CurrentSelectionAddress = require('command-panel/commands/current-selection-address') var RegexAddress = require('command-panel/commands/regex-address') var SelectAllMatches = require('command-panel/commands/select-all-matches') @@ -19,15 +20,16 @@ address = addressRange / primitiveAddress addressRange = start:primitiveAddress? _ ',' _ end:address? { - if (!start) start = new LineAddress(0) + if (!start) start = new ZeroAddress() if (!end) end = new EofAddress() return new AddressRange(start, end) } primitiveAddress - = lineNumber:integer { return new LineAddress(lineNumber) } + = '0' { return new ZeroAddress() } / '$' { return new EofAddress() } / '.' { return new CurrentSelectionAddress() } + / lineNumber:integer { return new LineAddress(lineNumber) } / regexAddress regexAddress diff --git a/src/extensions/command-panel/commands/zero-address.coffee b/src/extensions/command-panel/commands/zero-address.coffee new file mode 100644 index 000000000..c8c2dfb69 --- /dev/null +++ b/src/extensions/command-panel/commands/zero-address.coffee @@ -0,0 +1,9 @@ +Address = require 'command-panel/commands/address' +Range = require 'range' + +module.exports = +class ZeroAddress extends Address + getRange: (editor) -> + new Range([0, 0], [0, 0]) + + isRelative: -> false From 8e3c3a13a9603fca9a5bdbc7bfd04ed44e87d031 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 12 Jul 2012 17:59:45 -0600 Subject: [PATCH 23/97] Substitution commands don't change editor selection --- spec/extensions/command-interpreter-spec.coffee | 2 +- .../command-panel/commands/command.coffee | 2 +- .../commands/composite-command.coffee | 15 +++++++-------- .../command-panel/commands/substitution.coffee | 2 +- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/spec/extensions/command-interpreter-spec.coffee b/spec/extensions/command-interpreter-spec.coffee index b4c7c1e46..ebe05e3a0 100644 --- a/spec/extensions/command-interpreter-spec.coffee +++ b/spec/extensions/command-interpreter-spec.coffee @@ -243,7 +243,7 @@ describe "CommandInterpreter", -> it "restores the original selections upon completion if it is the last command", -> editor.setSelectedBufferRanges([[[5, 0], [5, 20]], [[6, 0], [6, 44]]]) interpreter.eval(editor, ',s/current/foo/g') - expect(editor.getSelectedBufferRanges()).toEqual [[[5, 0], [5, 20]], [[6, 0], [6, 44]]] + expect(editor.getSelectedBufferRanges()).toEqual [[[5, 0], [5, 16]], [[6, 0], [6, 36]]] describe "when command selects folded text", -> it "unfolds lines that command selects", -> diff --git a/src/extensions/command-panel/commands/command.coffee b/src/extensions/command-panel/commands/command.coffee index 5c6487384..b337f0a61 100644 --- a/src/extensions/command-panel/commands/command.coffee +++ b/src/extensions/command-panel/commands/command.coffee @@ -3,4 +3,4 @@ _ = require 'underscore' module.exports = class Command isAddress: -> false - restoreSelections: false + preserveSelections: false diff --git a/src/extensions/command-panel/commands/composite-command.coffee b/src/extensions/command-panel/commands/composite-command.coffee index 90323ef19..32c3262e0 100644 --- a/src/extensions/command-panel/commands/composite-command.coffee +++ b/src/extensions/command-panel/commands/composite-command.coffee @@ -5,19 +5,18 @@ class CompositeCommand constructor: (@subcommands) -> execute: (editor) -> - initialRanges = editor.getSelectedBufferRanges() + currentRanges = editor.getSelectedBufferRanges() for command in @subcommands newRanges = [] - currentRanges = editor.getSelectedBufferRanges() - for currentRange in currentRanges - newRanges.push(command.execute(editor, currentRange)...) + for range in currentRanges + newRanges.push(command.execute(editor, range)...) + currentRanges = newRanges - for range in newRanges + unless command.preserveSelections + for range in currentRanges for row in [range.start.row..range.end.row] editor.destroyFoldsContainingBufferRow(row) - - editor.setSelectedBufferRanges(newRanges) - editor.setSelectedBufferRanges(initialRanges) if command.restoreSelections + editor.setSelectedBufferRanges(currentRanges) reverse: -> new CompositeCommand(@subcommands.map (command) -> command.reverse()) diff --git a/src/extensions/command-panel/commands/substitution.coffee b/src/extensions/command-panel/commands/substitution.coffee index 4ba6f2ff9..d2c7f7c2d 100644 --- a/src/extensions/command-panel/commands/substitution.coffee +++ b/src/extensions/command-panel/commands/substitution.coffee @@ -4,7 +4,7 @@ module.exports = class Substitution extends Command regex: null replacementText: null - restoreSelections: true + preserveSelections: true constructor: (pattern, replacementText, options) -> @replacementText = replacementText From f02e7246068acf5621b805dd7ae755b0642e48c3 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 12 Jul 2012 18:00:18 -0600 Subject: [PATCH 24/97] :poop: --- src/extensions/snippets/snippets.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extensions/snippets/snippets.coffee b/src/extensions/snippets/snippets.coffee index d1c51de9d..de88ed7a1 100644 --- a/src/extensions/snippets/snippets.coffee +++ b/src/extensions/snippets/snippets.coffee @@ -27,7 +27,7 @@ module.exports = editor.on 'snippets:expand', (e) => editSession = editor.activeEditSession prefix = editSession.getLastCursor().getCurrentWordPrefix() - if snippet = @snippetsByExtension[editSession.getFileExtension()][prefix] + if snippet = @snippetsByExtension[editSession.getFileExtension()]?[prefix] editSession.transact -> snippetExpansion = new SnippetExpansion(snippet, editSession) editSession.snippetExpansion = snippetExpansion From f99146b42fada4bc5ceb0ec4e9b2a332664c1b9b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 12 Jul 2012 18:40:34 -0600 Subject: [PATCH 25/97] EditSession.setSelectedBufferRanges destroys folds It also clears all existing selections and makes new ones. --- spec/app/edit-session-spec.coffee | 38 ++++++++++++++++++++++++------- src/app/edit-session.coffee | 12 ++++++---- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index 2bdc18d68..46ec5debc 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -523,6 +523,31 @@ describe "EditSession", -> editSession.selectWord() expect(editSession.getSelectedText()).toBe '' + describe ".setSelectedBufferRanges(ranges)", -> + it "clears existing selections and creates selections for each of the given ranges", -> + editSession.setSelectedBufferRanges([[[2, 2], [3, 3]], [[4, 4], [5, 5]]]) + expect(editSession.getSelectedBufferRanges()).toEqual [[[2, 2], [3, 3]], [[4, 4], [5, 5]]] + + editSession.setSelectedBufferRanges([[[5, 5], [6, 6]]]) + expect(editSession.getSelectedBufferRanges()).toEqual [[[5, 5], [6, 6]]] + + it "merges intersecting selections", -> + editSession.setSelectedBufferRanges([[[2, 2], [3, 3]], [[3, 0], [5, 5]]]) + expect(editSession.getSelectedBufferRanges()).toEqual [[[2, 2], [5, 5]]] + + it "removes folds that contain the selections", -> + editSession.setSelectedBufferRange([[0,0], [0,0]]) + editSession.createFold(1, 4) + editSession.createFold(2, 3) + editSession.createFold(6, 8) + editSession.createFold(10, 11) + + editSession.setSelectedBufferRanges([[[2, 2], [3, 3]], [[6, 6], [7, 7]]]) + expect(editSession.lineForScreenRow(1).fold).toBeUndefined() + expect(editSession.lineForScreenRow(2).fold).toBeUndefined() + expect(editSession.lineForScreenRow(6).fold).toBeUndefined() + expect(editSession.lineForScreenRow(10).fold).toBeDefined() + describe "when the cursor is moved while there is a selection", -> makeSelection = -> selection.setBufferRange [[1, 2], [1, 5]] @@ -1243,20 +1268,17 @@ describe "EditSession", -> selections = editSession.getSelections() expect(buffer.lineForRow(1)).toBe ' var = function( {' - expect(selections[0].getBufferRange()).toEqual [[1, 6], [1, 6]] - expect(selections[1].getBufferRange()).toEqual [[1, 17], [1, 17]] + + expect(editSession.getSelectedBufferRanges()).toEqual [[[1, 6], [1, 6]], [[1, 17], [1, 17]]] editSession.undo() - expect(selections[0].getBufferRange()).toEqual [[1, 6], [1, 6]] - expect(selections[1].getBufferRange()).toEqual [[1, 18], [1, 18]] + expect(editSession.getSelectedBufferRanges()).toEqual [[[1, 6], [1, 6]], [[1, 18], [1, 18]]] editSession.undo() - expect(selections[0].getBufferRange()).toEqual [[1, 6], [1, 10]] - expect(selections[1].getBufferRange()).toEqual [[1, 22], [1, 27]] + expect(editSession.getSelectedBufferRanges()).toEqual [[[1, 6], [1, 10]], [[1, 22], [1, 27]]] editSession.redo() - expect(selections[0].getBufferRange()).toEqual [[1, 6], [1, 6]] - expect(selections[1].getBufferRange()).toEqual [[1, 18], [1, 18]] + expect(editSession.getSelectedBufferRanges()).toEqual [[[1, 6], [1, 6]], [[1, 18], [1, 18]]] it "restores selected ranges even when the change occurred in another edit session", -> otherEditSession = fixturesProject.open(editSession.getPath()) diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index 53d4a265c..91bfefee4 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -5,6 +5,7 @@ DisplayBuffer = require 'display-buffer' Cursor = require 'cursor' Selection = require 'selection' EventEmitter = require 'event-emitter' +Range = require 'range' AnchorRange = require 'anchor-range' _ = require 'underscore' @@ -309,13 +310,14 @@ class EditSession @getLastSelection().setBufferRange(bufferRange, options) setSelectedBufferRanges: (bufferRanges, options) -> - selections = @getSelections() + throw new Error("Passed an empty array to setSelectedBufferRanges") unless bufferRanges.length + selection.destroy() for selection in @getSelections() for bufferRange, i in bufferRanges - if selections[i] - selections[i].setBufferRange(bufferRange, options) - else + bufferRange = Range.fromObject(bufferRange) + for row in [bufferRange.start.row..bufferRange.end.row] + @destroyFoldsContainingBufferRow(row) @addSelectionForBufferRange(bufferRange, options) - @mergeIntersectingSelections() + @mergeIntersectingSelections(options) removeSelection: (selection) -> _.remove(@selections, selection) From a4db677979be8c954402dd0625cca999bad7bc3f Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 12 Jul 2012 18:57:12 -0600 Subject: [PATCH 26/97] Implement setSelectedBufferRange w/ setSelectedBufferRanges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also add the preserveFolds option, which doesn't destroy folds containing the selected ranges – mainly for specs right now --- spec/app/edit-session-spec.coffee | 34 +++++++++++++++++++------------ spec/app/editor-spec.coffee | 8 ++++---- spec/app/selection-spec.coffee | 9 ++++++++ src/app/edit-session.coffee | 12 +++++------ src/app/selection.coffee | 1 + 5 files changed, 41 insertions(+), 23 deletions(-) diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index 46ec5debc..43466df05 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -535,18 +535,26 @@ describe "EditSession", -> editSession.setSelectedBufferRanges([[[2, 2], [3, 3]], [[3, 0], [5, 5]]]) expect(editSession.getSelectedBufferRanges()).toEqual [[[2, 2], [5, 5]]] - it "removes folds that contain the selections", -> - editSession.setSelectedBufferRange([[0,0], [0,0]]) - editSession.createFold(1, 4) - editSession.createFold(2, 3) - editSession.createFold(6, 8) - editSession.createFold(10, 11) + describe "when the preserveFolds option is false (the default)", -> + it "removes folds that contain the selections", -> + editSession.setSelectedBufferRange([[0,0], [0,0]]) + editSession.createFold(1, 4) + editSession.createFold(2, 3) + editSession.createFold(6, 8) + editSession.createFold(10, 11) - editSession.setSelectedBufferRanges([[[2, 2], [3, 3]], [[6, 6], [7, 7]]]) - expect(editSession.lineForScreenRow(1).fold).toBeUndefined() - expect(editSession.lineForScreenRow(2).fold).toBeUndefined() - expect(editSession.lineForScreenRow(6).fold).toBeUndefined() - expect(editSession.lineForScreenRow(10).fold).toBeDefined() + editSession.setSelectedBufferRanges([[[2, 2], [3, 3]], [[6, 6], [7, 7]]]) + expect(editSession.lineForScreenRow(1).fold).toBeUndefined() + expect(editSession.lineForScreenRow(2).fold).toBeUndefined() + expect(editSession.lineForScreenRow(6).fold).toBeUndefined() + expect(editSession.lineForScreenRow(10).fold).toBeDefined() + + describe "when the preserve folds option is true", -> + it "does not remove folds that contain the selections", -> + editSession.setSelectedBufferRange([[0,0], [0,0]]) + editSession.createFold(1, 4) + editSession.setSelectedBufferRanges([[[2, 2], [3, 3]]], preserveFolds: true) + expect(editSession.lineForScreenRow(1).fold).toBeDefined() describe "when the cursor is moved while there is a selection", -> makeSelection = -> selection.setBufferRange [[1, 2], [1, 5]] @@ -895,8 +903,8 @@ describe "EditSession", -> describe "when the selection ends on a folded line", -> it "destroys the fold", -> - editSession.toggleFoldAtBufferRow(4) editSession.setSelectedBufferRange([[3,0], [4,0]]) + editSession.toggleFoldAtBufferRow(4) editSession.backspace() expect(buffer.lineForRow(3)).toBe " return sort(left).concat(pivot).concat(sort(right));" @@ -980,12 +988,12 @@ describe "EditSession", -> describe "when the cursor is on a folded line", -> it "removes the lines contained by the fold", -> + editSession.setSelectedBufferRange([[2, 0], [2, 0]]) editSession.createFold(2,4) editSession.createFold(2,6) oldLine7 = buffer.lineForRow(7) oldLine8 = buffer.lineForRow(8) - editSession.setSelectedBufferRange([[2, 0], [2, 0]]) editSession.delete() expect(editSession.lineForScreenRow(2).text).toBe oldLine7 expect(editSession.lineForScreenRow(3).text).toBe oldLine8 diff --git a/spec/app/editor-spec.coffee b/spec/app/editor-spec.coffee index bce9373d3..757d8ca28 100644 --- a/spec/app/editor-spec.coffee +++ b/spec/app/editor-spec.coffee @@ -1549,13 +1549,13 @@ describe "Editor", -> it "adds/removes the 'selected' class to the fold's line element and hides the cursor if it is on the fold line", -> editor.createFold(2, 4) - editor.setSelectedBufferRange([[1, 0], [2, 0]], reverse: true) + editor.setSelectedBufferRange([[1, 0], [2, 0]], preserveFolds: true, reverse: true) expect(editor.lineElementForScreenRow(2)).toMatchSelector('.fold.selected') - editor.setSelectedBufferRange([[1, 0], [1, 1]]) + editor.setSelectedBufferRange([[1, 0], [1, 1]], preserveFolds: true) expect(editor.lineElementForScreenRow(2)).not.toMatchSelector('.fold.selected') - editor.setSelectedBufferRange([[1, 0], [5, 0]]) + editor.setSelectedBufferRange([[1, 0], [5, 0]], preserveFolds: true) expect(editor.lineElementForScreenRow(2)).toMatchSelector('.fold.selected') editor.setCursorScreenPosition([3,0]) @@ -1574,7 +1574,7 @@ describe "Editor", -> editor.renderLines() # re-render lines so certain lines are not rendered editor.createFold(2, 4) - editor.setSelectedBufferRange([[1, 0], [5, 0]]) + editor.setSelectedBufferRange([[1, 0], [5, 0]], preserveFolds: true) expect(editor.renderedLines.find('.fold.selected')).toExist() editor.scrollToBottom() diff --git a/spec/app/selection-spec.coffee b/spec/app/selection-spec.coffee index 0a91272ea..a43ad7f1e 100644 --- a/spec/app/selection-spec.coffee +++ b/spec/app/selection-spec.coffee @@ -66,3 +66,12 @@ describe "Selection", -> buffer.insert([2, 5], 'abc') expect(changeScreenRangeHandler).toHaveBeenCalled() + + describe "when the selection is destroyed", -> + it "destroys its cursor and its anchor's cursor", -> + selection.setBufferRange([[2, 0], [2, 10]]) + + selection.destroy() + + expect(editSession.getAnchors().indexOf(selection.anchor)).toBe -1 + expect(editSession.getAnchors().indexOf(selection.cursor.anchor)).toBe -1 diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index 91bfefee4..7887cb900 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -306,17 +306,17 @@ class EditSession @addCursor().selection.setBufferRange(bufferRange, options) setSelectedBufferRange: (bufferRange, options) -> - @clearSelections() - @getLastSelection().setBufferRange(bufferRange, options) + @setSelectedBufferRanges([bufferRange], options) - setSelectedBufferRanges: (bufferRanges, options) -> + setSelectedBufferRanges: (bufferRanges, options={}) -> throw new Error("Passed an empty array to setSelectedBufferRanges") unless bufferRanges.length selection.destroy() for selection in @getSelections() for bufferRange, i in bufferRanges bufferRange = Range.fromObject(bufferRange) - for row in [bufferRange.start.row..bufferRange.end.row] - @destroyFoldsContainingBufferRow(row) - @addSelectionForBufferRange(bufferRange, options) + unless options.preserveFolds + for row in [bufferRange.start.row..bufferRange.end.row] + @destroyFoldsContainingBufferRow(row) + @addSelectionForBufferRange(bufferRange, options) @mergeIntersectingSelections(options) removeSelection: (selection) -> diff --git a/src/app/selection.coffee b/src/app/selection.coffee index ffe5c58b4..85819ef6b 100644 --- a/src/app/selection.coffee +++ b/src/app/selection.coffee @@ -21,6 +21,7 @@ class Selection if @cursor @cursor.off('.selection') @cursor.destroy() + @anchor?.destroy() @editSession.removeSelection(this) @trigger 'destroy' From 42f322a112a2d0e3fd11f2d356237a008ca8b3e3 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Fri, 13 Jul 2012 15:10:37 -0600 Subject: [PATCH 27/97] CommandInterpreter uses project and edit sessions instead of editor to execute We don't want to pass view objects into it! --- .../command-interpreter-spec.coffee | 202 +++++++++--------- src/app/root-view.coffee | 3 + .../command-panel/command-interpreter.coffee | 14 +- .../command-panel/command-panel.coffee | 8 +- .../commands/address-range.coffee | 4 +- .../command-panel/commands/address.coffee | 4 +- .../commands/composite-command.coffee | 12 +- .../commands/current-selection-address.coffee | 4 +- .../command-panel/commands/eof-address.coffee | 7 +- .../commands/regex-address.coffee | 12 +- .../commands/select-all-matches.coffee | 6 +- .../commands/substitution.coffee | 6 +- .../commands/zero-address.coffee | 2 +- 13 files changed, 141 insertions(+), 143 deletions(-) diff --git a/spec/extensions/command-interpreter-spec.coffee b/spec/extensions/command-interpreter-spec.coffee index ebe05e3a0..c84bd7ea6 100644 --- a/spec/extensions/command-interpreter-spec.coffee +++ b/spec/extensions/command-interpreter-spec.coffee @@ -1,76 +1,74 @@ CommandInterpreter = require 'command-panel/command-interpreter' Buffer = require 'buffer' EditSession = require 'edit-session' -Editor = require 'editor' describe "CommandInterpreter", -> - [interpreter, editor, buffer] = [] + [interpreter, editSession, buffer] = [] beforeEach -> + interpreter = new CommandInterpreter(fixturesProject) editSession = fixturesProject.open('sample.js') buffer = editSession.buffer - editor = new Editor(editSession: editSession) - interpreter = new CommandInterpreter() afterEach -> - editor.remove() + editSession.destroy() describe "addresses", -> beforeEach -> - editor.addSelectionForBufferRange([[7,0], [7,11]]) - editor.addSelectionForBufferRange([[8,0], [8,11]]) + editSession.addSelectionForBufferRange([[7,0], [7,11]]) + editSession.addSelectionForBufferRange([[8,0], [8,11]]) describe "a line address", -> it "selects the specified line", -> - interpreter.eval(editor, '4') - expect(editor.getSelections().length).toBe 1 - expect(editor.getSelection().getBufferRange()).toEqual [[3, 0], [4, 0]] + interpreter.eval('4', editSession) + expect(editSession.getSelections().length).toBe 1 + expect(editSession.getSelection().getBufferRange()).toEqual [[3, 0], [4, 0]] describe "0", -> it "selects the zero-length string at the start of the file", -> - interpreter.eval(editor, '0') - expect(editor.getSelections().length).toBe 1 - expect(editor.getSelection().getBufferRange()).toEqual [[0,0], [0,0]] + interpreter.eval('0', editSession) + expect(editSession.getSelections().length).toBe 1 + expect(editSession.getSelection().getBufferRange()).toEqual [[0,0], [0,0]] - interpreter.eval(editor, '0,1') - expect(editor.getSelections().length).toBe 1 - expect(editor.getSelection().getBufferRange()).toEqual [[0,0], [1,0]] + interpreter.eval('0,1', editSession) + expect(editSession.getSelections().length).toBe 1 + expect(editSession.getSelection().getBufferRange()).toEqual [[0,0], [1,0]] describe "$", -> it "selects EOF", -> - interpreter.eval(editor, '$') - expect(editor.getSelections().length).toBe 1 - expect(editor.getSelection().getBufferRange()).toEqual [[12,2], [12,2]] + interpreter.eval('$', editSession) + expect(editSession.getSelections().length).toBe 1 + expect(editSession.getSelection().getBufferRange()).toEqual [[12,2], [12,2]] - interpreter.eval(editor, '1,$') - expect(editor.getSelections().length).toBe 1 - expect(editor.getSelection().getBufferRange()).toEqual [[0,0], [12,2]] + interpreter.eval('1,$', editSession) + expect(editSession.getSelections().length).toBe 1 + expect(editSession.getSelection().getBufferRange()).toEqual [[0,0], [12,2]] describe ".", -> describe "when a single selection", -> it 'maintains the current selection', -> - editor.clearSelections() - editor.setSelectedBufferRange([[1,1], [2,2]]) - interpreter.eval(editor, '.') - expect(editor.getSelection().getBufferRange()).toEqual [[1,1], [2,2]] + editSession.clearSelections() + editSession.setSelectedBufferRange([[1,1], [2,2]]) + interpreter.eval('.', editSession) + expect(editSession.getSelection().getBufferRange()).toEqual [[1,1], [2,2]] - editor.setSelectedBufferRange([[1,1], [2,2]]) - interpreter.eval(editor, '.,') - expect(editor.getSelection().getBufferRange()).toEqual [[1,1], [12,2]] + editSession.setSelectedBufferRange([[1,1], [2,2]]) + interpreter.eval('.,', editSession) + expect(editSession.getSelection().getBufferRange()).toEqual [[1,1], [12,2]] - editor.setSelectedBufferRange([[1,1], [2,2]]) - interpreter.eval(editor, ',.') - expect(editor.getSelection().getBufferRange()).toEqual [[0,0], [2,2]] + editSession.setSelectedBufferRange([[1,1], [2,2]]) + interpreter.eval(',.', editSession) + expect(editSession.getSelection().getBufferRange()).toEqual [[0,0], [2,2]] describe "with multiple selections", -> it "maintains the current selections", -> - preSelections = editor.getSelections() + preSelections = editSession.getSelections() expect(preSelections.length).toBe 3 [preRange1, preRange2, preRange3] = preSelections.map (s) -> s.getScreenRange() - interpreter.eval(editor, '.') + interpreter.eval('.', editSession) - selections = editor.getSelections() + selections = editSession.getSelections() expect(selections.length).toBe 3 [selection1, selection2, selection3] = selections expect(selection1.getScreenRange()).toEqual preRange1 @@ -79,72 +77,72 @@ describe "CommandInterpreter", -> describe "/regex/", -> beforeEach -> - editor.clearSelections() + editSession.clearSelections() it 'selects text matching regex after current selection', -> - editor.setSelectedBufferRange([[4,16], [4,20]]) - interpreter.eval(editor, '/pivot/') - expect(editor.getSelection().getBufferRange()).toEqual [[6,16], [6,21]] + editSession.setSelectedBufferRange([[4,16], [4,20]]) + interpreter.eval('/pivot/', editSession) + expect(editSession.getSelection().getBufferRange()).toEqual [[6,16], [6,21]] it 'does not require the trailing slash', -> - editor.setSelectedBufferRange([[4,16], [4,20]]) - interpreter.eval(editor, '/pivot') - expect(editor.getSelection().getBufferRange()).toEqual [[6,16], [6,21]] + editSession.setSelectedBufferRange([[4,16], [4,20]]) + interpreter.eval('/pivot', editSession) + expect(editSession.getSelection().getBufferRange()).toEqual [[6,16], [6,21]] it "searches from the end of each selection in the buffer", -> - editor.clearSelections() - editor.setSelectedBufferRange([[4,16], [4,20]]) - editor.addSelectionForBufferRange([[1,16], [2,20]]) - expect(editor.getSelections().length).toBe 2 - interpreter.eval(editor, '/pivot') - selections = editor.getSelections() + editSession.clearSelections() + editSession.setSelectedBufferRange([[4,16], [4,20]]) + editSession.addSelectionForBufferRange([[1,16], [2,20]]) + expect(editSession.getSelections().length).toBe 2 + interpreter.eval('/pivot', editSession) + selections = editSession.getSelections() expect(selections.length).toBe 2 expect(selections[0].getBufferRange()).toEqual [[3,8], [3,13]] expect(selections[1].getBufferRange()).toEqual [[6,16], [6,21]] it "wraps around to the beginning of the buffer, but doesn't infinitely loop if no matches are found", -> - editor.setSelectedBufferRange([[10, 0], [10,3]]) - interpreter.eval(editor, '/pivot') - expect(editor.getSelection().getBufferRange()).toEqual [[3,8], [3,13]] + editSession.setSelectedBufferRange([[10, 0], [10,3]]) + interpreter.eval('/pivot', editSession) + expect(editSession.getSelection().getBufferRange()).toEqual [[3,8], [3,13]] - interpreter.eval(editor, '/mike tyson') - expect(editor.getSelection().getBufferRange()).toEqual [[3,8], [3,13]] + interpreter.eval('/mike tyson', editSession) + expect(editSession.getSelection().getBufferRange()).toEqual [[3,8], [3,13]] it "searches in reverse when prefixed with a -", -> - editor.setSelectedBufferRange([[6, 16], [6, 22]]) - interpreter.eval(editor, '-/pivot') - expect(editor.getSelection().getBufferRange()).toEqual [[3,8], [3,13]] + editSession.setSelectedBufferRange([[6, 16], [6, 22]]) + interpreter.eval('-/pivot', editSession) + expect(editSession.getSelection().getBufferRange()).toEqual [[3,8], [3,13]] describe "address range", -> describe "when two addresses are specified", -> it "selects from the begining of the left address to the end of the right address", -> - interpreter.eval(editor, '4,7') - expect(editor.getSelections().length).toBe 1 - expect(editor.getSelection().getBufferRange()).toEqual [[3, 0], [7, 0]] + interpreter.eval('4,7', editSession) + expect(editSession.getSelections().length).toBe 1 + expect(editSession.getSelection().getBufferRange()).toEqual [[3, 0], [7, 0]] describe "when the left address is unspecified", -> it "selects from the begining of buffer to the end of the right address", -> - interpreter.eval(editor, ',7') - expect(editor.getSelections().length).toBe 1 - expect(editor.getSelection().getBufferRange()).toEqual [[0, 0], [7, 0]] + interpreter.eval(',7', editSession) + expect(editSession.getSelections().length).toBe 1 + expect(editSession.getSelection().getBufferRange()).toEqual [[0, 0], [7, 0]] describe "when the right address is unspecified", -> it "selects from the begining of left address to the end file", -> - interpreter.eval(editor, '4,') - expect(editor.getSelections().length).toBe 1 - expect(editor.getSelection().getBufferRange()).toEqual [[3, 0], [12, 2]] + interpreter.eval('4,', editSession) + expect(editSession.getSelections().length).toBe 1 + expect(editSession.getSelection().getBufferRange()).toEqual [[3, 0], [12, 2]] describe "when the neither address is specified", -> it "selects the entire file", -> - interpreter.eval(editor, ',') - expect(editor.getSelections().length).toBe 1 - expect(editor.getSelection().getBufferRange()).toEqual [[0, 0], [12, 2]] + interpreter.eval(',', editSession) + expect(editSession.getSelections().length).toBe 1 + expect(editSession.getSelection().getBufferRange()).toEqual [[0, 0], [12, 2]] describe "x/regex/", -> it "sets the current selection to every match of the regex in the current selection", -> - interpreter.eval(editor, '6,7 x/current/') + interpreter.eval('6,7 x/current/', editSession) - selections = editor.getSelections() + selections = editSession.getSelections() expect(selections.length).toBe 4 expect(selections[0].getBufferRange()).toEqual [[5,6], [5,13]] @@ -154,9 +152,9 @@ describe "CommandInterpreter", -> describe "when matching /$/", -> it "matches the end of each line in the selected region", -> - interpreter.eval(editor, '6,8 x/$/') + interpreter.eval('6,8 x/$/', editSession) - cursors = editor.getCursors() + cursors = editSession.getCursors() expect(cursors.length).toBe 3 expect(cursors[0].getBufferPosition()).toEqual [5, 30] @@ -164,12 +162,12 @@ describe "CommandInterpreter", -> expect(cursors[2].getBufferPosition()).toEqual [7, 5] it "loops through current selections and selects text matching the regex", -> - editor.setSelectedBufferRange [[3,0], [3,62]] - editor.addSelectionForBufferRange [[6,0], [6,65]] + editSession.setSelectedBufferRange [[3,0], [3,62]] + editSession.addSelectionForBufferRange [[6,0], [6,65]] - interpreter.eval(editor, 'x/current') + interpreter.eval('x/current', editSession) - selections = editor.getSelections() + selections = editSession.getSelections() expect(selections.length).toBe 4 expect(selections[0].getBufferRange()).toEqual [[3,31], [3,38]] @@ -179,35 +177,35 @@ describe "CommandInterpreter", -> describe "substitution", -> it "does nothing if there are no matches", -> - editor.setSelectedBufferRange([[6, 0], [6, 44]]) - interpreter.eval(editor, 's/not-in-text/foo/') + editSession.setSelectedBufferRange([[6, 0], [6, 44]]) + interpreter.eval('s/not-in-text/foo/', editSession) expect(buffer.lineForRow(6)).toBe ' current < pivot ? left.push(current) : right.push(current);' describe "when not global", -> describe "when there is a single selection", -> it "performs a single substitution within the current selection", -> - editor.setSelectedBufferRange([[6, 0], [6, 44]]) - interpreter.eval(editor, 's/current/foo/') + editSession.setSelectedBufferRange([[6, 0], [6, 44]]) + interpreter.eval('s/current/foo/', editSession) expect(buffer.lineForRow(6)).toBe ' foo < pivot ? left.push(current) : right.push(current);' describe "when there are multiple selections", -> it "performs a single substitutions within each of the selections", -> - editor.setSelectedBufferRange([[5, 0], [5, 20]]) - editor.addSelectionForBufferRange([[6, 0], [6, 44]]) + editSession.setSelectedBufferRange([[5, 0], [5, 20]]) + editSession.addSelectionForBufferRange([[6, 0], [6, 44]]) - interpreter.eval(editor, 's/current/foo/') + interpreter.eval('s/current/foo/', editSession) expect(buffer.lineForRow(5)).toBe ' foo = items.shift();' expect(buffer.lineForRow(6)).toBe ' foo < pivot ? left.push(current) : right.push(current);' describe "when global", -> it "performs a multiple substitutions within the current selection", -> - editor.setSelectedBufferRange([[6, 0], [6, 44]]) - interpreter.eval(editor, 's/current/foo/g') + editSession.setSelectedBufferRange([[6, 0], [6, 44]]) + interpreter.eval('s/current/foo/g', editSession) expect(buffer.lineForRow(6)).toBe ' foo < pivot ? left.push(foo) : right.push(current);' describe "when prefixed with an address", -> it "only makes substitutions within given lines", -> - interpreter.eval(editor, '4,6s/ /!/g') + interpreter.eval('4,6s/ /!/g', editSession) expect(buffer.lineForRow(2)).toBe ' if (items.length <= 1) return items;' expect(buffer.lineForRow(3)).toBe '!!!!var!pivot!=!items.shift(),!current,!left!=![],!right!=![];' expect(buffer.lineForRow(4)).toBe '!!!!while(items.length!>!0)!{' @@ -216,7 +214,7 @@ describe "CommandInterpreter", -> describe "when matching $", -> it "matches the end of each line and avoids infinitely looping on a zero-width match", -> - interpreter.eval(editor, ',s/$/!!!/g') + interpreter.eval(',s/$/!!!/g', editSession) expect(buffer.lineForRow(0)).toBe 'var quicksort = function () {!!!' expect(buffer.lineForRow(2)).toBe ' if (items.length <= 1) return items;!!!' expect(buffer.lineForRow(6)).toBe ' current < pivot ? left.push(current) : right.push(current);!!!' @@ -224,7 +222,7 @@ describe "CommandInterpreter", -> describe "when matching ^", -> it "matches the beginning of each line and avoids infinitely looping on a zero-width match", -> - interpreter.eval(editor, ',s/^/!!!/g') + interpreter.eval(',s/^/!!!/g', editSession) expect(buffer.lineForRow(0)).toBe '!!!var quicksort = function () {' expect(buffer.lineForRow(2)).toBe '!!! if (items.length <= 1) return items;' expect(buffer.lineForRow(6)).toBe '!!! current < pivot ? left.push(current) : right.push(current);' @@ -232,27 +230,27 @@ describe "CommandInterpreter", -> describe "when there are multiple selections", -> it "performs a multiple substitutions within each of the selections", -> - editor.setSelectedBufferRange([[5, 0], [5, 20]]) - editor.addSelectionForBufferRange([[6, 0], [6, 44]]) + editSession.setSelectedBufferRange([[5, 0], [5, 20]]) + editSession.addSelectionForBufferRange([[6, 0], [6, 44]]) - interpreter.eval(editor, 's/current/foo/g') + interpreter.eval('s/current/foo/g', editSession) expect(buffer.lineForRow(5)).toBe ' foo = items.shift();' expect(buffer.lineForRow(6)).toBe ' foo < pivot ? left.push(foo) : right.push(current);' describe "when prefixed with an address", -> it "restores the original selections upon completion if it is the last command", -> - editor.setSelectedBufferRanges([[[5, 0], [5, 20]], [[6, 0], [6, 44]]]) - interpreter.eval(editor, ',s/current/foo/g') - expect(editor.getSelectedBufferRanges()).toEqual [[[5, 0], [5, 16]], [[6, 0], [6, 36]]] + editSession.setSelectedBufferRanges([[[5, 0], [5, 20]], [[6, 0], [6, 44]]]) + interpreter.eval(',s/current/foo/g', editSession) + expect(editSession.getSelectedBufferRanges()).toEqual [[[5, 0], [5, 16]], [[6, 0], [6, 36]]] describe "when command selects folded text", -> it "unfolds lines that command selects", -> - editor.createFold(1, 9) - editor.createFold(5, 8) - editor.setSelectedBufferRange([[0,0], [0,0]]) + editSession.createFold(1, 9) + editSession.createFold(5, 8) + editSession.setSelectedBufferRange([[0,0], [0,0]]) - interpreter.eval(editor, '/push/') - expect(editor.getSelection().getBufferRange()).toEqual [[6,29], [6,33]] - expect(editor.lineForScreenRow(1).fold).toBeUndefined() - expect(editor.lineForScreenRow(5).fold).toBeUndefined() - expect(editor.lineForScreenRow(6).text).toBe buffer.lineForRow(6) + interpreter.eval('/push/', editSession) + expect(editSession.getSelection().getBufferRange()).toEqual [[6,29], [6,33]] + expect(editSession.lineForScreenRow(1).fold).toBeUndefined() + expect(editSession.lineForScreenRow(5).fold).toBeUndefined() + expect(editSession.lineForScreenRow(6).text).toBe buffer.lineForRow(6) diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index 8f42392f2..41d3dfe0e 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -162,6 +162,9 @@ class RootView extends View else @panes.find('.editor:first').view() + getActiveEditSession: -> + @getActiveEditor()?.activeEditSession + focusNextPane: -> panes = @panes.find('.pane') currentIndex = panes.toArray().indexOf(@getFocusedPane()[0]) diff --git a/src/extensions/command-panel/command-interpreter.coffee b/src/extensions/command-panel/command-interpreter.coffee index 0190a5e71..eb328c9b6 100644 --- a/src/extensions/command-panel/command-interpreter.coffee +++ b/src/extensions/command-panel/command-interpreter.coffee @@ -3,16 +3,16 @@ PEG = require 'pegjs' module.exports = class CommandInterpreter - constructor: -> + constructor: (@project) -> @parser = PEG.buildParser(fs.read(require.resolve 'command-panel/commands.pegjs')) - eval: (editor, string) -> + eval: (string, activeEditSession) -> compositeCommand = @parser.parse(string) @lastRelativeAddress = compositeCommand if compositeCommand.isRelativeAddress() - compositeCommand.execute(editor) + compositeCommand.execute(@project, activeEditSession) - repeatRelativeAddress: (editor) -> - @lastRelativeAddress?.execute(editor) + repeatRelativeAddress: (activeEditSession) -> + @lastRelativeAddress?.execute(@project, activeEditSession) - repeatRelativeAddressInReverse: (editor) -> - @lastRelativeAddress?.reverse().execute(editor) + repeatRelativeAddressInReverse: (activeEditSession) -> + @lastRelativeAddress?.reverse().execute(@project, activeEditSession) diff --git a/src/extensions/command-panel/command-panel.coffee b/src/extensions/command-panel/command-panel.coffee index 242374edc..677be9453 100644 --- a/src/extensions/command-panel/command-panel.coffee +++ b/src/extensions/command-panel/command-panel.coffee @@ -35,7 +35,7 @@ class CommandPanel extends View historyIndex: 0 initialize: (@rootView)-> - @commandInterpreter = new CommandInterpreter() + @commandInterpreter = new CommandInterpreter(@rootView.project) @history = [] @rootView.on 'command-panel:toggle', => @toggle() @@ -64,7 +64,7 @@ class CommandPanel extends View execute: (command = @miniEditor.getText()) -> try - @commandInterpreter.eval(@rootView.getActiveEditor(), command) + @commandInterpreter.eval(command, @rootView.getActiveEditSession()) catch error if error instanceof SyntaxError @flashError() @@ -87,10 +87,10 @@ class CommandPanel extends View @miniEditor.setText(@history[@historyIndex] or '') repeatRelativeAddress: -> - @commandInterpreter.repeatRelativeAddress(@rootView.getActiveEditor()) + @commandInterpreter.repeatRelativeAddress(@rootView.getActiveEditSession()) repeatRelativeAddressInReverse: -> - @commandInterpreter.repeatRelativeAddressInReverse(@rootView.getActiveEditor()) + @commandInterpreter.repeatRelativeAddressInReverse(@rootView.getActiveEditSession()) setSelectionAsLastRelativeAddress: -> selection = @rootView.getActiveEditor().getSelectedText() diff --git a/src/extensions/command-panel/commands/address-range.coffee b/src/extensions/command-panel/commands/address-range.coffee index e8f3c68dd..45cb5afda 100644 --- a/src/extensions/command-panel/commands/address-range.coffee +++ b/src/extensions/command-panel/commands/address-range.coffee @@ -5,8 +5,8 @@ module.exports = class AddressRange extends Address constructor: (@startAddress, @endAddress) -> - getRange: (editor, currentRange) -> - new Range(@startAddress.getRange(editor, currentRange).start, @endAddress.getRange(editor, currentRange).end) + getRange: (buffer, range) -> + new Range(@startAddress.getRange(buffer, range).start, @endAddress.getRange(buffer, range).end) isRelative: -> @startAddress.isRelative() and @endAddress.isRelative() diff --git a/src/extensions/command-panel/commands/address.coffee b/src/extensions/command-panel/commands/address.coffee index e6ef1e11b..6f8c6fdb0 100644 --- a/src/extensions/command-panel/commands/address.coffee +++ b/src/extensions/command-panel/commands/address.coffee @@ -2,7 +2,7 @@ Command = require 'command-panel/commands/command' module.exports = class Address extends Command - execute: (editor, currentRange) -> - [@getRange(editor, currentRange)] + execute: (project, buffer, range) -> + [@getRange(buffer, range)] isAddress: -> true diff --git a/src/extensions/command-panel/commands/composite-command.coffee b/src/extensions/command-panel/commands/composite-command.coffee index 32c3262e0..5b1db18a1 100644 --- a/src/extensions/command-panel/commands/composite-command.coffee +++ b/src/extensions/command-panel/commands/composite-command.coffee @@ -4,19 +4,17 @@ module.exports = class CompositeCommand constructor: (@subcommands) -> - execute: (editor) -> - currentRanges = editor.getSelectedBufferRanges() + execute: (project, activeEditSession) -> + currentRanges = activeEditSession.getSelectedBufferRanges() + for command in @subcommands newRanges = [] for range in currentRanges - newRanges.push(command.execute(editor, range)...) + newRanges.push(command.execute(project, activeEditSession.buffer, range)...) currentRanges = newRanges unless command.preserveSelections - for range in currentRanges - for row in [range.start.row..range.end.row] - editor.destroyFoldsContainingBufferRow(row) - editor.setSelectedBufferRanges(currentRanges) + activeEditSession.setSelectedBufferRanges(currentRanges) reverse: -> new CompositeCommand(@subcommands.map (command) -> command.reverse()) diff --git a/src/extensions/command-panel/commands/current-selection-address.coffee b/src/extensions/command-panel/commands/current-selection-address.coffee index fe4b67f2b..5a230f842 100644 --- a/src/extensions/command-panel/commands/current-selection-address.coffee +++ b/src/extensions/command-panel/commands/current-selection-address.coffee @@ -3,7 +3,7 @@ Range = require 'range' module.exports = class CurrentSelectionAddress extends Address - getRange: (editor, currentRange) -> - currentRange + getRange: (buffer, range) -> + range isRelative: -> true diff --git a/src/extensions/command-panel/commands/eof-address.coffee b/src/extensions/command-panel/commands/eof-address.coffee index c4f41b577..f84213420 100644 --- a/src/extensions/command-panel/commands/eof-address.coffee +++ b/src/extensions/command-panel/commands/eof-address.coffee @@ -3,9 +3,8 @@ Range = require 'range' module.exports = class EofAddress extends Address - getRange: (editor) -> - lastRow = editor.getLastBufferRow() - column = editor.lineLengthForBufferRow(lastRow) - new Range([lastRow, column], [lastRow, column]) + getRange: (buffer, range) -> + eof = buffer.getEofPosition() + new Range(eof, eof) isRelative: -> false diff --git a/src/extensions/command-panel/commands/regex-address.coffee b/src/extensions/command-panel/commands/regex-address.coffee index 8fbf13b78..37a4737ed 100644 --- a/src/extensions/command-panel/commands/regex-address.coffee +++ b/src/extensions/command-panel/commands/regex-address.coffee @@ -10,25 +10,25 @@ class RegexAddress extends Address @isReversed = isReversed @regex = new RegExp(pattern) - getRange: (editor, currentRange) -> - rangeBefore = new Range([0, 0], currentRange.start) - rangeAfter = new Range(currentRange.end, editor.getEofPosition()) + getRange: (buffer, range) -> + rangeBefore = new Range([0, 0], range.start) + rangeAfter = new Range(range.end, buffer.getEofPosition()) rangeToSearch = if @isReversed then rangeBefore else rangeAfter rangeToReturn = null scanMethodName = if @isReversed then "backwardsScanInRange" else "scanInRange" - editor[scanMethodName] @regex, rangeToSearch, (match, range) -> + buffer[scanMethodName] @regex, rangeToSearch, (match, range) -> rangeToReturn = range if rangeToReturn rangeToReturn else rangeToSearch = if @isReversed then rangeAfter else rangeBefore - editor[scanMethodName] @regex, rangeToSearch, (match, range) -> + buffer[scanMethodName] @regex, rangeToSearch, (match, range) -> rangeToReturn = range - rangeToReturn or currentRange + rangeToReturn or range isRelative: -> true diff --git a/src/extensions/command-panel/commands/select-all-matches.coffee b/src/extensions/command-panel/commands/select-all-matches.coffee index 0c8163e5f..1e390df8e 100644 --- a/src/extensions/command-panel/commands/select-all-matches.coffee +++ b/src/extensions/command-panel/commands/select-all-matches.coffee @@ -8,8 +8,8 @@ class SelectAllMatches extends Command constructor: (pattern) -> @regex = new RegExp(pattern, 'g') - execute: (editor, currentRange) -> + execute: (project, buffer, range) -> rangesToSelect = [] - editor.scanInRange @regex, currentRange, (match, range) -> - rangesToSelect.push(range) + buffer.scanInRange @regex, range, (match, matchRange) -> + rangesToSelect.push(matchRange) rangesToSelect diff --git a/src/extensions/command-panel/commands/substitution.coffee b/src/extensions/command-panel/commands/substitution.coffee index d2c7f7c2d..f56803839 100644 --- a/src/extensions/command-panel/commands/substitution.coffee +++ b/src/extensions/command-panel/commands/substitution.coffee @@ -10,7 +10,7 @@ class Substitution extends Command @replacementText = replacementText @regex = new RegExp(pattern, options.join('')) - execute: (editor, currentRange) -> - editor.scanInRange @regex, currentRange, (match, matchRange, { replace }) => + execute: (project, buffer, range) -> + buffer.scanInRange @regex, range, (match, matchRange, { replace }) => replace(@replacementText) - [currentRange] + [range] diff --git a/src/extensions/command-panel/commands/zero-address.coffee b/src/extensions/command-panel/commands/zero-address.coffee index c8c2dfb69..f40b6d377 100644 --- a/src/extensions/command-panel/commands/zero-address.coffee +++ b/src/extensions/command-panel/commands/zero-address.coffee @@ -3,7 +3,7 @@ Range = require 'range' module.exports = class ZeroAddress extends Address - getRange: (editor) -> + getRange: -> new Range([0, 0], [0, 0]) isRelative: -> false From c7b71353880acc4387d86ec76073ca41dd34312c Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Fri, 13 Jul 2012 17:16:53 -0600 Subject: [PATCH 28/97] Temporary fix: When editor is resized, adjust width of rendered lines. We do this to ensure that the lines aren't longer than the scroll view if they don't have to be. We really should use min-width instead because it's automatic. Also, trigger window resize when we make the editor narrower. --- spec/app/editor-spec.coffee | 1 + spec/spec-helper.coffee | 1 + src/app/editor.coffee | 4 +++- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/spec/app/editor-spec.coffee b/spec/app/editor-spec.coffee index 140c80088..6651fb6c1 100644 --- a/spec/app/editor-spec.coffee +++ b/spec/app/editor-spec.coffee @@ -1241,6 +1241,7 @@ describe "Editor", -> expect(editor.renderedLines.find(".line:last").text()).toBe buffer.lineForRow(6) it "increases the width of the rendered lines element to be either the width of the longest line or the width of the scrollView (whichever is longer)", -> + setEditorWidthInChars(editor, editor.maxScreenLineLength()) widthBefore = editor.renderedLines.width() expect(widthBefore).toBe editor.scrollView.width() buffer.change([[12,0], [12,0]], [1..50].join('')) diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 62c51b39d..eb82d2c3b 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -142,6 +142,7 @@ window.tokensText = (tokens) -> window.setEditorWidthInChars = (editor, widthInChars, charWidth=editor.charWidth) -> editor.width(charWidth * widthInChars + editor.renderedLines.position().left) + $(window).trigger 'resize' # update width of editor's on-screen lines window.setEditorHeightInLines = (editor, heightInChars, charHeight=editor.lineHeight) -> editor.height(charHeight * heightInChars + editor.renderedLines.position().top) diff --git a/src/app/editor.coffee b/src/app/editor.coffee index 7fe8edba2..9a31078da 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -337,7 +337,9 @@ class Editor extends View @calculateDimensions() @hiddenInput.width(@charWidth) @setSoftWrapColumn() if @activeEditSession.getSoftWrap() - $(window).on "resize.editor#{@id}", => @updateRenderedLines() + $(window).on "resize.editor#{@id}", => + @updateRenderedLines() + @adjustWidthOfRenderedLines() # TODO: This should happen automatically with CSS @focus() if @isFocused @renderWhenAttached() From 9eae88e97bd698372472e837ba9ca72848a87c1d Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Fri, 13 Jul 2012 17:18:10 -0600 Subject: [PATCH 29/97] Anchors and AnchorRanges are added to the buffer. Also: Destroying an AnchorRange removes it from the EditSession / Buffer. --- src/app/anchor-range.coffee | 6 +++++- src/app/anchor.coffee | 14 ++++++++++---- src/app/buffer.coffee | 27 +++++++++++++++++++++++++++ src/app/edit-session.coffee | 9 ++++++--- 4 files changed, 48 insertions(+), 8 deletions(-) diff --git a/src/app/anchor-range.coffee b/src/app/anchor-range.coffee index 6340945e7..b193c4690 100644 --- a/src/app/anchor-range.coffee +++ b/src/app/anchor-range.coffee @@ -4,8 +4,10 @@ module.exports = class AnchorRange start: null end: null + buffer: null + editSession: null # optional - constructor: (@editSession, bufferRange) -> + constructor: (bufferRange, @buffer, @editSession) -> bufferRange = Range.fromObject(bufferRange) @startAnchor = @editSession.addAnchorAtBufferPosition(bufferRange.start, ignoreEqual: true) @endAnchor = @editSession.addAnchorAtBufferPosition(bufferRange.end) @@ -22,3 +24,5 @@ class AnchorRange destroy: -> @startAnchor.destroy() @endAnchor.destroy() + @buffer.removeAnchorRange(this) + @editSession?.removeAnchorRange(this) diff --git a/src/app/anchor.coffee b/src/app/anchor.coffee index 69fad7dcd..a0da28ea4 100644 --- a/src/app/anchor.coffee +++ b/src/app/anchor.coffee @@ -4,12 +4,17 @@ _ = require 'underscore' module.exports = class Anchor - editor: null + buffer: null + editSession: null # optional bufferPosition: null screenPosition: null + ignoreEqual: false + strong: false - constructor: (@editSession, options = {}) -> - { @ignoreEqual, @strong } = options + constructor: (@buffer, options = {}) -> + + throw new Error("no edit session!") unless options.editSession + { @editSession, @ignoreEqual, @strong } = options handleBufferChange: (e) -> { oldRange, newRange } = e @@ -69,7 +74,8 @@ class Anchor @setScreenPosition(screenPosition, bufferChange: options.bufferChange, clip: false, assignBufferPosition: false) destroy: -> - @editSession.removeAnchor(this) + @buffer.removeAnchor(this) + @editSession?.removeAnchor(this) @trigger 'destroy' @off() diff --git a/src/app/buffer.coffee b/src/app/buffer.coffee index 21aa60e57..2b7c61231 100644 --- a/src/app/buffer.coffee +++ b/src/app/buffer.coffee @@ -6,6 +6,8 @@ Range = require 'range' EventEmitter = require 'event-emitter' UndoManager = require 'undo-manager' BufferChangeOperation = require 'buffer-change-operation' +Anchor = require 'anchor' +AnchorRange = require 'anchor-range' module.exports = class Buffer @@ -14,9 +16,13 @@ class Buffer modified: null lines: null file: null + anchors: null + anchorRanges: null constructor: (path) -> @id = @constructor.idCounter++ + @anchors = [] + @anchorRanges = [] @lines = [''] if path @@ -173,6 +179,27 @@ class Buffer isModified: -> @modified + addAnchor: (options) -> + anchor = new Anchor(this, options) + @anchors.push(anchor) + anchor + + addAnchorAtPosition: (position, options) -> + anchor = @addAnchor(options) + anchor.setBufferPosition(position) + anchor + + addAnchorRange: (range, editSession) -> + anchorRange = new AnchorRange(range, this, editSession) + @anchorRanges.push(anchorRange) + anchorRange + + removeAnchor: (anchor) -> + _.remove(@anchors, anchor) + + removeAnchorRange: (anchorRange) -> + _.remove(@anchorRanges, anchorRange) + matchesInCharacterRange: (regex, startIndex, endIndex) -> text = @getText() matches = [] diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index a3466ff56..5a1db824f 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -258,8 +258,8 @@ class EditSession getAnchors: -> new Array(@anchors...) - addAnchor: (options) -> - anchor = new Anchor(this, options) + addAnchor: (options={}) -> + anchor = @buffer.addAnchor(_.extend({editSession: this}, options)) @anchors.push(anchor) anchor @@ -269,13 +269,16 @@ class EditSession anchor addAnchorRange: (range) -> - anchorRange = new AnchorRange(this, range) + anchorRange = @buffer.addAnchorRange(range, this) @anchorRanges.push(anchorRange) anchorRange removeAnchor: (anchor) -> _.remove(@anchors, anchor) + removeAnchorRange: (anchorRange) -> + _.remove(@anchorRanges, anchorRange) + getCursors: -> new Array(@cursors...) getCursor: (index=0) -> From 926067164d398893fc3da2510eef900ff688ae7d Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Fri, 13 Jul 2012 17:42:41 -0600 Subject: [PATCH 30/97] Buffer updates anchors when it changes instead of EditSession --- spec/app/buffer-spec.coffee | 20 ++++++++++++++++++++ src/app/anchor.coffee | 5 ++--- src/app/buffer-change-operation.coffee | 5 ++++- src/app/buffer.coffee | 11 +++++++++++ src/app/cursor.coffee | 2 +- src/app/edit-session.coffee | 14 +++----------- 6 files changed, 41 insertions(+), 16 deletions(-) diff --git a/spec/app/buffer-spec.coffee b/spec/app/buffer-spec.coffee index 932506576..7ca4709e0 100644 --- a/spec/app/buffer-spec.coffee +++ b/spec/app/buffer-spec.coffee @@ -533,3 +533,23 @@ describe 'Buffer', -> expect(buffer.positionForCharacterIndex(30)).toEqual [1, 0] expect(buffer.positionForCharacterIndex(61)).toEqual [2, 0] expect(buffer.positionForCharacterIndex(408)).toEqual [12, 2] + + describe "anchors", -> + [anchor, destroyHandler] = [] + + beforeEach -> + destroyHandler = jasmine.createSpy("destroyHandler") + anchor = buffer.addAnchorAtPosition([4, 25]) + anchor.on 'destroy', destroyHandler + + describe "when a buffer change precedes an anchor", -> + it "moves the anchor in accordance with the change", -> + buffer.delete([[3, 0], [4, 10]]) + expect(anchor.getBufferPosition()).toEqual [3, 15] + expect(destroyHandler).not.toHaveBeenCalled() + + describe "when a buffer change surrounds an anchor", -> + it "destroys the anchor", -> + buffer.delete([[3, 0], [5, 0]]) + expect(destroyHandler).toHaveBeenCalled() + expect(buffer.getAnchors().indexOf(anchor)).toBe -1 diff --git a/src/app/anchor.coffee b/src/app/anchor.coffee index a0da28ea4..2755974c0 100644 --- a/src/app/anchor.coffee +++ b/src/app/anchor.coffee @@ -12,8 +12,6 @@ class Anchor strong: false constructor: (@buffer, options = {}) -> - - throw new Error("no edit session!") unless options.editSession { @editSession, @ignoreEqual, @strong } = options handleBufferChange: (e) -> @@ -48,7 +46,7 @@ class Anchor setBufferPosition: (position, options={}) -> @bufferPosition = Point.fromObject(position) clip = options.clip ? true - @bufferPosition = @editSession.clipBufferPosition(@bufferPosition) if clip + @bufferPosition = @buffer.clipPosition(@bufferPosition) if clip @refreshScreenPosition(options) getScreenPosition: -> @@ -70,6 +68,7 @@ class Anchor @trigger 'change-screen-position', @screenPosition, bufferChange: options.bufferChange refreshScreenPosition: (options={}) -> + return unless @editSession screenPosition = @editSession.screenPositionForBufferPosition(@bufferPosition, options) @setScreenPosition(screenPosition, bufferChange: options.bufferChange, clip: false, assignBufferPosition: false) diff --git a/src/app/buffer-change-operation.coffee b/src/app/buffer-change-operation.coffee index 2aa0e6098..92d6b4600 100644 --- a/src/app/buffer-change-operation.coffee +++ b/src/app/buffer-change-operation.coffee @@ -38,7 +38,10 @@ class BufferChangeOperation newTextLines[lastLineIndex] += suffix @buffer.replaceLines(oldRange.start.row, oldRange.end.row, newTextLines) - @buffer.trigger 'change', { oldRange, newRange, oldText, newText } + + event = { oldRange, newRange, oldText, newText } + @buffer.trigger 'change', event + anchor.handleBufferChange(event) for anchor in @buffer.getAnchors() newRange calculateNewRange: (oldRange, newText) -> diff --git a/src/app/buffer.coffee b/src/app/buffer.coffee index 2b7c61231..db253bd57 100644 --- a/src/app/buffer.coffee +++ b/src/app/buffer.coffee @@ -140,6 +140,15 @@ class Buffer operation = new BufferChangeOperation({buffer: this, oldRange, newText}) @pushOperation(operation) + clipPosition: (position) -> + { row, column } = Point.fromObject(position) + row = 0 if row < 0 + column = 0 if column < 0 + row = Math.min(@getLastRow(), row) + column = Math.min(@lineLengthForRow(row), column) + + new Point(row, column) + prefixAndSuffixForRange: (range) -> prefix: @lines[range.start.row][0...range.start.column] suffix: @lines[range.end.row][range.end.column..] @@ -179,6 +188,8 @@ class Buffer isModified: -> @modified + getAnchors: -> new Array(@anchors...) + addAnchor: (options) -> anchor = new Anchor(this, options) @anchors.push(anchor) diff --git a/src/app/cursor.coffee b/src/app/cursor.coffee index a7f3a736f..af26c65c0 100644 --- a/src/app/cursor.coffee +++ b/src/app/cursor.coffee @@ -18,7 +18,7 @@ class Cursor @setBufferPosition(bufferPosition) if bufferPosition destroy: -> - @editSession.removeAnchor(@anchor) + @anchor.destroy() @editSession.removeCursor(this) @trigger 'destroy' diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index 5a1db824f..3b5a2c1e6 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -45,9 +45,7 @@ class EditSession @buffer.on "path-change.edit-session-#{@id}", => @trigger 'buffer-path-change' - @buffer.on "change.edit-session-#{@id}", (e) => - anchor.handleBufferChange(e) for anchor in @getAnchors() - @mergeCursors() + @buffer.on "change.edit-session-#{@id}", (e) => @mergeCursors() @displayBuffer.on "change.edit-session-#{@id}", (e) => @trigger 'screen-lines-change', e @@ -89,14 +87,8 @@ class EditSession getSoftWrap: -> @softWrap setSoftWrap: (@softWrap) -> - clipBufferPosition: (bufferPosition, options) -> - { row, column } = Point.fromObject(bufferPosition) - row = 0 if row < 0 - column = 0 if column < 0 - row = Math.min(@buffer.getLastRow(), row) - column = Math.min(@buffer.lineLengthForRow(row), column) - - new Point(row, column) + clipBufferPosition: (bufferPosition) -> + @buffer.clipPosition(bufferPosition) getFileExtension: -> @buffer.getExtension() getPath: -> @buffer.getPath() From fd24b82d4784cd950621e1966e529761919dedb3 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Fri, 13 Jul 2012 18:30:10 -0600 Subject: [PATCH 31/97] WIP: Converting to operations, but substitution operations need anchor ranges --- spec/extensions/command-interpreter-spec.coffee | 14 +------------- src/app/edit-session.coffee | 6 +++++- .../command-panel/commands/address.coffee | 5 +++-- .../commands/composite-command.coffee | 15 +++++++-------- .../commands/select-all-matches.coffee | 10 +++++----- .../command-panel/commands/substitution.coffee | 13 ++++++++++--- src/extensions/command-panel/operation.coffee | 11 +++++++++++ 7 files changed, 42 insertions(+), 32 deletions(-) create mode 100644 src/extensions/command-panel/operation.coffee diff --git a/spec/extensions/command-interpreter-spec.coffee b/spec/extensions/command-interpreter-spec.coffee index c84bd7ea6..2b30664fb 100644 --- a/spec/extensions/command-interpreter-spec.coffee +++ b/spec/extensions/command-interpreter-spec.coffee @@ -2,7 +2,7 @@ CommandInterpreter = require 'command-panel/command-interpreter' Buffer = require 'buffer' EditSession = require 'edit-session' -describe "CommandInterpreter", -> +fdescribe "CommandInterpreter", -> [interpreter, editSession, buffer] = [] beforeEach -> @@ -242,15 +242,3 @@ describe "CommandInterpreter", -> editSession.setSelectedBufferRanges([[[5, 0], [5, 20]], [[6, 0], [6, 44]]]) interpreter.eval(',s/current/foo/g', editSession) expect(editSession.getSelectedBufferRanges()).toEqual [[[5, 0], [5, 16]], [[6, 0], [6, 36]]] - - describe "when command selects folded text", -> - it "unfolds lines that command selects", -> - editSession.createFold(1, 9) - editSession.createFold(5, 8) - editSession.setSelectedBufferRange([[0,0], [0,0]]) - - interpreter.eval('/push/', editSession) - expect(editSession.getSelection().getBufferRange()).toEqual [[6,29], [6,33]] - expect(editSession.lineForScreenRow(1).fold).toBeUndefined() - expect(editSession.lineForScreenRow(5).fold).toBeUndefined() - expect(editSession.lineForScreenRow(6).text).toBe buffer.lineForRow(6) diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index 3b5a2c1e6..9eecf6a57 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -302,13 +302,14 @@ class EditSession addSelectionForBufferRange: (bufferRange, options) -> @addCursor().selection.setBufferRange(bufferRange, options) + @mergeIntersectingSelections() setSelectedBufferRange: (bufferRange, options) -> @setSelectedBufferRanges([bufferRange], options) setSelectedBufferRanges: (bufferRanges, options={}) -> throw new Error("Passed an empty array to setSelectedBufferRanges") unless bufferRanges.length - selection.destroy() for selection in @getSelections() + @clearAllSelections() for bufferRange, i in bufferRanges bufferRange = Range.fromObject(bufferRange) unless options.preserveFolds @@ -326,6 +327,9 @@ class EditSession selection.destroy() lastSelection.clear() + clearAllSelections: -> + selection.destroy() for selection in @getSelections() + getSelections: -> new Array(@selections...) getSelection: (index) -> diff --git a/src/extensions/command-panel/commands/address.coffee b/src/extensions/command-panel/commands/address.coffee index 6f8c6fdb0..594d4a52d 100644 --- a/src/extensions/command-panel/commands/address.coffee +++ b/src/extensions/command-panel/commands/address.coffee @@ -1,8 +1,9 @@ Command = require 'command-panel/commands/command' +Operation = require 'command-panel/operation' module.exports = class Address extends Command - execute: (project, buffer, range) -> - [@getRange(buffer, range)] + compile: (project, buffer, range) -> + [new Operation(buffer: buffer, bufferRange: @getRange(buffer, range))] isAddress: -> true diff --git a/src/extensions/command-panel/commands/composite-command.coffee b/src/extensions/command-panel/commands/composite-command.coffee index 5b1db18a1..3916d5a4a 100644 --- a/src/extensions/command-panel/commands/composite-command.coffee +++ b/src/extensions/command-panel/commands/composite-command.coffee @@ -4,17 +4,16 @@ module.exports = class CompositeCommand constructor: (@subcommands) -> - execute: (project, activeEditSession) -> - currentRanges = activeEditSession.getSelectedBufferRanges() - + execute: (project, editSession) -> + currentRanges = editSession.getSelectedBufferRanges() for command in @subcommands - newRanges = [] + operations = [] for range in currentRanges - newRanges.push(command.execute(project, activeEditSession.buffer, range)...) - currentRanges = newRanges + operations.push(command.compile(project, editSession.buffer, range)...) + currentRanges = operations.map (o) -> o.getBufferRange() - unless command.preserveSelections - activeEditSession.setSelectedBufferRanges(currentRanges) + editSession.clearAllSelections() unless command.preserveSelections + operation.execute(editSession) for operation in operations reverse: -> new CompositeCommand(@subcommands.map (command) -> command.reverse()) diff --git a/src/extensions/command-panel/commands/select-all-matches.coffee b/src/extensions/command-panel/commands/select-all-matches.coffee index 1e390df8e..471646f4b 100644 --- a/src/extensions/command-panel/commands/select-all-matches.coffee +++ b/src/extensions/command-panel/commands/select-all-matches.coffee @@ -1,5 +1,5 @@ Command = require 'command-panel/commands/command' -Range = require 'range' +Operation = require 'command-panel/operation' module.exports = class SelectAllMatches extends Command @@ -8,8 +8,8 @@ class SelectAllMatches extends Command constructor: (pattern) -> @regex = new RegExp(pattern, 'g') - execute: (project, buffer, range) -> - rangesToSelect = [] + compile: (project, buffer, range) -> + operations = [] buffer.scanInRange @regex, range, (match, matchRange) -> - rangesToSelect.push(matchRange) - rangesToSelect + operations.push(new Operation(buffer: buffer, bufferRange: matchRange)) + operations diff --git a/src/extensions/command-panel/commands/substitution.coffee b/src/extensions/command-panel/commands/substitution.coffee index f56803839..9940c1ed1 100644 --- a/src/extensions/command-panel/commands/substitution.coffee +++ b/src/extensions/command-panel/commands/substitution.coffee @@ -1,4 +1,5 @@ Command = require 'command-panel/commands/command' +Operation = require 'command-panel/operation' module.exports = class Substitution extends Command @@ -10,7 +11,13 @@ class Substitution extends Command @replacementText = replacementText @regex = new RegExp(pattern, options.join('')) - execute: (project, buffer, range) -> + compile: (project, buffer, range) -> + operations = [] buffer.scanInRange @regex, range, (match, matchRange, { replace }) => - replace(@replacementText) - [range] + operations.push(new Operation( + buffer: buffer, + bufferRange: matchRange, + newText: @replacementText + preserveSelection: true + )) + operations diff --git a/src/extensions/command-panel/operation.coffee b/src/extensions/command-panel/operation.coffee new file mode 100644 index 000000000..39d68b231 --- /dev/null +++ b/src/extensions/command-panel/operation.coffee @@ -0,0 +1,11 @@ +module.exports = +class Operation + constructor: ({@buffer, bufferRange, @newText, @preserveSelection}) -> + @anchorRange = @buffer.addAnchorRange(bufferRange) + + + getBufferRange: -> @bufferRange + + execute: (editSession) -> + @buffer.change(@getBufferRange(), @newText) if @newText + editSession.addSelectionForBufferRange(@getBufferRange()) unless @preserveSelection From 075968e42c05134b9b2638e46dd1e14a83599318 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 13 Jul 2012 19:46:08 -0600 Subject: [PATCH 32/97] Associate compiled operations with anchor ranges. All tests pass again. This allows them to update their target range in the face of upstream changes. --- spec/extensions/command-interpreter-spec.coffee | 5 +++-- src/app/anchor-range.coffee | 4 ++-- src/app/edit-session.coffee | 5 +++++ .../command-panel/commands/composite-command.coffee | 5 ++++- src/extensions/command-panel/operation.coffee | 7 +++++-- 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/spec/extensions/command-interpreter-spec.coffee b/spec/extensions/command-interpreter-spec.coffee index 2b30664fb..c961838e2 100644 --- a/spec/extensions/command-interpreter-spec.coffee +++ b/spec/extensions/command-interpreter-spec.coffee @@ -2,8 +2,8 @@ CommandInterpreter = require 'command-panel/command-interpreter' Buffer = require 'buffer' EditSession = require 'edit-session' -fdescribe "CommandInterpreter", -> - [interpreter, editSession, buffer] = [] +describe "CommandInterpreter", -> + [interpreter, editSession, buffer, anchorCountBefore] = [] beforeEach -> interpreter = new CommandInterpreter(fixturesProject) @@ -12,6 +12,7 @@ fdescribe "CommandInterpreter", -> afterEach -> editSession.destroy() + expect(buffer.getAnchors().length).toBe 0 describe "addresses", -> beforeEach -> diff --git a/src/app/anchor-range.coffee b/src/app/anchor-range.coffee index b193c4690..54609a67f 100644 --- a/src/app/anchor-range.coffee +++ b/src/app/anchor-range.coffee @@ -9,8 +9,8 @@ class AnchorRange constructor: (bufferRange, @buffer, @editSession) -> bufferRange = Range.fromObject(bufferRange) - @startAnchor = @editSession.addAnchorAtBufferPosition(bufferRange.start, ignoreEqual: true) - @endAnchor = @editSession.addAnchorAtBufferPosition(bufferRange.end) + @startAnchor = @buffer.addAnchorAtPosition(bufferRange.start, ignoreEqual: true) + @endAnchor = @buffer.addAnchorAtPosition(bufferRange.end) getBufferRange: -> new Range(@startAnchor.getBufferPosition(), @endAnchor.getBufferPosition()) diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index 9eecf6a57..ca4bd2e27 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -57,6 +57,8 @@ class EditSession @displayBuffer.off ".edit-session-#{@id}" @displayBuffer.destroy() @project.removeEditSession(this) + anchor.destroy() for anchor in @getAnchors() + anchorRange.destroy() for anchorRange in @getAnchorRanges() serialize: -> buffer: @buffer.getPath() @@ -250,6 +252,9 @@ class EditSession getAnchors: -> new Array(@anchors...) + getAnchorRanges: -> + new Array(@anchorRanges...) + addAnchor: (options={}) -> anchor = @buffer.addAnchor(_.extend({editSession: this}, options)) @anchors.push(anchor) diff --git a/src/extensions/command-panel/commands/composite-command.coffee b/src/extensions/command-panel/commands/composite-command.coffee index 3916d5a4a..2ae072f50 100644 --- a/src/extensions/command-panel/commands/composite-command.coffee +++ b/src/extensions/command-panel/commands/composite-command.coffee @@ -7,13 +7,16 @@ class CompositeCommand execute: (project, editSession) -> currentRanges = editSession.getSelectedBufferRanges() for command in @subcommands + operations?.forEach (o) -> o.destroy() operations = [] for range in currentRanges operations.push(command.compile(project, editSession.buffer, range)...) currentRanges = operations.map (o) -> o.getBufferRange() editSession.clearAllSelections() unless command.preserveSelections - operation.execute(editSession) for operation in operations + for operation in operations + operation.execute(editSession) + operation.destroy() reverse: -> new CompositeCommand(@subcommands.map (command) -> command.reverse()) diff --git a/src/extensions/command-panel/operation.coffee b/src/extensions/command-panel/operation.coffee index 39d68b231..be7716725 100644 --- a/src/extensions/command-panel/operation.coffee +++ b/src/extensions/command-panel/operation.coffee @@ -3,9 +3,12 @@ class Operation constructor: ({@buffer, bufferRange, @newText, @preserveSelection}) -> @anchorRange = @buffer.addAnchorRange(bufferRange) - - getBufferRange: -> @bufferRange + getBufferRange: -> + @anchorRange.getBufferRange() execute: (editSession) -> @buffer.change(@getBufferRange(), @newText) if @newText editSession.addSelectionForBufferRange(@getBufferRange()) unless @preserveSelection + + destroy: -> + @anchorRange.destroy() \ No newline at end of file From 7865799b34b326c4314c525e36888717f47c0441 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 14 Jul 2012 10:22:14 -0600 Subject: [PATCH 33/97] Enable accelerated and threaded compositing on CEF --- Atom/src/AtomController.mm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Atom/src/AtomController.mm b/Atom/src/AtomController.mm index a8c4373c5..4a8adc0fb 100644 --- a/Atom/src/AtomController.mm +++ b/Atom/src/AtomController.mm @@ -208,8 +208,8 @@ void AppGetBrowserSettings(CefBrowserSettings& settings) { settings.databases_disabled = false; settings.application_cache_disabled = false; settings.webgl_disabled = false; - settings.accelerated_compositing_enabled = false; - settings.threaded_compositing_enabled = false; + settings.accelerated_compositing_enabled = true; + settings.threaded_compositing_enabled = true; settings.accelerated_layers_disabled = false; settings.accelerated_video_disabled = false; settings.accelerated_2d_canvas_disabled = false; From 1ac6581f33a727f9610b8589094adbd90b3038ed Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 15 Jul 2012 17:32:30 -0600 Subject: [PATCH 34/97] EditSession.setSelectedBufferRanges recycles selection instances This prevents changing the scroll position when it isn't necessary. --- spec/app/edit-session-spec.coffee | 8 ++++++++ src/app/edit-session.coffee | 10 ++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index 43466df05..17d6f78a0 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -535,6 +535,14 @@ describe "EditSession", -> editSession.setSelectedBufferRanges([[[2, 2], [3, 3]], [[3, 0], [5, 5]]]) expect(editSession.getSelectedBufferRanges()).toEqual [[[2, 2], [5, 5]]] + it "recyles existing selection instances", -> + selection = editSession.getSelection() + editSession.setSelectedBufferRanges([[[2, 2], [3, 3]], [[4, 4], [5, 5]]]) + + [selection1, selection2] = editSession.getSelections() + expect(selection1).toBe selection + expect(selection1.getBufferRange()).toEqual [[2, 2], [3, 3]] + describe "when the preserveFolds option is false (the default)", -> it "removes folds that contain the selections", -> editSession.setSelectedBufferRange([[0,0], [0,0]]) diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index ca4bd2e27..2d457b28b 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -314,13 +314,19 @@ class EditSession setSelectedBufferRanges: (bufferRanges, options={}) -> throw new Error("Passed an empty array to setSelectedBufferRanges") unless bufferRanges.length - @clearAllSelections() + + selections = @getSelections() + selection.destroy() for selection in selections[bufferRanges.length...] + for bufferRange, i in bufferRanges bufferRange = Range.fromObject(bufferRange) unless options.preserveFolds for row in [bufferRange.start.row..bufferRange.end.row] @destroyFoldsContainingBufferRow(row) - @addSelectionForBufferRange(bufferRange, options) + if selections[i] + selections[i].setBufferRange(bufferRange, options) + else + @addSelectionForBufferRange(bufferRange, options) @mergeIntersectingSelections(options) removeSelection: (selection) -> From 629525383ef9f7890415bf5765dfd4bbfb0ee338 Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Mon, 16 Jul 2012 09:17:43 -0700 Subject: [PATCH 35/97] Adjusting the width of rendered lines happens automatically with CSS --- src/app/editor.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/editor.coffee b/src/app/editor.coffee index 21a7643b9..e9a0b926d 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -339,7 +339,6 @@ class Editor extends View @setSoftWrapColumn() if @activeEditSession.getSoftWrap() $(window).on "resize.editor#{@id}", => @updateRenderedLines() - @adjustWidthOfRenderedLines() # TODO: This should happen automatically with CSS @focus() if @isFocused @renderWhenAttached() From 1d21de1e838f371853c5645f02d05a0557ca039f Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 17 Jul 2012 10:38:01 -0600 Subject: [PATCH 36/97] Merge cursors after buffer changes that didn't occur via the EditSession The buffer now emits a new event 'update-anchors-after-change' to signal that all the anchors have been updated, which is an appropriate time to merge cursors. --- spec/app/edit-session-spec.coffee | 10 ++++------ src/app/buffer-change-operation.coffee | 1 + src/app/edit-session.coffee | 3 ++- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index b708e32b8..f2efa7d53 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -1362,14 +1362,12 @@ describe "EditSession", -> [cursor1, cursor2, cursor3] = editSession.getCursors() expect(editSession.getCursors().length).toBe 3 - editSession.backspace() + buffer.delete([[0, 0], [0, 1]]) + + expect(editSession.getCursors().length).toBe 2 expect(editSession.getCursors()).toEqual [cursor1, cursor3] expect(cursor1.getBufferPosition()).toEqual [0,0] - expect(cursor3.getBufferPosition()).toEqual [1,0] - - editSession.insertText "x" - expect(editSession.lineForBufferRow(0)).toBe "xar quicksort = function () {" - expect(editSession.lineForBufferRow(1)).toBe "x var sort = function(items) {" + expect(cursor3.getBufferPosition()).toEqual [1,1] describe "folding", -> describe "structural folding", -> diff --git a/src/app/buffer-change-operation.coffee b/src/app/buffer-change-operation.coffee index 92d6b4600..b9f61bec8 100644 --- a/src/app/buffer-change-operation.coffee +++ b/src/app/buffer-change-operation.coffee @@ -42,6 +42,7 @@ class BufferChangeOperation event = { oldRange, newRange, oldText, newText } @buffer.trigger 'change', event anchor.handleBufferChange(event) for anchor in @buffer.getAnchors() + @buffer.trigger 'update-anchors-after-change' newRange calculateNewRange: (oldRange, newText) -> diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index 7786b7ac1..56ccdb3e1 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -45,7 +45,8 @@ class EditSession @buffer.on "path-change.edit-session-#{@id}", => @trigger 'buffer-path-change' - @buffer.on "change.edit-session-#{@id}", (e) => @mergeCursors() + @buffer.on "update-anchors-after-change.edit-session-#{@id}", => + @mergeCursors() @displayBuffer.on "change.edit-session-#{@id}", (e) => @trigger 'screen-lines-change', e From 4584f47cf20a19bf187cb4e983ca625623fda68c Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 17 Jul 2012 11:17:46 -0600 Subject: [PATCH 37/97] Pass all ranges to each command when composing commands --- .../command-panel/commands/address.coffee | 5 +++-- .../commands/composite-command.coffee | 4 +--- .../commands/select-all-matches.coffee | 7 ++++--- .../command-panel/commands/substitution.coffee | 17 +++++++++-------- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/extensions/command-panel/commands/address.coffee b/src/extensions/command-panel/commands/address.coffee index 594d4a52d..7949a379d 100644 --- a/src/extensions/command-panel/commands/address.coffee +++ b/src/extensions/command-panel/commands/address.coffee @@ -3,7 +3,8 @@ Operation = require 'command-panel/operation' module.exports = class Address extends Command - compile: (project, buffer, range) -> - [new Operation(buffer: buffer, bufferRange: @getRange(buffer, range))] + compile: (project, buffer, ranges) -> + ranges.map (range) => + new Operation(buffer: buffer, bufferRange: @getRange(buffer, range)) isAddress: -> true diff --git a/src/extensions/command-panel/commands/composite-command.coffee b/src/extensions/command-panel/commands/composite-command.coffee index 2ae072f50..b6a10c37f 100644 --- a/src/extensions/command-panel/commands/composite-command.coffee +++ b/src/extensions/command-panel/commands/composite-command.coffee @@ -8,9 +8,7 @@ class CompositeCommand currentRanges = editSession.getSelectedBufferRanges() for command in @subcommands operations?.forEach (o) -> o.destroy() - operations = [] - for range in currentRanges - operations.push(command.compile(project, editSession.buffer, range)...) + operations = command.compile(project, editSession.buffer, currentRanges) currentRanges = operations.map (o) -> o.getBufferRange() editSession.clearAllSelections() unless command.preserveSelections diff --git a/src/extensions/command-panel/commands/select-all-matches.coffee b/src/extensions/command-panel/commands/select-all-matches.coffee index 471646f4b..eb828f0ac 100644 --- a/src/extensions/command-panel/commands/select-all-matches.coffee +++ b/src/extensions/command-panel/commands/select-all-matches.coffee @@ -8,8 +8,9 @@ class SelectAllMatches extends Command constructor: (pattern) -> @regex = new RegExp(pattern, 'g') - compile: (project, buffer, range) -> + compile: (project, buffer, ranges) -> operations = [] - buffer.scanInRange @regex, range, (match, matchRange) -> - operations.push(new Operation(buffer: buffer, bufferRange: matchRange)) + for range in ranges + buffer.scanInRange @regex, range, (match, matchRange) -> + operations.push(new Operation(buffer: buffer, bufferRange: matchRange)) operations diff --git a/src/extensions/command-panel/commands/substitution.coffee b/src/extensions/command-panel/commands/substitution.coffee index 9940c1ed1..046beda20 100644 --- a/src/extensions/command-panel/commands/substitution.coffee +++ b/src/extensions/command-panel/commands/substitution.coffee @@ -11,13 +11,14 @@ class Substitution extends Command @replacementText = replacementText @regex = new RegExp(pattern, options.join('')) - compile: (project, buffer, range) -> + compile: (project, buffer, ranges) -> operations = [] - buffer.scanInRange @regex, range, (match, matchRange, { replace }) => - operations.push(new Operation( - buffer: buffer, - bufferRange: matchRange, - newText: @replacementText - preserveSelection: true - )) + for range in ranges + buffer.scanInRange @regex, range, (match, matchRange, { replace }) => + operations.push(new Operation( + buffer: buffer, + bufferRange: matchRange, + newText: @replacementText + preserveSelection: true + )) operations From 330e9cebd95ad23a2b642d7025a93e8a5876c5d0 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 17 Jul 2012 12:02:45 -0600 Subject: [PATCH 38/97] Make it possible for command compilation to be async by returning promises --- .../command-interpreter-spec.coffee | 336 +++++++++++------- .../command-panel/commands/address.coffee | 5 +- .../commands/composite-command.coffee | 29 +- .../commands/select-all-matches.coffee | 5 +- .../commands/substitution.coffee | 5 +- 5 files changed, 231 insertions(+), 149 deletions(-) diff --git a/spec/extensions/command-interpreter-spec.coffee b/spec/extensions/command-interpreter-spec.coffee index c961838e2..adfadc261 100644 --- a/spec/extensions/command-interpreter-spec.coffee +++ b/spec/extensions/command-interpreter-spec.coffee @@ -21,45 +21,59 @@ describe "CommandInterpreter", -> describe "a line address", -> it "selects the specified line", -> - interpreter.eval('4', editSession) - expect(editSession.getSelections().length).toBe 1 - expect(editSession.getSelection().getBufferRange()).toEqual [[3, 0], [4, 0]] + waitsForPromise -> interpreter.eval('4', editSession) + runs -> + expect(editSession.getSelections().length).toBe 1 + expect(editSession.getSelection().getBufferRange()).toEqual [[3, 0], [4, 0]] describe "0", -> it "selects the zero-length string at the start of the file", -> - interpreter.eval('0', editSession) - expect(editSession.getSelections().length).toBe 1 - expect(editSession.getSelection().getBufferRange()).toEqual [[0,0], [0,0]] + waitsForPromise -> interpreter.eval('0', editSession) + runs -> + expect(editSession.getSelections().length).toBe 1 + expect(editSession.getSelection().getBufferRange()).toEqual [[0,0], [0,0]] - interpreter.eval('0,1', editSession) - expect(editSession.getSelections().length).toBe 1 - expect(editSession.getSelection().getBufferRange()).toEqual [[0,0], [1,0]] + interpreter.eval('0,1', editSession) + expect(editSession.getSelections().length).toBe 1 + expect(editSession.getSelection().getBufferRange()).toEqual [[0,0], [1,0]] describe "$", -> it "selects EOF", -> - interpreter.eval('$', editSession) - expect(editSession.getSelections().length).toBe 1 - expect(editSession.getSelection().getBufferRange()).toEqual [[12,2], [12,2]] + waitsForPromise -> interpreter.eval('$', editSession) + runs -> + expect(editSession.getSelections().length).toBe 1 + expect(editSession.getSelection().getBufferRange()).toEqual [[12,2], [12,2]] - interpreter.eval('1,$', editSession) - expect(editSession.getSelections().length).toBe 1 - expect(editSession.getSelection().getBufferRange()).toEqual [[0,0], [12,2]] + waitsForPromise -> interpreter.eval('1,$', editSession) + runs -> + expect(editSession.getSelections().length).toBe 1 + expect(editSession.getSelection().getBufferRange()).toEqual [[0,0], [12,2]] describe ".", -> describe "when a single selection", -> it 'maintains the current selection', -> editSession.clearSelections() - editSession.setSelectedBufferRange([[1,1], [2,2]]) - interpreter.eval('.', editSession) - expect(editSession.getSelection().getBufferRange()).toEqual [[1,1], [2,2]] - editSession.setSelectedBufferRange([[1,1], [2,2]]) - interpreter.eval('.,', editSession) - expect(editSession.getSelection().getBufferRange()).toEqual [[1,1], [12,2]] + waitsForPromise -> + editSession.setSelectedBufferRange([[1,1], [2,2]]) + interpreter.eval('.', editSession) - editSession.setSelectedBufferRange([[1,1], [2,2]]) - interpreter.eval(',.', editSession) - expect(editSession.getSelection().getBufferRange()).toEqual [[0,0], [2,2]] + runs -> + expect(editSession.getSelection().getBufferRange()).toEqual [[1,1], [2,2]] + + waitsForPromise -> + editSession.setSelectedBufferRange([[1,1], [2,2]]) + interpreter.eval('.,', editSession) + + runs -> + expect(editSession.getSelection().getBufferRange()).toEqual [[1,1], [12,2]] + + waitsForPromise -> + editSession.setSelectedBufferRange([[1,1], [2,2]]) + interpreter.eval(',.', editSession) + + runs -> + expect(editSession.getSelection().getBufferRange()).toEqual [[0,0], [2,2]] describe "with multiple selections", -> it "maintains the current selections", -> @@ -67,179 +81,225 @@ describe "CommandInterpreter", -> expect(preSelections.length).toBe 3 [preRange1, preRange2, preRange3] = preSelections.map (s) -> s.getScreenRange() - interpreter.eval('.', editSession) + waitsForPromise -> interpreter.eval('.', editSession) - selections = editSession.getSelections() - expect(selections.length).toBe 3 - [selection1, selection2, selection3] = selections - expect(selection1.getScreenRange()).toEqual preRange1 - expect(selection2.getScreenRange()).toEqual preRange2 - expect(selection3.getScreenRange()).toEqual preRange3 + runs -> + selections = editSession.getSelections() + expect(selections.length).toBe 3 + [selection1, selection2, selection3] = selections + expect(selection1.getScreenRange()).toEqual preRange1 + expect(selection2.getScreenRange()).toEqual preRange2 + expect(selection3.getScreenRange()).toEqual preRange3 describe "/regex/", -> beforeEach -> editSession.clearSelections() it 'selects text matching regex after current selection', -> - editSession.setSelectedBufferRange([[4,16], [4,20]]) - interpreter.eval('/pivot/', editSession) - expect(editSession.getSelection().getBufferRange()).toEqual [[6,16], [6,21]] + waitsForPromise -> + editSession.setSelectedBufferRange([[4,16], [4,20]]) + interpreter.eval('/pivot/', editSession) + + runs -> + expect(editSession.getSelection().getBufferRange()).toEqual [[6,16], [6,21]] it 'does not require the trailing slash', -> - editSession.setSelectedBufferRange([[4,16], [4,20]]) - interpreter.eval('/pivot', editSession) - expect(editSession.getSelection().getBufferRange()).toEqual [[6,16], [6,21]] + waitsForPromise -> + editSession.setSelectedBufferRange([[4,16], [4,20]]) + interpreter.eval('/pivot', editSession) + + runs -> + expect(editSession.getSelection().getBufferRange()).toEqual [[6,16], [6,21]] it "searches from the end of each selection in the buffer", -> - editSession.clearSelections() - editSession.setSelectedBufferRange([[4,16], [4,20]]) - editSession.addSelectionForBufferRange([[1,16], [2,20]]) - expect(editSession.getSelections().length).toBe 2 - interpreter.eval('/pivot', editSession) - selections = editSession.getSelections() - expect(selections.length).toBe 2 - expect(selections[0].getBufferRange()).toEqual [[3,8], [3,13]] - expect(selections[1].getBufferRange()).toEqual [[6,16], [6,21]] + waitsForPromise -> + editSession.clearSelections() + editSession.setSelectedBufferRange([[4,16], [4,20]]) + editSession.addSelectionForBufferRange([[1,16], [2,20]]) + expect(editSession.getSelections().length).toBe 2 + interpreter.eval('/pivot', editSession) + + runs -> + selections = editSession.getSelections() + expect(selections.length).toBe 2 + expect(selections[0].getBufferRange()).toEqual [[3,8], [3,13]] + expect(selections[1].getBufferRange()).toEqual [[6,16], [6,21]] it "wraps around to the beginning of the buffer, but doesn't infinitely loop if no matches are found", -> - editSession.setSelectedBufferRange([[10, 0], [10,3]]) - interpreter.eval('/pivot', editSession) - expect(editSession.getSelection().getBufferRange()).toEqual [[3,8], [3,13]] + waitsForPromise -> + editSession.setSelectedBufferRange([[10, 0], [10,3]]) + interpreter.eval('/pivot', editSession) - interpreter.eval('/mike tyson', editSession) - expect(editSession.getSelection().getBufferRange()).toEqual [[3,8], [3,13]] + runs -> + expect(editSession.getSelection().getBufferRange()).toEqual [[3,8], [3,13]] + + waitsForPromise -> + interpreter.eval('/mike tyson', editSession) + + runs -> + expect(editSession.getSelection().getBufferRange()).toEqual [[3,8], [3,13]] it "searches in reverse when prefixed with a -", -> - editSession.setSelectedBufferRange([[6, 16], [6, 22]]) - interpreter.eval('-/pivot', editSession) - expect(editSession.getSelection().getBufferRange()).toEqual [[3,8], [3,13]] + waitsForPromise -> + editSession.setSelectedBufferRange([[6, 16], [6, 22]]) + interpreter.eval('-/pivot', editSession) + + runs -> + expect(editSession.getSelection().getBufferRange()).toEqual [[3,8], [3,13]] describe "address range", -> describe "when two addresses are specified", -> it "selects from the begining of the left address to the end of the right address", -> - interpreter.eval('4,7', editSession) - expect(editSession.getSelections().length).toBe 1 - expect(editSession.getSelection().getBufferRange()).toEqual [[3, 0], [7, 0]] + waitsForPromise -> interpreter.eval('4,7', editSession) + + runs -> + expect(editSession.getSelections().length).toBe 1 + expect(editSession.getSelection().getBufferRange()).toEqual [[3, 0], [7, 0]] describe "when the left address is unspecified", -> it "selects from the begining of buffer to the end of the right address", -> - interpreter.eval(',7', editSession) - expect(editSession.getSelections().length).toBe 1 - expect(editSession.getSelection().getBufferRange()).toEqual [[0, 0], [7, 0]] + waitsForPromise -> interpreter.eval(',7', editSession) + runs -> + expect(editSession.getSelections().length).toBe 1 + expect(editSession.getSelection().getBufferRange()).toEqual [[0, 0], [7, 0]] describe "when the right address is unspecified", -> it "selects from the begining of left address to the end file", -> - interpreter.eval('4,', editSession) - expect(editSession.getSelections().length).toBe 1 - expect(editSession.getSelection().getBufferRange()).toEqual [[3, 0], [12, 2]] + waitsForPromise -> interpreter.eval('4,', editSession) + runs -> + expect(editSession.getSelections().length).toBe 1 + expect(editSession.getSelection().getBufferRange()).toEqual [[3, 0], [12, 2]] describe "when the neither address is specified", -> it "selects the entire file", -> - interpreter.eval(',', editSession) - expect(editSession.getSelections().length).toBe 1 - expect(editSession.getSelection().getBufferRange()).toEqual [[0, 0], [12, 2]] + waitsForPromise -> interpreter.eval(',', editSession) + runs -> + expect(editSession.getSelections().length).toBe 1 + expect(editSession.getSelection().getBufferRange()).toEqual [[0, 0], [12, 2]] describe "x/regex/", -> it "sets the current selection to every match of the regex in the current selection", -> - interpreter.eval('6,7 x/current/', editSession) + waitsForPromise -> interpreter.eval('6,7 x/current/', editSession) - selections = editSession.getSelections() - expect(selections.length).toBe 4 + runs -> + selections = editSession.getSelections() + expect(selections.length).toBe 4 - expect(selections[0].getBufferRange()).toEqual [[5,6], [5,13]] - expect(selections[1].getBufferRange()).toEqual [[6,6], [6,13]] - expect(selections[2].getBufferRange()).toEqual [[6,34], [6,41]] - expect(selections[3].getBufferRange()).toEqual [[6,56], [6,63]] + expect(selections[0].getBufferRange()).toEqual [[5,6], [5,13]] + expect(selections[1].getBufferRange()).toEqual [[6,6], [6,13]] + expect(selections[2].getBufferRange()).toEqual [[6,34], [6,41]] + expect(selections[3].getBufferRange()).toEqual [[6,56], [6,63]] describe "when matching /$/", -> it "matches the end of each line in the selected region", -> - interpreter.eval('6,8 x/$/', editSession) + waitsForPromise -> interpreter.eval('6,8 x/$/', editSession) - cursors = editSession.getCursors() - expect(cursors.length).toBe 3 + runs -> + cursors = editSession.getCursors() + expect(cursors.length).toBe 3 - expect(cursors[0].getBufferPosition()).toEqual [5, 30] - expect(cursors[1].getBufferPosition()).toEqual [6, 65] - expect(cursors[2].getBufferPosition()).toEqual [7, 5] + expect(cursors[0].getBufferPosition()).toEqual [5, 30] + expect(cursors[1].getBufferPosition()).toEqual [6, 65] + expect(cursors[2].getBufferPosition()).toEqual [7, 5] it "loops through current selections and selects text matching the regex", -> - editSession.setSelectedBufferRange [[3,0], [3,62]] - editSession.addSelectionForBufferRange [[6,0], [6,65]] + waitsForPromise -> + editSession.setSelectedBufferRange [[3,0], [3,62]] + editSession.addSelectionForBufferRange [[6,0], [6,65]] + interpreter.eval('x/current', editSession) - interpreter.eval('x/current', editSession) + runs -> + selections = editSession.getSelections() + expect(selections.length).toBe 4 - selections = editSession.getSelections() - expect(selections.length).toBe 4 - - expect(selections[0].getBufferRange()).toEqual [[3,31], [3,38]] - expect(selections[1].getBufferRange()).toEqual [[6,6], [6,13]] - expect(selections[2].getBufferRange()).toEqual [[6,34], [6,41]] - expect(selections[3].getBufferRange()).toEqual [[6,56], [6,63]] + expect(selections[0].getBufferRange()).toEqual [[3,31], [3,38]] + expect(selections[1].getBufferRange()).toEqual [[6,6], [6,13]] + expect(selections[2].getBufferRange()).toEqual [[6,34], [6,41]] + expect(selections[3].getBufferRange()).toEqual [[6,56], [6,63]] describe "substitution", -> it "does nothing if there are no matches", -> - editSession.setSelectedBufferRange([[6, 0], [6, 44]]) - interpreter.eval('s/not-in-text/foo/', editSession) - expect(buffer.lineForRow(6)).toBe ' current < pivot ? left.push(current) : right.push(current);' + waitsForPromise -> + editSession.setSelectedBufferRange([[6, 0], [6, 44]]) + interpreter.eval('s/not-in-text/foo/', editSession) + + runs -> + expect(buffer.lineForRow(6)).toBe ' current < pivot ? left.push(current) : right.push(current);' describe "when not global", -> describe "when there is a single selection", -> it "performs a single substitution within the current selection", -> - editSession.setSelectedBufferRange([[6, 0], [6, 44]]) - interpreter.eval('s/current/foo/', editSession) - expect(buffer.lineForRow(6)).toBe ' foo < pivot ? left.push(current) : right.push(current);' + waitsForPromise -> + editSession.setSelectedBufferRange([[6, 0], [6, 44]]) + interpreter.eval('s/current/foo/', editSession) + runs -> + expect(buffer.lineForRow(6)).toBe ' foo < pivot ? left.push(current) : right.push(current);' describe "when there are multiple selections", -> it "performs a single substitutions within each of the selections", -> - editSession.setSelectedBufferRange([[5, 0], [5, 20]]) - editSession.addSelectionForBufferRange([[6, 0], [6, 44]]) + waitsForPromise -> + editSession.setSelectedBufferRange([[5, 0], [5, 20]]) + editSession.addSelectionForBufferRange([[6, 0], [6, 44]]) + interpreter.eval('s/current/foo/', editSession) - interpreter.eval('s/current/foo/', editSession) - expect(buffer.lineForRow(5)).toBe ' foo = items.shift();' - expect(buffer.lineForRow(6)).toBe ' foo < pivot ? left.push(current) : right.push(current);' + runs -> + expect(buffer.lineForRow(5)).toBe ' foo = items.shift();' + expect(buffer.lineForRow(6)).toBe ' foo < pivot ? left.push(current) : right.push(current);' describe "when global", -> it "performs a multiple substitutions within the current selection", -> - editSession.setSelectedBufferRange([[6, 0], [6, 44]]) - interpreter.eval('s/current/foo/g', editSession) - expect(buffer.lineForRow(6)).toBe ' foo < pivot ? left.push(foo) : right.push(current);' - - describe "when prefixed with an address", -> - it "only makes substitutions within given lines", -> - interpreter.eval('4,6s/ /!/g', editSession) - expect(buffer.lineForRow(2)).toBe ' if (items.length <= 1) return items;' - expect(buffer.lineForRow(3)).toBe '!!!!var!pivot!=!items.shift(),!current,!left!=![],!right!=![];' - expect(buffer.lineForRow(4)).toBe '!!!!while(items.length!>!0)!{' - expect(buffer.lineForRow(5)).toBe '!!!!!!current!=!items.shift();' - expect(buffer.lineForRow(6)).toBe ' current < pivot ? left.push(current) : right.push(current);' - - describe "when matching $", -> - it "matches the end of each line and avoids infinitely looping on a zero-width match", -> - interpreter.eval(',s/$/!!!/g', editSession) - expect(buffer.lineForRow(0)).toBe 'var quicksort = function () {!!!' - expect(buffer.lineForRow(2)).toBe ' if (items.length <= 1) return items;!!!' - expect(buffer.lineForRow(6)).toBe ' current < pivot ? left.push(current) : right.push(current);!!!' - expect(buffer.lineForRow(12)).toBe '};!!!' - - describe "when matching ^", -> - it "matches the beginning of each line and avoids infinitely looping on a zero-width match", -> - interpreter.eval(',s/^/!!!/g', editSession) - expect(buffer.lineForRow(0)).toBe '!!!var quicksort = function () {' - expect(buffer.lineForRow(2)).toBe '!!! if (items.length <= 1) return items;' - expect(buffer.lineForRow(6)).toBe '!!! current < pivot ? left.push(current) : right.push(current);' - expect(buffer.lineForRow(12)).toBe '!!!};' - - describe "when there are multiple selections", -> - it "performs a multiple substitutions within each of the selections", -> - editSession.setSelectedBufferRange([[5, 0], [5, 20]]) - editSession.addSelectionForBufferRange([[6, 0], [6, 44]]) - + waitsForPromise -> + editSession.setSelectedBufferRange([[6, 0], [6, 44]]) interpreter.eval('s/current/foo/g', editSession) - expect(buffer.lineForRow(5)).toBe ' foo = items.shift();' + + runs -> expect(buffer.lineForRow(6)).toBe ' foo < pivot ? left.push(foo) : right.push(current);' + describe "when prefixed with an address", -> + it "only makes substitutions within given lines", -> + waitsForPromise -> interpreter.eval('4,6s/ /!/g', editSession) + + runs -> + expect(buffer.lineForRow(2)).toBe ' if (items.length <= 1) return items;' + expect(buffer.lineForRow(3)).toBe '!!!!var!pivot!=!items.shift(),!current,!left!=![],!right!=![];' + expect(buffer.lineForRow(4)).toBe '!!!!while(items.length!>!0)!{' + expect(buffer.lineForRow(5)).toBe '!!!!!!current!=!items.shift();' + expect(buffer.lineForRow(6)).toBe ' current < pivot ? left.push(current) : right.push(current);' + + describe "when matching $", -> + it "matches the end of each line and avoids infinitely looping on a zero-width match", -> + waitsForPromise -> interpreter.eval(',s/$/!!!/g', editSession) + runs -> + expect(buffer.lineForRow(0)).toBe 'var quicksort = function () {!!!' + expect(buffer.lineForRow(2)).toBe ' if (items.length <= 1) return items;!!!' + expect(buffer.lineForRow(6)).toBe ' current < pivot ? left.push(current) : right.push(current);!!!' + expect(buffer.lineForRow(12)).toBe '};!!!' + + describe "when matching ^", -> + it "matches the beginning of each line and avoids infinitely looping on a zero-width match", -> + waitsForPromise -> interpreter.eval(',s/^/!!!/g', editSession) + runs -> + expect(buffer.lineForRow(0)).toBe '!!!var quicksort = function () {' + expect(buffer.lineForRow(2)).toBe '!!! if (items.length <= 1) return items;' + expect(buffer.lineForRow(6)).toBe '!!! current < pivot ? left.push(current) : right.push(current);' + expect(buffer.lineForRow(12)).toBe '!!!};' + + describe "when there are multiple selections", -> + it "performs a multiple substitutions within each of the selections", -> + waitsForPromise -> + editSession.setSelectedBufferRange([[5, 0], [5, 20]]) + editSession.addSelectionForBufferRange([[6, 0], [6, 44]]) + interpreter.eval('s/current/foo/g', editSession) + + runs -> + expect(buffer.lineForRow(5)).toBe ' foo = items.shift();' + expect(buffer.lineForRow(6)).toBe ' foo < pivot ? left.push(foo) : right.push(current);' + describe "when prefixed with an address", -> it "restores the original selections upon completion if it is the last command", -> - editSession.setSelectedBufferRanges([[[5, 0], [5, 20]], [[6, 0], [6, 44]]]) - interpreter.eval(',s/current/foo/g', editSession) - expect(editSession.getSelectedBufferRanges()).toEqual [[[5, 0], [5, 16]], [[6, 0], [6, 36]]] + waitsForPromise -> + editSession.setSelectedBufferRanges([[[5, 0], [5, 20]], [[6, 0], [6, 44]]]) + interpreter.eval(',s/current/foo/g', editSession) + + runs -> + expect(editSession.getSelectedBufferRanges()).toEqual [[[5, 0], [5, 16]], [[6, 0], [6, 36]]] diff --git a/src/extensions/command-panel/commands/address.coffee b/src/extensions/command-panel/commands/address.coffee index 7949a379d..9f3a7af5f 100644 --- a/src/extensions/command-panel/commands/address.coffee +++ b/src/extensions/command-panel/commands/address.coffee @@ -1,10 +1,13 @@ Command = require 'command-panel/commands/command' Operation = require 'command-panel/operation' +$ = require 'jquery' module.exports = class Address extends Command compile: (project, buffer, ranges) -> - ranges.map (range) => + deferred = $.Deferred() + deferred.resolve ranges.map (range) => new Operation(buffer: buffer, bufferRange: @getRange(buffer, range)) + deferred.promise() isAddress: -> true diff --git a/src/extensions/command-panel/commands/composite-command.coffee b/src/extensions/command-panel/commands/composite-command.coffee index b6a10c37f..6569e2828 100644 --- a/src/extensions/command-panel/commands/composite-command.coffee +++ b/src/extensions/command-panel/commands/composite-command.coffee @@ -1,4 +1,5 @@ _ = require 'underscore' +$ = require 'jquery' module.exports = class CompositeCommand @@ -6,15 +7,27 @@ class CompositeCommand execute: (project, editSession) -> currentRanges = editSession.getSelectedBufferRanges() - for command in @subcommands - operations?.forEach (o) -> o.destroy() - operations = command.compile(project, editSession.buffer, currentRanges) - currentRanges = operations.map (o) -> o.getBufferRange() + @executeCommands(@subcommands, project, editSession, currentRanges) - editSession.clearAllSelections() unless command.preserveSelections - for operation in operations - operation.execute(editSession) - operation.destroy() + executeCommands: (commands, project, editSession, ranges) -> + deferred = $.Deferred() + [currentCommand, remainingCommands...] = commands + + currentCommand.compile(project, editSession.buffer, ranges).done (operations) => + if remainingCommands.length + nextRanges = operations.map (operation) -> + operation.destroy() + operation.getBufferRange() + @executeCommands(remainingCommands, project, editSession, nextRanges).done -> + deferred.resolve() + else + editSession.clearAllSelections() unless currentCommand.preserveSelections + for operation in operations + operation.execute(editSession) + operation.destroy() + deferred.resolve() + + deferred.promise() reverse: -> new CompositeCommand(@subcommands.map (command) -> command.reverse()) diff --git a/src/extensions/command-panel/commands/select-all-matches.coffee b/src/extensions/command-panel/commands/select-all-matches.coffee index eb828f0ac..f9d490bd7 100644 --- a/src/extensions/command-panel/commands/select-all-matches.coffee +++ b/src/extensions/command-panel/commands/select-all-matches.coffee @@ -1,5 +1,6 @@ Command = require 'command-panel/commands/command' Operation = require 'command-panel/operation' +$ = require 'jquery' module.exports = class SelectAllMatches extends Command @@ -9,8 +10,10 @@ class SelectAllMatches extends Command @regex = new RegExp(pattern, 'g') compile: (project, buffer, ranges) -> + deferred = $.Deferred() operations = [] for range in ranges buffer.scanInRange @regex, range, (match, matchRange) -> operations.push(new Operation(buffer: buffer, bufferRange: matchRange)) - operations + deferred.resolve(operations) + deferred.promise() diff --git a/src/extensions/command-panel/commands/substitution.coffee b/src/extensions/command-panel/commands/substitution.coffee index 046beda20..d96aaa762 100644 --- a/src/extensions/command-panel/commands/substitution.coffee +++ b/src/extensions/command-panel/commands/substitution.coffee @@ -1,5 +1,6 @@ Command = require 'command-panel/commands/command' Operation = require 'command-panel/operation' +$ = require 'jquery' module.exports = class Substitution extends Command @@ -12,6 +13,7 @@ class Substitution extends Command @regex = new RegExp(pattern, options.join('')) compile: (project, buffer, ranges) -> + deferred = $.Deferred() operations = [] for range in ranges buffer.scanInRange @regex, range, (match, matchRange, { replace }) => @@ -21,4 +23,5 @@ class Substitution extends Command newText: @replacementText preserveSelection: true )) - operations + deferred.resolve(operations) + deferred.promise() From c3fe9aa0b32a9b919749703f9ff337f62923e657 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Tue, 17 Jul 2012 14:16:12 -0600 Subject: [PATCH 39/97] Implement refcounting scheme on Buffer --- spec/app/buffer-spec.coffee | 8 +++----- spec/app/root-view-spec.coffee | 2 +- spec/extensions/autocomplete-spec.coffee | 3 ++- src/app/buffer.coffee | 10 ++++++++++ src/app/edit-session.coffee | 5 +++++ src/app/editor.coffee | 5 ++++- src/app/project.coffee | 21 +++++++++------------ 7 files changed, 34 insertions(+), 20 deletions(-) diff --git a/spec/app/buffer-spec.coffee b/spec/app/buffer-spec.coffee index 7ca4709e0..591870bfb 100644 --- a/spec/app/buffer-spec.coffee +++ b/spec/app/buffer-spec.coffee @@ -11,7 +11,7 @@ describe 'Buffer', -> buffer = new Buffer(filePath) afterEach -> - buffer.destroy() + buffer?.destroy() describe 'constructor', -> beforeEach -> @@ -31,9 +31,9 @@ describe 'Buffer', -> describe "when no file exists for the path", -> it "throws an exception", -> + buffer = null filePath = "does-not-exist.txt" expect(fs.exists(filePath)).toBeFalsy() - expect(-> new Buffer(filePath)).toThrow() describe "when no path is given", -> @@ -91,9 +91,6 @@ describe 'Buffer', -> expect(buffer.isModified()).toBeFalsy() describe ".isModified()", -> - beforeEach -> - buffer.destroy() - it "returns true when user changes buffer", -> expect(buffer.isModified()).toBeFalsy() buffer.insert([0,0], "hi") @@ -102,6 +99,7 @@ describe 'Buffer', -> it "returns false after modified buffer is saved", -> filePath = "/tmp/atom-tmp-file" fs.write(filePath, '') + buffer.destroy() buffer = new Buffer(filePath) expect(buffer.isModified()).toBe false diff --git a/spec/app/root-view-spec.coffee b/spec/app/root-view-spec.coffee index 5114e6569..c323a0b38 100644 --- a/spec/app/root-view-spec.coffee +++ b/spec/app/root-view-spec.coffee @@ -491,7 +491,7 @@ describe "RootView", -> rootView.focus() expect(pathChangeHandler).not.toHaveBeenCalled() - editor2.edit(editor1.activeEditSession) + editor2.edit(editor1.activeEditSession.copy()) editor2.focus() expect(pathChangeHandler).not.toHaveBeenCalled() diff --git a/spec/extensions/autocomplete-spec.coffee b/spec/extensions/autocomplete-spec.coffee index ca430de4c..5e865c8b1 100644 --- a/spec/extensions/autocomplete-spec.coffee +++ b/spec/extensions/autocomplete-spec.coffee @@ -15,7 +15,7 @@ describe "Autocomplete", -> miniEditor = autocomplete.miniEditor afterEach -> - editor.remove() + editor?.remove() describe "@activate(rootView)", -> it "activates autocomplete on all existing and future editors (but not on autocomplete's own mini editor)", -> @@ -382,6 +382,7 @@ describe "Autocomplete", -> editor.remove() editor.getBuffer().insert([0,0], "s") expect(autocomplete.buildWordList).not.toHaveBeenCalled() + editor = null describe ".attach()", -> beforeEach -> diff --git a/src/app/buffer.coffee b/src/app/buffer.coffee index db253bd57..b8e7731cc 100644 --- a/src/app/buffer.coffee +++ b/src/app/buffer.coffee @@ -18,6 +18,7 @@ class Buffer file: null anchors: null anchorRanges: null + refcount: 0 constructor: (path) -> @id = @constructor.idCounter++ @@ -36,8 +37,17 @@ class Buffer @modified = false destroy: -> + throw new Error("Destroying buffer twice with path '#{@getPath()}'") if @destroyed + @destroyed = true @file?.off() + retain: -> + @refcount++ + + release: -> + @refcount-- + @destroy() if @refcount <= 0 + getPath: -> @file?.getPath() diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index 56ccdb3e1..40fe58d83 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -42,6 +42,7 @@ class EditSession @selections = [] @addCursorAtScreenPosition([0, 0]) + @buffer.retain() @buffer.on "path-change.edit-session-#{@id}", => @trigger 'buffer-path-change' @@ -54,7 +55,11 @@ class EditSession anchor.refreshScreenPosition() for anchor in @getAnchors() destroy: -> + throw new Error("Edit session already destroyed") if @destroyed + @destroyed = true + @buffer.off ".edit-session-#{@id}" + @buffer.release() @displayBuffer.off ".edit-session-#{@id}" @displayBuffer.destroy() @project.removeEditSession(this) diff --git a/src/app/editor.coffee b/src/app/editor.coffee index e0ed5fa1d..c130c2a40 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -570,8 +570,11 @@ class Editor extends View if @pane() then @pane().remove() else super rootView?.focus() + getEditSessions: -> + new Array(@editSessions...) + destroyEditSessions: -> - for session in @editSessions + for session in @getEditSessions() session.destroy() renderWhenAttached: -> diff --git a/src/app/project.coffee b/src/app/project.coffee index 610592708..20baeae56 100644 --- a/src/app/project.coffee +++ b/src/app/project.coffee @@ -100,22 +100,14 @@ class Project softTabs: @getSoftTabs() softWrap: @getSoftWrap() + getEditSessions: -> + new Array(@editSessions...) + destroy: -> - for editSession in _.clone(@editSessions) - @removeEditSession(editSession) + editSession.destroy() for editSession in @getEditSessions() removeEditSession: (editSession) -> _.remove(@editSessions, editSession) - @destroyBufferIfOrphaned(editSession.buffer) - - destroyBufferIfOrphaned: (buffer) -> - unless _.find(@editSessions, (editSession) -> editSession.buffer == buffer) - buffer.destroy() - - buildBuffer: (filePath) -> - buffer = new Buffer(filePath) - @trigger 'new-buffer', buffer - buffer getBuffers: -> buffers = [] @@ -127,6 +119,11 @@ class Project bufferWithPath: (path) -> return editSession.buffer for editSession in @editSessions when editSession.buffer.getPath() == path + buildBuffer: (filePath) -> + buffer = new Buffer(filePath) + @trigger 'new-buffer', buffer + buffer + scan: (regex, iterator) -> regex = new RegExp(regex.source, 'g') command = "grep --null --perl-regexp --with-filename --line-number --recursive --regexp=\"#{regex.source}\" #{@getPath()}" From 34e96fb8d772ecbf1214bd295135f513e2eec1e0 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Tue, 17 Jul 2012 14:26:12 -0600 Subject: [PATCH 40/97] WIP: Adding find all matches in project command --- .../command-interpreter-spec.coffee | 22 ++++++++++++++++++- spec/fixtures/dir/a-dir/oh-git | 1 + src/app/project.coffee | 17 +++++++------- src/extensions/command-panel/commands.pegjs | 6 ++++- .../command-panel/commands/command.coffee | 1 + .../commands/composite-command.coffee | 11 ++++++---- .../select-all-matches-in-project.coffee | 20 +++++++++++++++++ src/extensions/command-panel/operation.coffee | 2 ++ 8 files changed, 65 insertions(+), 15 deletions(-) create mode 100644 src/extensions/command-panel/commands/select-all-matches-in-project.coffee diff --git a/spec/extensions/command-interpreter-spec.coffee b/spec/extensions/command-interpreter-spec.coffee index adfadc261..dbddcb546 100644 --- a/spec/extensions/command-interpreter-spec.coffee +++ b/spec/extensions/command-interpreter-spec.coffee @@ -1,11 +1,13 @@ CommandInterpreter = require 'command-panel/command-interpreter' +Project = require 'project' Buffer = require 'buffer' EditSession = require 'edit-session' describe "CommandInterpreter", -> - [interpreter, editSession, buffer, anchorCountBefore] = [] + [project, interpreter, editSession, buffer, anchorCountBefore] = [] beforeEach -> + project = new Project(fixturesProject.resolve('dir/')) interpreter = new CommandInterpreter(fixturesProject) editSession = fixturesProject.open('sample.js') buffer = editSession.buffer @@ -303,3 +305,21 @@ describe "CommandInterpreter", -> runs -> expect(editSession.getSelectedBufferRanges()).toEqual [[[5, 0], [5, 16]], [[6, 0], [6, 36]]] + + describe "X x/regex/", -> + it "returns selection operations for all regex matches in all the project's files", -> + editSession.destroy() + project = new Project(fixturesProject.resolve('dir/')) + interpreter = new CommandInterpreter(fixturesProject) + editSession = project.open('a') + + operations = null + waitsForPromise -> + interpreter.eval("X x/a+/", editSession).done (ops) -> + operations = ops + + runs -> + console.log operations + + + operation.destroy() for operation in operations diff --git a/spec/fixtures/dir/a-dir/oh-git b/spec/fixtures/dir/a-dir/oh-git index e69de29bb..64998f776 100644 --- a/spec/fixtures/dir/a-dir/oh-git +++ b/spec/fixtures/dir/a-dir/oh-git @@ -0,0 +1 @@ +bbb aaaa \ No newline at end of file diff --git a/src/app/project.coffee b/src/app/project.coffee index 20baeae56..07e4a519c 100644 --- a/src/app/project.coffee +++ b/src/app/project.coffee @@ -77,13 +77,7 @@ class Project setSoftWrap: (@softWrap) -> open: (filePath, editSessionOptions={}) -> - if filePath? - filePath = @resolve(filePath) - buffer = @bufferWithPath(filePath) ? @buildBuffer(filePath) - else - buffer = @buildBuffer() - - @buildEditSession(buffer, editSessionOptions) + @buildEditSession(@bufferForPath(filePath), editSessionOptions) buildEditSession: (buffer, editSessionOptions) -> options = _.extend(@defaultEditSessionOptions(), editSessionOptions) @@ -116,8 +110,13 @@ class Project buffers - bufferWithPath: (path) -> - return editSession.buffer for editSession in @editSessions when editSession.buffer.getPath() == path + bufferForPath: (filePath) -> + if filePath? + filePath = @resolve(filePath) + return editSession.buffer for editSession in @editSessions when editSession.buffer.getPath() == filePath + @buildBuffer(filePath) + else + @buildBuffer() buildBuffer: (filePath) -> buffer = new Buffer(filePath) diff --git a/src/extensions/command-panel/commands.pegjs b/src/extensions/command-panel/commands.pegjs index 982a206fe..7761003eb 100644 --- a/src/extensions/command-panel/commands.pegjs +++ b/src/extensions/command-panel/commands.pegjs @@ -8,6 +8,7 @@ var CurrentSelectionAddress = require('command-panel/commands/current-selection-address') var RegexAddress = require('command-panel/commands/regex-address') var SelectAllMatches = require('command-panel/commands/select-all-matches') + var SelectAllMatchesInProject = require('command-panel/commands/select-all-matches-in-project') } start = expressions:(expression+) { @@ -35,7 +36,7 @@ primitiveAddress regexAddress = reverse:'-'? '/' pattern:pattern '/'? { return new RegexAddress(pattern, reverse.length > 0)} -command = substitution / selectAllMatches +command = substitution / selectAllMatches / selectAllMatchesInProject substitution = "s" _ "/" find:pattern "/" replace:pattern "/" _ options:[g]* { @@ -45,6 +46,9 @@ substitution selectAllMatches = 'x' _ '/' pattern:pattern '/'? { return new SelectAllMatches(pattern) } +selectAllMatchesInProject + = 'X' _ 'x' _ '/' pattern:pattern '/'? { return new SelectAllMatchesInProject(pattern) } + pattern = pattern:[^/]* { return pattern.join('') } diff --git a/src/extensions/command-panel/commands/command.coffee b/src/extensions/command-panel/commands/command.coffee index b337f0a61..181def85b 100644 --- a/src/extensions/command-panel/commands/command.coffee +++ b/src/extensions/command-panel/commands/command.coffee @@ -4,3 +4,4 @@ module.exports = class Command isAddress: -> false preserveSelections: false + previewOperations: false \ No newline at end of file diff --git a/src/extensions/command-panel/commands/composite-command.coffee b/src/extensions/command-panel/commands/composite-command.coffee index 6569e2828..0e643a6f7 100644 --- a/src/extensions/command-panel/commands/composite-command.coffee +++ b/src/extensions/command-panel/commands/composite-command.coffee @@ -22,10 +22,13 @@ class CompositeCommand deferred.resolve() else editSession.clearAllSelections() unless currentCommand.preserveSelections - for operation in operations - operation.execute(editSession) - operation.destroy() - deferred.resolve() + if currentCommand.previewOperations + deferred.resolve(operations) + else + for operation in operations + operation.execute(editSession) + operation.destroy() + deferred.resolve() deferred.promise() diff --git a/src/extensions/command-panel/commands/select-all-matches-in-project.coffee b/src/extensions/command-panel/commands/select-all-matches-in-project.coffee new file mode 100644 index 000000000..e6e75e407 --- /dev/null +++ b/src/extensions/command-panel/commands/select-all-matches-in-project.coffee @@ -0,0 +1,20 @@ +Command = require 'command-panel/commands/command' +Operation = require 'command-panel/operation' +$ = require 'jquery' + +module.exports = +class SelectAllMatchesInProject extends Command + regex: null + previewOperations: true + + constructor: (pattern) -> + @regex = new RegExp(pattern, 'g') + + compile: (project, buffer, range) -> + deferred = $.Deferred() + operations = [] + promise = project.scan @regex, ({path, range}) -> + operations.push(new Operation(buffer: project.bufferForPath(path), bufferRange: range)) + + promise.done -> deferred.resolve(operations) + deferred.promise() diff --git a/src/extensions/command-panel/operation.coffee b/src/extensions/command-panel/operation.coffee index be7716725..fd707f37e 100644 --- a/src/extensions/command-panel/operation.coffee +++ b/src/extensions/command-panel/operation.coffee @@ -1,6 +1,7 @@ module.exports = class Operation constructor: ({@buffer, bufferRange, @newText, @preserveSelection}) -> + @buffer.retain() @anchorRange = @buffer.addAnchorRange(bufferRange) getBufferRange: -> @@ -11,4 +12,5 @@ class Operation editSession.addSelectionForBufferRange(@getBufferRange()) unless @preserveSelection destroy: -> + @buffer.release() @anchorRange.destroy() \ No newline at end of file From 126bdc113843555a6f3f41c8acfd24fd9dfc80ec Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Tue, 17 Jul 2012 14:26:20 -0600 Subject: [PATCH 41/97] :lipstick: --- spec/app/project-spec.coffee | 2 -- 1 file changed, 2 deletions(-) diff --git a/spec/app/project-spec.coffee b/spec/app/project-spec.coffee index b9203b1a5..8f3a13e0e 100644 --- a/spec/app/project-spec.coffee +++ b/spec/app/project-spec.coffee @@ -125,5 +125,3 @@ describe "Project", -> path: project.resolve('a') match: ['aa', 'a'] range: [[1, 3], [1, 5]] - - From 4b147c04e5c95e762b1d2734c3d24a58089578d1 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 17 Jul 2012 14:44:04 -0600 Subject: [PATCH 42/97] RootView.proto.open returns the opened edit session --- spec/app/root-view-spec.coffee | 32 +++++++++++++++++++++----------- src/app/editor.coffee | 2 +- src/app/root-view.coffee | 22 ++++++++++++---------- 3 files changed, 34 insertions(+), 22 deletions(-) diff --git a/spec/app/root-view-spec.coffee b/spec/app/root-view-spec.coffee index c323a0b38..5bf2e5b80 100644 --- a/spec/app/root-view-spec.coffee +++ b/spec/app/root-view-spec.coffee @@ -526,16 +526,18 @@ describe "RootView", -> expect(rootView.getActiveEditor()).toBeUndefined() describe "when called with no path", -> - it "opens an empty buffer in a new editor", -> - rootView.open() + it "opens / returns an edit session for an empty buffer in a new editor", -> + editSession = rootView.open() expect(rootView.getActiveEditor()).toBeDefined() expect(rootView.getActiveEditor().getPath()).toBeUndefined() + expect(editSession).toBe rootView.getActiveEditor().activeEditSession describe "when called with a path", -> it "opens a buffer with the given path in a new editor", -> - rootView.open('b') + editSession = rootView.open('b') expect(rootView.getActiveEditor()).toBeDefined() expect(rootView.getActiveEditor().getPath()).toBe require.resolve('fixtures/dir/b') + expect(editSession).toBe rootView.getActiveEditor().activeEditSession describe "when there is an active editor", -> beforeEach -> @@ -543,8 +545,9 @@ describe "RootView", -> describe "when called with no path", -> it "opens an empty buffer in the active editor", -> - rootView.open() + editSession = rootView.open() expect(rootView.getActiveEditor().getPath()).toBeUndefined() + expect(editSession).toBe rootView.getActiveEditor().activeEditSession describe "when called with a path", -> [editor1, editor2] = [] @@ -566,16 +569,19 @@ describe "RootView", -> expect(activeEditor.getPath()).toBe require.resolve('fixtures/dir/a') previousEditSession = activeEditor.activeEditSession - rootView.open('b') + editSession = rootView.open('b') expect(activeEditor.activeEditSession).not.toBe previousEditSession + expect(editSession).toBe rootView.getActiveEditor().activeEditSession - rootView.open('a') + editSession = rootView.open('a') expect(activeEditor.activeEditSession).toBe previousEditSession + expect(editSession).toBe previousEditSession describe "when the active editor does not have an edit session for the given path", -> it "creates a new edit session for the given path in the active editor", -> - rootView.open('b') + editSession = rootView.open('b') expect(activeEditor.editSessions.length).toBe 2 + expect(editSession).toBe rootView.getActiveEditor().activeEditSession describe "when the 'allowActiveEditorChange' option is true", -> describe "when the active editor has an edit session for the given path", -> @@ -584,23 +590,27 @@ describe "RootView", -> expect(activeEditor.getPath()).toBe require.resolve('fixtures/dir/a') previousEditSession = activeEditor.activeEditSession - rootView.open('b') + editSession = rootView.open('b') expect(activeEditor.activeEditSession).not.toBe previousEditSession + expect(editSession).toBe activeEditor.activeEditSession - rootView.open('a', allowActiveEditorChange: true) + editSession = rootView.open('a', allowActiveEditorChange: true) expect(activeEditor.activeEditSession).toBe previousEditSession + expect(editSession).toBe activeEditor.activeEditSession describe "when the active editor does *not* have an edit session for the given path", -> describe "when another editor has an edit session for the path", -> it "focuses the other editor and activates its edit session for the path", -> expect(rootView.getActiveEditor()).toBe editor1 - rootView.open('b', allowActiveEditorChange: true) + editSession = rootView.open('b', allowActiveEditorChange: true) expect(rootView.getActiveEditor()).toBe editor2 expect(editor2.getPath()).toBe require.resolve('fixtures/dir/b') + expect(editSession).toBe rootView.getActiveEditor().activeEditSession describe "when no other editor has an edit session for the path either", -> it "creates a new edit session for the path on the current active editor", -> path = require.resolve('fixtures/sample.js') - rootView.open(path, allowActiveEditorChange: true) + editSession = rootView.open(path, allowActiveEditorChange: true) expect(rootView.getActiveEditor()).toBe editor1 expect(editor1.getPath()).toBe path + expect(editSession).toBe rootView.getActiveEditor().activeEditSession diff --git a/src/app/editor.coffee b/src/app/editor.coffee index c130c2a40..2727f0a56 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -394,7 +394,7 @@ class Editor extends View for editSession, index in @editSessions if editSession.buffer.getPath() == path @setActiveEditSessionIndex(index) - return true + return @activeEditSession false getOpenBufferPaths: -> diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index 41d3dfe0e..6115f9a8d 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -95,8 +95,9 @@ class RootView extends View changeFocus = options.changeFocus ? true allowActiveEditorChange = options.allowActiveEditorChange ? false - unless @openInExistingEditor(path, allowActiveEditorChange) - editor = new Editor(editSession: @project.open(path)) + unless editSession = @openInExistingEditor(path, allowActiveEditorChange) + editSession = @project.open(path) + editor = new Editor({editSession}) pane = new Pane(editor) @panes.append(pane) if changeFocus @@ -104,23 +105,24 @@ class RootView extends View else @makeEditorActive(editor) + editSession + openInExistingEditor: (path, allowActiveEditorChange) -> if activeEditor = @getActiveEditor() path = @project.resolve(path) if path - if activeEditor.activateEditSessionForPath(path) - return true + if editSession = activeEditor.activateEditSessionForPath(path) + return editSession if allowActiveEditorChange for editor in @getEditors() - if editor.activateEditSessionForPath(path) + if editSession = editor.activateEditSessionForPath(path) editor.focus() - return true + return editSession - activeEditor.edit(@project.open(path)) - true - else - false + editSession = @project.open(path) + activeEditor.edit(editSession) + editSession editorFocused: (editor) -> @makeEditorActive(editor) if @panes.containsElement(editor) From d8189a6fc473aa53c16cb0edbe99db4aacc27c57 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 17 Jul 2012 16:32:20 -0600 Subject: [PATCH 43/97] "X x/regex/" command returns operations Also, passing an EditSession is optional when calling CommandInterpreter.eval. X commands don't require it, but other commands will throw exceptions if it's missing. --- .../command-interpreter-spec.coffee | 19 +++++++++++-------- .../commands/composite-command.coffee | 6 +++--- src/extensions/command-panel/operation.coffee | 3 +++ 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/spec/extensions/command-interpreter-spec.coffee b/spec/extensions/command-interpreter-spec.coffee index dbddcb546..11cf661c3 100644 --- a/spec/extensions/command-interpreter-spec.coffee +++ b/spec/extensions/command-interpreter-spec.coffee @@ -13,7 +13,7 @@ describe "CommandInterpreter", -> buffer = editSession.buffer afterEach -> - editSession.destroy() + editSession?.destroy() expect(buffer.getAnchors().length).toBe 0 describe "addresses", -> @@ -310,16 +310,19 @@ describe "CommandInterpreter", -> it "returns selection operations for all regex matches in all the project's files", -> editSession.destroy() project = new Project(fixturesProject.resolve('dir/')) - interpreter = new CommandInterpreter(fixturesProject) - editSession = project.open('a') + interpreter = new CommandInterpreter(project) operations = null waitsForPromise -> - interpreter.eval("X x/a+/", editSession).done (ops) -> - operations = ops + interpreter.eval("X x/a+/").done (ops) -> operations = ops runs -> - console.log operations + expect(operations.length).toBeGreaterThan 3 + for operation in operations + editSession = project.open(operation.getPath()) + operation.execute(editSession) + expect(editSession.getSelectedText()).toMatch /a+/ + editSession.destroy() + operation.destroy() - - operation.destroy() for operation in operations + editSession = null diff --git a/src/extensions/command-panel/commands/composite-command.coffee b/src/extensions/command-panel/commands/composite-command.coffee index 0e643a6f7..c15cc9e2f 100644 --- a/src/extensions/command-panel/commands/composite-command.coffee +++ b/src/extensions/command-panel/commands/composite-command.coffee @@ -6,14 +6,14 @@ class CompositeCommand constructor: (@subcommands) -> execute: (project, editSession) -> - currentRanges = editSession.getSelectedBufferRanges() + currentRanges = editSession?.getSelectedBufferRanges() @executeCommands(@subcommands, project, editSession, currentRanges) executeCommands: (commands, project, editSession, ranges) -> deferred = $.Deferred() [currentCommand, remainingCommands...] = commands - currentCommand.compile(project, editSession.buffer, ranges).done (operations) => + currentCommand.compile(project, editSession?.buffer, ranges).done (operations) => if remainingCommands.length nextRanges = operations.map (operation) -> operation.destroy() @@ -21,7 +21,7 @@ class CompositeCommand @executeCommands(remainingCommands, project, editSession, nextRanges).done -> deferred.resolve() else - editSession.clearAllSelections() unless currentCommand.preserveSelections + editSession?.clearAllSelections() unless currentCommand.preserveSelections if currentCommand.previewOperations deferred.resolve(operations) else diff --git a/src/extensions/command-panel/operation.coffee b/src/extensions/command-panel/operation.coffee index fd707f37e..0f47201dc 100644 --- a/src/extensions/command-panel/operation.coffee +++ b/src/extensions/command-panel/operation.coffee @@ -4,6 +4,9 @@ class Operation @buffer.retain() @anchorRange = @buffer.addAnchorRange(bufferRange) + getPath: -> + @buffer.getPath() + getBufferRange: -> @anchorRange.getBufferRange() From 0bc9973d9f1a6467fd992b36d11812f7e06774d5 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 17 Jul 2012 16:54:29 -0600 Subject: [PATCH 44/97] Don't set the ':' prompt font in command panel because it looks bad afterward --- spec/extensions/command-panel-spec.coffee | 3 --- src/extensions/command-panel/command-panel.coffee | 1 - 2 files changed, 4 deletions(-) diff --git a/spec/extensions/command-panel-spec.coffee b/spec/extensions/command-panel-spec.coffee index 52cb065fb..19cad8004 100644 --- a/spec/extensions/command-panel-spec.coffee +++ b/spec/extensions/command-panel-spec.coffee @@ -34,11 +34,8 @@ describe "CommandPanel", -> expect(commandPanel.miniEditor.isFocused).toBeFalsy() rootView.trigger 'command-panel:toggle' - window.advanceClock() # Setting the font is in a defer statement expect(rootView.find('.command-panel').view()).toBe commandPanel expect(commandPanel.miniEditor.isFocused).toBeTruthy() - # this is currently assigned dynamically since our css scheme lacks variables - expect(commandPanel.prompt.css('font')).toBe commandPanel.miniEditor.css('font') commandPanel.miniEditor.insertText 's/war/peace/g' rootView.trigger 'command-panel:toggle' diff --git a/src/extensions/command-panel/command-panel.coffee b/src/extensions/command-panel/command-panel.coffee index 677be9453..0cbde9cb2 100644 --- a/src/extensions/command-panel/command-panel.coffee +++ b/src/extensions/command-panel/command-panel.coffee @@ -56,7 +56,6 @@ class CommandPanel extends View @rootView.append(this) @miniEditor.focus() @miniEditor.setText(text) - @prompt.css 'font', @miniEditor.css('font') detach: -> @rootView.focus() From dcb8fd71e0a5a9199e1ead2ad5640f3fac96a4ae Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 17 Jul 2012 17:03:19 -0600 Subject: [PATCH 45/97] Convert a command panel spec to integration-style to prepare for command preview --- spec/extensions/command-panel-spec.coffee | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/spec/extensions/command-panel-spec.coffee b/spec/extensions/command-panel-spec.coffee index 19cad8004..b60116196 100644 --- a/spec/extensions/command-panel-spec.coffee +++ b/spec/extensions/command-panel-spec.coffee @@ -2,13 +2,14 @@ RootView = require 'root-view' CommandPanel = require 'command-panel' describe "CommandPanel", -> - [rootView, editor, commandPanel] = [] + [rootView, editor, buffer, commandPanel] = [] beforeEach -> rootView = new RootView rootView.open(require.resolve 'fixtures/sample.js') rootView.enableKeymap() editor = rootView.getActiveEditor() + buffer = editor.activeEditSession.buffer commandPanel = requireExtension('command-panel') afterEach -> @@ -109,13 +110,14 @@ describe "CommandPanel", -> expect(rootView.find('.command-panel')).not.toExist() describe "when return is pressed on the panel's editor", -> - it "calls execute", -> - spyOn(commandPanel, 'execute') - rootView.trigger 'command-panel:toggle' - commandPanel.miniEditor.insertText 's/hate/love/g' - commandPanel.miniEditor.trigger keydownEvent('enter') + describe "if the command has an immediate effect", -> + it "executes it immediately on the current buffer", -> + rootView.trigger 'command-panel:toggle' + commandPanel.miniEditor.insertText ',s/sort/torta/g' + commandPanel.miniEditor.trigger keydownEvent('enter') - expect(commandPanel.execute).toHaveBeenCalled() + expect(buffer.lineForRow(0)).toMatch /quicktorta/ + expect(buffer.lineForRow(1)).toMatch /var torta/ describe "if the command is malformed", -> it "adds and removes an error class to the command panel and does not close it", -> From f998da301c89cedd480c24be43619e9f4d5b91c2 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Tue, 17 Jul 2012 18:39:00 -0600 Subject: [PATCH 46/97] Native handler logs when completion callback for child process raises exception --- Atom/src/native_handler.mm | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Atom/src/native_handler.mm b/Atom/src/native_handler.mm index 7b8103898..c3e096c68 100644 --- a/Atom/src/native_handler.mm +++ b/Atom/src/native_handler.mm @@ -473,6 +473,11 @@ bool NativeHandler::Execute(const CefString& name, args.push_back(CefV8Value::CreateString([errorOutput UTF8String])); callback->ExecuteFunction(callback, args, retval, e, false); + + if (e.get()) { + NSLog(@"Error thrown in TaskTerminatedHandle %s", e->GetMessage().ToString().c_str()); + } + context->Exit(); stdout.fileHandleForReading.writeabilityHandler = nil; From faea4aedcc23dbaecefe35ffa7b15e66c4d9325a Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Tue, 17 Jul 2012 18:39:29 -0600 Subject: [PATCH 47/97] Show preview list when X x operation completes --- .../command-interpreter-spec.coffee | 25 ++++++++-------- spec/extensions/command-panel-spec.coffee | 17 ++++++++++- .../command-panel/command-panel.coffee | 30 +++++++++++++++---- .../commands/composite-command.coffee | 2 +- src/extensions/command-panel/operation.coffee | 3 ++ .../command-panel/preview-item.coffee | 8 +++++ static/command-panel.css | 8 +++++ 7 files changed, 73 insertions(+), 20 deletions(-) create mode 100644 src/extensions/command-panel/preview-item.coffee diff --git a/spec/extensions/command-interpreter-spec.coffee b/spec/extensions/command-interpreter-spec.coffee index 11cf661c3..414948c4c 100644 --- a/spec/extensions/command-interpreter-spec.coffee +++ b/spec/extensions/command-interpreter-spec.coffee @@ -204,20 +204,21 @@ describe "CommandInterpreter", -> expect(cursors[1].getBufferPosition()).toEqual [6, 65] expect(cursors[2].getBufferPosition()).toEqual [7, 5] - it "loops through current selections and selects text matching the regex", -> - waitsForPromise -> - editSession.setSelectedBufferRange [[3,0], [3,62]] - editSession.addSelectionForBufferRange [[6,0], [6,65]] - interpreter.eval('x/current', editSession) + describe "when text is initially selected", -> + it "loops through current selections and selects text matching the regex", -> + waitsForPromise -> + editSession.setSelectedBufferRange [[3,0], [3,62]] + editSession.addSelectionForBufferRange [[6,0], [6,65]] + interpreter.eval('x/current', editSession) - runs -> - selections = editSession.getSelections() - expect(selections.length).toBe 4 + runs -> + selections = editSession.getSelections() + expect(selections.length).toBe 4 - expect(selections[0].getBufferRange()).toEqual [[3,31], [3,38]] - expect(selections[1].getBufferRange()).toEqual [[6,6], [6,13]] - expect(selections[2].getBufferRange()).toEqual [[6,34], [6,41]] - expect(selections[3].getBufferRange()).toEqual [[6,56], [6,63]] + expect(selections[0].getBufferRange()).toEqual [[3,31], [3,38]] + expect(selections[1].getBufferRange()).toEqual [[6,6], [6,13]] + expect(selections[2].getBufferRange()).toEqual [[6,34], [6,41]] + expect(selections[3].getBufferRange()).toEqual [[6,56], [6,63]] describe "substitution", -> it "does nothing if there are no matches", -> diff --git a/spec/extensions/command-panel-spec.coffee b/spec/extensions/command-panel-spec.coffee index b60116196..0997ac316 100644 --- a/spec/extensions/command-panel-spec.coffee +++ b/spec/extensions/command-panel-spec.coffee @@ -13,7 +13,7 @@ describe "CommandPanel", -> commandPanel = requireExtension('command-panel') afterEach -> - rootView.remove() + rootView.deactivate() describe "serialization", -> it "preserves the command panel's mini editor text and visibility across reloads", -> @@ -119,6 +119,21 @@ describe "CommandPanel", -> expect(buffer.lineForRow(0)).toMatch /quicktorta/ expect(buffer.lineForRow(1)).toMatch /var torta/ + describe "when the command returns operations to be previewed", -> + fit "displays a preview of the operations above the mini-editor", -> + rootView.attachToDom() + editor.remove() + + rootView.trigger 'command-panel:toggle' + + commandPanel.miniEditor.insertText + + waitsForPromise -> commandPanel.execute('X x/a+/') + + runs -> + expect(commandPanel).toBeVisible() + expect(commandPanel.previewList).toBeVisible() + describe "if the command is malformed", -> it "adds and removes an error class to the command panel and does not close it", -> rootView.trigger 'command-panel:toggle' diff --git a/src/extensions/command-panel/command-panel.coffee b/src/extensions/command-panel/command-panel.coffee index 0cbde9cb2..da44344bd 100644 --- a/src/extensions/command-panel/command-panel.coffee +++ b/src/extensions/command-panel/command-panel.coffee @@ -2,6 +2,7 @@ CommandInterpreter = require 'command-panel/command-interpreter' RegexAddress = require 'command-panel/commands/regex-address' CompositeCommand = require 'command-panel/commands/composite-command' +PreviewItem = require 'command-panel/preview-item' Editor = require 'editor' {SyntaxError} = require('pegjs').parser @@ -16,6 +17,9 @@ class CommandPanel extends View else @instance = new CommandPanel(rootView) + @deactivate: -> + @instance.detach() + @serialize: -> text: @instance.miniEditor.getText() visible: @instance.hasParent() @@ -27,8 +31,10 @@ class CommandPanel extends View @content: -> @div class: 'command-panel', => - @div ':', class: 'prompt', outlet: 'prompt' - @subview 'miniEditor', new Editor(mini: true) + @ol class: 'preview-list', outlet: 'previewList' + @div class: 'prompt-and-editor', => + @div ':', class: 'prompt', outlet: 'prompt' + @subview 'miniEditor', new Editor(mini: true) commandInterpreter: null history: null @@ -54,16 +60,25 @@ class CommandPanel extends View attach: (text='') -> @rootView.append(this) + @previewList.hide() @miniEditor.focus() @miniEditor.setText(text) detach: -> @rootView.focus() + if @previewedOperations + operation.destroy() for operation in @previewedOperations super execute: (command = @miniEditor.getText()) -> try - @commandInterpreter.eval(command, @rootView.getActiveEditSession()) + @commandInterpreter.eval(command, @rootView.getActiveEditSession()).done (operations) => + @history.push(command) + @historyIndex = @history.length + if operations?.length + @populatePreviewList(operations) + else + @detach() catch error if error instanceof SyntaxError @flashError() @@ -71,9 +86,12 @@ class CommandPanel extends View else throw error - @history.push(command) - @historyIndex = @history.length - @detach() + populatePreviewList: (operations) -> + @previewedOperations = operations + @previewList.empty() + for operation in operations + @previewList.append(new PreviewItem(operation)) + @previewList.show() navigateBackwardInHistory: -> return if @historyIndex == 0 diff --git a/src/extensions/command-panel/commands/composite-command.coffee b/src/extensions/command-panel/commands/composite-command.coffee index c15cc9e2f..395c4ce11 100644 --- a/src/extensions/command-panel/commands/composite-command.coffee +++ b/src/extensions/command-panel/commands/composite-command.coffee @@ -21,10 +21,10 @@ class CompositeCommand @executeCommands(remainingCommands, project, editSession, nextRanges).done -> deferred.resolve() else - editSession?.clearAllSelections() unless currentCommand.preserveSelections if currentCommand.previewOperations deferred.resolve(operations) else + editSession?.clearAllSelections() unless currentCommand.preserveSelections for operation in operations operation.execute(editSession) operation.destroy() diff --git a/src/extensions/command-panel/operation.coffee b/src/extensions/command-panel/operation.coffee index 0f47201dc..154d1b4d9 100644 --- a/src/extensions/command-panel/operation.coffee +++ b/src/extensions/command-panel/operation.coffee @@ -14,6 +14,9 @@ class Operation @buffer.change(@getBufferRange(), @newText) if @newText editSession.addSelectionForBufferRange(@getBufferRange()) unless @preserveSelection + preview: -> + "sad :-(" + destroy: -> @buffer.release() @anchorRange.destroy() \ No newline at end of file diff --git a/src/extensions/command-panel/preview-item.coffee b/src/extensions/command-panel/preview-item.coffee new file mode 100644 index 000000000..a9b75c9ef --- /dev/null +++ b/src/extensions/command-panel/preview-item.coffee @@ -0,0 +1,8 @@ +{View} = require 'space-pen' + +module.exports = +class PreviewItem extends View + @content: (operation) -> + @li => + @span operation.getPath() + @span operation.preview() diff --git a/static/command-panel.css b/static/command-panel.css index faa942f54..50d218159 100644 --- a/static/command-panel.css +++ b/static/command-panel.css @@ -4,6 +4,14 @@ width: 100%; background: #515151; padding: 3px; +} + +.command-panel .preview-list { + max-height: 300px; + overflow: auto; +} + +.command-panel .prompt-and-editor { display: -webkit-box; } From 51b7b506871313028eb11bf6a92df31a9de4c8c5 Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Tue, 17 Jul 2012 20:11:43 -0700 Subject: [PATCH 48/97] Operations are initialized with the project --- src/extensions/command-panel/commands/address.coffee | 6 +++++- .../commands/select-all-matches-in-project.coffee | 6 +++++- .../command-panel/commands/select-all-matches.coffee | 6 +++++- src/extensions/command-panel/commands/substitution.coffee | 5 +++-- src/extensions/command-panel/operation.coffee | 6 ++++-- 5 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/extensions/command-panel/commands/address.coffee b/src/extensions/command-panel/commands/address.coffee index 9f3a7af5f..b15c07d63 100644 --- a/src/extensions/command-panel/commands/address.coffee +++ b/src/extensions/command-panel/commands/address.coffee @@ -7,7 +7,11 @@ class Address extends Command compile: (project, buffer, ranges) -> deferred = $.Deferred() deferred.resolve ranges.map (range) => - new Operation(buffer: buffer, bufferRange: @getRange(buffer, range)) + new Operation + project: project + buffer: buffer + bufferRange: @getRange(buffer, range) + deferred.promise() isAddress: -> true diff --git a/src/extensions/command-panel/commands/select-all-matches-in-project.coffee b/src/extensions/command-panel/commands/select-all-matches-in-project.coffee index e6e75e407..6e09abbe9 100644 --- a/src/extensions/command-panel/commands/select-all-matches-in-project.coffee +++ b/src/extensions/command-panel/commands/select-all-matches-in-project.coffee @@ -14,7 +14,11 @@ class SelectAllMatchesInProject extends Command deferred = $.Deferred() operations = [] promise = project.scan @regex, ({path, range}) -> - operations.push(new Operation(buffer: project.bufferForPath(path), bufferRange: range)) + operations.push(new Operation( + project: project + buffer: project.bufferForPath(path) + bufferRange: range + )) promise.done -> deferred.resolve(operations) deferred.promise() diff --git a/src/extensions/command-panel/commands/select-all-matches.coffee b/src/extensions/command-panel/commands/select-all-matches.coffee index f9d490bd7..fb5017f25 100644 --- a/src/extensions/command-panel/commands/select-all-matches.coffee +++ b/src/extensions/command-panel/commands/select-all-matches.coffee @@ -14,6 +14,10 @@ class SelectAllMatches extends Command operations = [] for range in ranges buffer.scanInRange @regex, range, (match, matchRange) -> - operations.push(new Operation(buffer: buffer, bufferRange: matchRange)) + operations.push(new Operation( + project: project + buffer: buffer + bufferRange: matchRange + )) deferred.resolve(operations) deferred.promise() diff --git a/src/extensions/command-panel/commands/substitution.coffee b/src/extensions/command-panel/commands/substitution.coffee index d96aaa762..403cb3731 100644 --- a/src/extensions/command-panel/commands/substitution.coffee +++ b/src/extensions/command-panel/commands/substitution.coffee @@ -18,8 +18,9 @@ class Substitution extends Command for range in ranges buffer.scanInRange @regex, range, (match, matchRange, { replace }) => operations.push(new Operation( - buffer: buffer, - bufferRange: matchRange, + project: project + buffer: buffer + bufferRange: matchRange newText: @replacementText preserveSelection: true )) diff --git a/src/extensions/command-panel/operation.coffee b/src/extensions/command-panel/operation.coffee index 154d1b4d9..17b40cbe2 100644 --- a/src/extensions/command-panel/operation.coffee +++ b/src/extensions/command-panel/operation.coffee @@ -1,11 +1,13 @@ +{$$$} = require 'space-pen' + module.exports = class Operation - constructor: ({@buffer, bufferRange, @newText, @preserveSelection}) -> + constructor: ({@project, @buffer, bufferRange, @newText, @preserveSelection}) -> @buffer.retain() @anchorRange = @buffer.addAnchorRange(bufferRange) getPath: -> - @buffer.getPath() + @project.relativize(@buffer.getPath()) getBufferRange: -> @anchorRange.getBufferRange() From bd9cbde54c8c02f39367561ffe30fc48a2063da8 Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Tue, 17 Jul 2012 20:12:21 -0700 Subject: [PATCH 49/97] More specific tests for command panel previews --- spec/extensions/command-panel-spec.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/extensions/command-panel-spec.coffee b/spec/extensions/command-panel-spec.coffee index 0997ac316..97ff029f3 100644 --- a/spec/extensions/command-panel-spec.coffee +++ b/spec/extensions/command-panel-spec.coffee @@ -133,6 +133,9 @@ describe "CommandPanel", -> runs -> expect(commandPanel).toBeVisible() expect(commandPanel.previewList).toBeVisible() + previewItem = commandPanel.previewList.find("li:contains(dir/a)").view() + expect(previewItem.path.text()).toBe "dir/a" + expect(previewItem.preview.text()).toMatch /a+/ describe "if the command is malformed", -> it "adds and removes an error class to the command panel and does not close it", -> From a5032e3811ec6cfeac41f262a109ecbc73b1a5ff Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Tue, 17 Jul 2012 20:12:57 -0700 Subject: [PATCH 50/97] PreviewItem displays matched text --- src/extensions/command-panel/operation.coffee | 3 ++- src/extensions/command-panel/preview-item.coffee | 5 +++-- static/command-panel.css | 14 ++++++++++++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/extensions/command-panel/operation.coffee b/src/extensions/command-panel/operation.coffee index 17b40cbe2..34b13fcdc 100644 --- a/src/extensions/command-panel/operation.coffee +++ b/src/extensions/command-panel/operation.coffee @@ -17,7 +17,8 @@ class Operation editSession.addSelectionForBufferRange(@getBufferRange()) unless @preserveSelection preview: -> - "sad :-(" + range = @anchorRange.getBufferRange() + @buffer.textInRange(range) destroy: -> @buffer.release() diff --git a/src/extensions/command-panel/preview-item.coffee b/src/extensions/command-panel/preview-item.coffee index a9b75c9ef..e8f9b9ded 100644 --- a/src/extensions/command-panel/preview-item.coffee +++ b/src/extensions/command-panel/preview-item.coffee @@ -4,5 +4,6 @@ module.exports = class PreviewItem extends View @content: (operation) -> @li => - @span operation.getPath() - @span operation.preview() + @span operation.getPath(), outlet: "path", class: "path" + @span outlet: "preview", class: "preview", -> + operation.preview() diff --git a/static/command-panel.css b/static/command-panel.css index 50d218159..ed6d89e4b 100644 --- a/static/command-panel.css +++ b/static/command-panel.css @@ -11,6 +11,20 @@ overflow: auto; } +.command-panel .preview-list .path{ + padding-left: 1em; + color: red; +} + +.command-panel .preview-list .preview { + padding-left: 1em; + color: green; +} + +.command-panel .preview-list .preview .match{ + font-weight: bold; +} + .command-panel .prompt-and-editor { display: -webkit-box; } From 86b0fee4f13c97dc1b2ef8fa639e3acf7dff2201 Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Tue, 17 Jul 2012 20:18:00 -0700 Subject: [PATCH 51/97] Make spec pass by using code that is not horribly written --- src/extensions/command-panel/operation.coffee | 2 +- src/extensions/command-panel/preview-item.coffee | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/extensions/command-panel/operation.coffee b/src/extensions/command-panel/operation.coffee index 34b13fcdc..56ebc3483 100644 --- a/src/extensions/command-panel/operation.coffee +++ b/src/extensions/command-panel/operation.coffee @@ -18,7 +18,7 @@ class Operation preview: -> range = @anchorRange.getBufferRange() - @buffer.textInRange(range) + @buffer.getTextInRange(range) destroy: -> @buffer.release() diff --git a/src/extensions/command-panel/preview-item.coffee b/src/extensions/command-panel/preview-item.coffee index e8f9b9ded..30f70a55b 100644 --- a/src/extensions/command-panel/preview-item.coffee +++ b/src/extensions/command-panel/preview-item.coffee @@ -5,5 +5,5 @@ class PreviewItem extends View @content: (operation) -> @li => @span operation.getPath(), outlet: "path", class: "path" - @span outlet: "preview", class: "preview", -> - operation.preview() + @span operation.preview(), outlet: "preview", class: "preview" + From 52d60df2726e2d65b80a919df110a619779bebfa Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Wed, 18 Jul 2012 09:22:32 -0700 Subject: [PATCH 52/97] Stylize command panel preview --- spec/extensions/command-panel-spec.coffee | 5 +++-- src/extensions/command-panel/operation.coffee | 7 ++++++- src/extensions/command-panel/preview-item.coffee | 8 +++++++- static/command-panel.css | 10 ++++++---- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/spec/extensions/command-panel-spec.coffee b/spec/extensions/command-panel-spec.coffee index 97ff029f3..f14ea5e2b 100644 --- a/spec/extensions/command-panel-spec.coffee +++ b/spec/extensions/command-panel-spec.coffee @@ -120,7 +120,7 @@ describe "CommandPanel", -> expect(buffer.lineForRow(1)).toMatch /var torta/ describe "when the command returns operations to be previewed", -> - fit "displays a preview of the operations above the mini-editor", -> + it "displays a preview of the operations above the mini-editor", -> rootView.attachToDom() editor.remove() @@ -135,7 +135,8 @@ describe "CommandPanel", -> expect(commandPanel.previewList).toBeVisible() previewItem = commandPanel.previewList.find("li:contains(dir/a)").view() expect(previewItem.path.text()).toBe "dir/a" - expect(previewItem.preview.text()).toMatch /a+/ + expect(previewItem.preview.text()).toBe "aaa bbb" + expect(previewItem.preview.find(".match").text()).toBe "aaa" describe "if the command is malformed", -> it "adds and removes an error class to the command panel and does not close it", -> diff --git a/src/extensions/command-panel/operation.coffee b/src/extensions/command-panel/operation.coffee index 56ebc3483..e2efc4f54 100644 --- a/src/extensions/command-panel/operation.coffee +++ b/src/extensions/command-panel/operation.coffee @@ -18,7 +18,12 @@ class Operation preview: -> range = @anchorRange.getBufferRange() - @buffer.getTextInRange(range) + line = @buffer.lineForRow(range.start.row) + prefix = line[0...range.start.column] + match = line[range.start.column...range.end.column] + suffix = line[range.end.column..] + + {prefix, suffix, match} destroy: -> @buffer.release() diff --git a/src/extensions/command-panel/preview-item.coffee b/src/extensions/command-panel/preview-item.coffee index 30f70a55b..dc72d2371 100644 --- a/src/extensions/command-panel/preview-item.coffee +++ b/src/extensions/command-panel/preview-item.coffee @@ -3,7 +3,13 @@ module.exports = class PreviewItem extends View @content: (operation) -> + {prefix, suffix, match} = operation.preview() + @li => @span operation.getPath(), outlet: "path", class: "path" - @span operation.preview(), outlet: "preview", class: "preview" + @span outlet: "preview", class: "preview", => + @span prefix + @span match, class: 'match' + @span suffix + diff --git a/static/command-panel.css b/static/command-panel.css index ed6d89e4b..89ffd8dd0 100644 --- a/static/command-panel.css +++ b/static/command-panel.css @@ -9,20 +9,22 @@ .command-panel .preview-list { max-height: 300px; overflow: auto; + padding-bottom: 3px; } .command-panel .preview-list .path{ - padding-left: 1em; - color: red; + padding-left: 3px; + color: #00ffff; } .command-panel .preview-list .preview { padding-left: 1em; - color: green; + color: #f6f3e8; } .command-panel .preview-list .preview .match{ - font-weight: bold; + background-color: #8E8A8A; + padding: 1px; } .command-panel .prompt-and-editor { From 9f004ad09c80cad6a1440d575a4750ce74bc8de3 Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Wed, 18 Jul 2012 10:40:34 -0700 Subject: [PATCH 53/97] retain and release methods return `this` --- src/app/buffer.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/buffer.coffee b/src/app/buffer.coffee index b8e7731cc..6914b835a 100644 --- a/src/app/buffer.coffee +++ b/src/app/buffer.coffee @@ -43,10 +43,12 @@ class Buffer retain: -> @refcount++ + this release: -> @refcount-- @destroy() if @refcount <= 0 + this getPath: -> @file?.getPath() From 522149c84d12bf881a6267dcd618cad644059f90 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Wed, 18 Jul 2012 11:06:13 -0700 Subject: [PATCH 54/97] Project.scan excludes ignored directories --- src/app/project.coffee | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/app/project.coffee b/src/app/project.coffee index 07e4a519c..c947b20aa 100644 --- a/src/app/project.coffee +++ b/src/app/project.coffee @@ -16,6 +16,7 @@ class Project autoIndent: null softTabs: null softWrap: null + ignoredPathRegexes: null constructor: (path) -> @setPath(path) @@ -23,6 +24,10 @@ class Project @setTabText(' ') @setAutoIndent(true) @setSoftTabs(true) + @ignoredPathRegexes = [ + /\.DS_Store$/ + /(^|\/)\.git(\/|$)/ + ] getPath: -> @rootDirectory?.path @@ -55,7 +60,10 @@ class Project deferred ignorePath: (path) -> - fs.base(path).match(/\.DS_Store/) or path.match(/(^|\/)\.git(\/|$)/) + _.find @ignoredPathRegexes, (regex) -> path.match(regex) + + ignorePathRegex: -> + @ignoredPathRegexes.map((regex) -> "(#{regex.source})").join("|") resolve: (filePath) -> filePath = fs.join(@getPath(), filePath) unless filePath[0] == '/' @@ -125,7 +133,13 @@ class Project scan: (regex, iterator) -> regex = new RegExp(regex.source, 'g') - command = "grep --null --perl-regexp --with-filename --line-number --recursive --regexp=\"#{regex.source}\" #{@getPath()}" + commands = [ + "find \"#{@getPath()}\" -type f" + "grep --perl-regexp --invert-match --regexp=\"#{@ignorePathRegex()}\"" + "xargs grep --null --perl-regexp --with-filename --line-number --recursive --regexp=\"#{regex.source}\"" + ] + + command = commands.join(" | ") ChildProcess.exec command, bufferLines: true, stdout: (data) -> for grepLine in data.split('\n') when grepLine.length nullCharIndex = grepLine.indexOf('\0') From d36873f70217864508fe9a030254e1ae2f61b308 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Wed, 18 Jul 2012 11:19:25 -0700 Subject: [PATCH 55/97] Rename Project.open to Project.buildEditSessionForPath --- benchmark/benchmark-suite.coffee | 10 ++++-- spec/app/edit-session-spec.coffee | 4 +-- spec/app/editor-spec.coffee | 36 +++++++++---------- spec/app/project-spec.coffee | 18 +++++----- spec/app/root-view-spec.coffee | 8 ++--- spec/extensions/autocomplete-spec.coffee | 6 ++-- .../command-interpreter-spec.coffee | 4 +-- spec/extensions/fuzzy-finder-spec.coffee | 2 +- src/app/edit-session.coffee | 2 +- src/app/project.coffee | 3 +- src/app/root-view.coffee | 4 +-- 11 files changed, 51 insertions(+), 46 deletions(-) diff --git a/benchmark/benchmark-suite.coffee b/benchmark/benchmark-suite.coffee index 1c70248aa..db66ec394 100644 --- a/benchmark/benchmark-suite.coffee +++ b/benchmark/benchmark-suite.coffee @@ -17,6 +17,10 @@ describe "editor.", -> window.shutdown() delete atom.rootViewStates[$windowNumber] + describe "opening-buffers.", -> + benchmark "300-line-file.", -> + buffer = rootView.project.bufferForPath('medium.coffee') + describe "empty-file.", -> benchmark "insert-delete", -> editor.insertText('x') @@ -24,7 +28,7 @@ describe "editor.", -> describe "300-line-file.", -> beforeEach -> - editor.edit rootView.project.open('medium.coffee') + editor.edit rootView.project.buildEditSessionForPath('medium.coffee') describe "at-begining.", -> benchmark "insert-delete", -> @@ -45,11 +49,11 @@ describe "editor.", -> describe "9000-line-file.", -> benchmark "opening.", 5, -> - editor.edit rootView.project.open('huge.js') + editor.edit rootView.project.buildEditSessionForPath('huge.js') describe "after-opening.", -> beforeEach -> - editor.edit rootView.project.open('huge.js') + editor.edit rootView.project.buildEditSessionForPath('huge.js') benchmark "moving-to-eof.", 1, -> editor.moveCursorToBottom() diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index f2efa7d53..796c4905c 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -7,7 +7,7 @@ describe "EditSession", -> beforeEach -> buffer = new Buffer() - editSession = fixturesProject.open('sample.js', autoIndent: false) + editSession = fixturesProject.buildEditSessionForPath('sample.js', autoIndent: false) buffer = editSession.buffer lineLengths = buffer.getLines().map (line) -> line.length @@ -1319,7 +1319,7 @@ describe "EditSession", -> expect(editSession.getSelectedBufferRanges()).toEqual [[[1, 6], [1, 6]], [[1, 18], [1, 18]]] it "restores selected ranges even when the change occurred in another edit session", -> - otherEditSession = fixturesProject.open(editSession.getPath()) + otherEditSession = fixturesProject.buildEditSessionForPath(editSession.getPath()) otherEditSession.setSelectedBufferRange([[2, 2], [3, 3]]) otherEditSession.delete() diff --git a/spec/app/editor-spec.coffee b/spec/app/editor-spec.coffee index 3bace9a2c..6600ca150 100644 --- a/spec/app/editor-spec.coffee +++ b/spec/app/editor-spec.coffee @@ -14,7 +14,7 @@ describe "Editor", -> getLineHeight = -> return cachedLineHeight if cachedLineHeight? - editorForMeasurement = new Editor(editSession: rootView.project.open('sample.js')) + editorForMeasurement = new Editor(editSession: rootView.project.buildEditSessionForPath('sample.js')) editorForMeasurement.attachToDom() cachedLineHeight = editorForMeasurement.lineHeight editorForMeasurement.remove() @@ -49,7 +49,7 @@ describe "Editor", -> rootView.height(8 * editor.lineHeight) rootView.width(50 * editor.charWidth) - editor.edit(rootView.project.open('two-hundred.txt')) + editor.edit(rootView.project.buildEditSessionForPath('two-hundred.txt')) editor.setCursorScreenPosition([5, 1]) editor.scrollTop(1.5 * editor.lineHeight) editor.scrollView.scrollLeft(44) @@ -113,7 +113,7 @@ describe "Editor", -> describe ".remove()", -> it "removes subscriptions from all edit session buffers", -> previousEditSession = editor.activeEditSession - otherEditSession = rootView.project.open(rootView.project.resolve('sample.txt')) + otherEditSession = rootView.project.buildEditSessionForPath(rootView.project.resolve('sample.txt')) expect(previousEditSession.buffer.subscriptionCount()).toBeGreaterThan 1 editor.edit(otherEditSession) @@ -125,7 +125,7 @@ describe "Editor", -> describe "when 'close' is triggered", -> it "closes active edit session and loads next edit session", -> - editor.edit(rootView.project.open()) + editor.edit(rootView.project.buildEditSessionForPath()) editSession = editor.activeEditSession spyOn(editSession, 'destroy').andCallThrough() spyOn(editor, "remove").andCallThrough() @@ -161,7 +161,7 @@ describe "Editor", -> otherEditSession = null beforeEach -> - otherEditSession = rootView.project.open() + otherEditSession = rootView.project.buildEditSessionForPath() describe "when the edit session wasn't previously assigned to this editor", -> it "adds edit session to editor", -> @@ -197,10 +197,10 @@ describe "Editor", -> beforeEach -> session0 = editor.activeEditSession - editor.edit(rootView.project.open('sample.txt')) + editor.edit(rootView.project.buildEditSessionForPath('sample.txt')) session1 = editor.activeEditSession - editor.edit(rootView.project.open('two-hundred.txt')) + editor.edit(rootView.project.buildEditSessionForPath('two-hundred.txt')) session2 = editor.activeEditSession describe ".setActiveEditSessionIndex(index)", -> @@ -277,7 +277,7 @@ describe "Editor", -> describe "when the current buffer has no path", -> selectedFilePath = null beforeEach -> - editor.edit(rootView.project.open()) + editor.edit(rootView.project.buildEditSessionForPath()) expect(editor.getPath()).toBeUndefined() editor.getBuffer().setText 'Save me to a new path' @@ -357,7 +357,7 @@ describe "Editor", -> spyOn(editor, 'pane').andReturn(fakePane) it "calls the corresponding split method on the containing pane with a new editor containing a copy of the active edit session", -> - editor.edit project.open("sample.txt") + editor.edit project.buildEditSessionForPath("sample.txt") editor.splitUp() expect(fakePane.splitUp).toHaveBeenCalled() [newEditor] = fakePane.splitUp.argsForCall[0] @@ -404,7 +404,7 @@ describe "Editor", -> it "emits event when editor receives a new buffer", -> eventHandler = jasmine.createSpy('eventHandler') editor.on 'editor-path-change', eventHandler - editor.edit(rootView.project.open(path)) + editor.edit(rootView.project.buildEditSessionForPath(path)) expect(eventHandler).toHaveBeenCalled() it "stops listening to events on previously set buffers", -> @@ -412,7 +412,7 @@ describe "Editor", -> oldBuffer = editor.getBuffer() editor.on 'editor-path-change', eventHandler - editor.edit(rootView.project.open(path)) + editor.edit(rootView.project.buildEditSessionForPath(path)) expect(eventHandler).toHaveBeenCalled() eventHandler.reset() @@ -1010,7 +1010,7 @@ describe "Editor", -> expect(editor.bufferPositionForScreenPosition(editor.getCursorScreenPosition())).toEqual [3, 60] it "does not wrap the lines of any newly assigned buffers", -> - otherEditSession = rootView.project.open() + otherEditSession = rootView.project.buildEditSessionForPath() otherEditSession.buffer.setText([1..100].join('')) editor.edit(otherEditSession) expect(editor.renderedLines.find('.line').length).toBe(1) @@ -1046,7 +1046,7 @@ describe "Editor", -> expect(editor.getCursorScreenPosition()).toEqual [11, 0] it "calls .setSoftWrapColumn() when the editor is attached because now its dimensions are available to calculate it", -> - otherEditor = new Editor(editSession: rootView.project.open('sample.js')) + otherEditor = new Editor(editSession: rootView.project.buildEditSessionForPath('sample.js')) spyOn(otherEditor, 'setSoftWrapColumn') otherEditor.setSoftWrap(true) @@ -1344,7 +1344,7 @@ describe "Editor", -> describe "when autoscrolling at the end of the document", -> it "renders lines properly", -> - editor.edit(rootView.project.open('two-hundred.txt')) + editor.edit(rootView.project.buildEditSessionForPath('two-hundred.txt')) editor.attachToDom(heightInLines: 5.5) expect(editor.renderedLines.find('.line').length).toBe 8 @@ -1510,7 +1510,7 @@ describe "Editor", -> describe "folding", -> beforeEach -> - editSession = rootView.project.open('two-hundred.txt') + editSession = rootView.project.buildEditSessionForPath('two-hundred.txt') buffer = editSession.buffer editor.edit(editSession) editor.attachToDom() @@ -1593,8 +1593,8 @@ describe "Editor", -> describe ".getOpenBufferPaths()", -> it "returns the paths of all non-anonymous buffers with edit sessions on this editor", -> - editor.edit(project.open('sample.txt')) - editor.edit(project.open('two-hundred.txt')) - editor.edit(project.open()) + editor.edit(project.buildEditSessionForPath('sample.txt')) + editor.edit(project.buildEditSessionForPath('two-hundred.txt')) + editor.edit(project.buildEditSessionForPath()) paths = editor.getOpenBufferPaths().map (path) -> project.relativize(path) expect(paths).toEqual = ['sample.js', 'sample.txt', 'two-hundred.txt'] diff --git a/spec/app/project-spec.coffee b/spec/app/project-spec.coffee index 8f3a13e0e..19d0f97eb 100644 --- a/spec/app/project-spec.coffee +++ b/spec/app/project-spec.coffee @@ -11,8 +11,8 @@ describe "Project", -> describe "when editSession is destroyed", -> it "removes edit session and calls destroy on buffer (if buffer is not referenced by other edit sessions)", -> - editSession = project.open("a") - anotherEditSession = project.open("a") + editSession = project.buildEditSessionForPath("a") + anotherEditSession = project.buildEditSessionForPath("a") buffer = editSession.buffer spyOn(buffer, 'destroy').andCallThrough() @@ -27,7 +27,7 @@ describe "Project", -> expect(buffer.destroy).toHaveBeenCalled() expect(project.editSessions.length).toBe 0 - describe ".open(path)", -> + describe ".buildEditSessionForPath(path)", -> [absolutePath, newBufferHandler, newEditSessionHandler] = [] beforeEach -> absolutePath = require.resolve('fixtures/dir/a') @@ -38,30 +38,30 @@ describe "Project", -> describe "when given an absolute path that hasn't been opened previously", -> it "returns a new edit session for the given path and emits 'new-buffer' and 'new-edit-session' events", -> - editSession = project.open(absolutePath) + editSession = project.buildEditSessionForPath(absolutePath) expect(editSession.buffer.getPath()).toBe absolutePath expect(newBufferHandler).toHaveBeenCalledWith editSession.buffer expect(newEditSessionHandler).toHaveBeenCalledWith editSession describe "when given a relative path that hasn't been opened previously", -> it "returns a new edit session for the given path (relative to the project root) and emits 'new-buffer' and 'new-edit-session' events", -> - editSession = project.open('a') + editSession = project.buildEditSessionForPath('a') expect(editSession.buffer.getPath()).toBe absolutePath expect(newBufferHandler).toHaveBeenCalledWith editSession.buffer expect(newEditSessionHandler).toHaveBeenCalledWith editSession describe "when passed the path to a buffer that has already been opened", -> it "returns a new edit session containing previously opened buffer and emits a 'new-edit-session' event", -> - editSession = project.open(absolutePath) + editSession = project.buildEditSessionForPath(absolutePath) newBufferHandler.reset() - expect(project.open(absolutePath).buffer).toBe editSession.buffer - expect(project.open('a').buffer).toBe editSession.buffer + expect(project.buildEditSessionForPath(absolutePath).buffer).toBe editSession.buffer + expect(project.buildEditSessionForPath('a').buffer).toBe editSession.buffer expect(newBufferHandler).not.toHaveBeenCalled() expect(newEditSessionHandler).toHaveBeenCalledWith editSession describe "when not passed a path", -> it "returns a new edit session and emits 'new-buffer' and 'new-edit-session' events", -> - editSession = project.open() + editSession = project.buildEditSessionForPath() expect(editSession.buffer.getPath()).toBeUndefined() expect(newBufferHandler).toHaveBeenCalledWith(editSession.buffer) expect(newEditSessionHandler).toHaveBeenCalledWith editSession diff --git a/spec/app/root-view-spec.coffee b/spec/app/root-view-spec.coffee index 5bf2e5b80..ca5f85302 100644 --- a/spec/app/root-view-spec.coffee +++ b/spec/app/root-view-spec.coffee @@ -75,10 +75,10 @@ describe "RootView", -> editor2 = editor1.splitRight() editor3 = editor2.splitRight() editor4 = editor2.splitDown() - editor2.edit(rootView.project.open('dir/b')) - editor3.edit(rootView.project.open('sample.js')) + editor2.edit(rootView.project.buildEditSessionForPath('dir/b')) + editor3.edit(rootView.project.buildEditSessionForPath('sample.js')) editor3.setCursorScreenPosition([2, 3]) - editor4.edit(rootView.project.open('sample.txt')) + editor4.edit(rootView.project.buildEditSessionForPath('sample.txt')) editor4.setCursorScreenPosition([0, 2]) rootView.attachToDom() editor2.focus() @@ -455,7 +455,7 @@ describe "RootView", -> editor2 = rootView.getActiveEditor().splitLeft() path = rootView.project.resolve('b') - editor2.edit(rootView.project.open(path)) + editor2.edit(rootView.project.buildEditSessionForPath(path)) expect(pathChangeHandler).toHaveBeenCalled() expect(document.title).toBe rootView.project.resolve(path) diff --git a/spec/extensions/autocomplete-spec.coffee b/spec/extensions/autocomplete-spec.coffee index 5e865c8b1..cb8c6585a 100644 --- a/spec/extensions/autocomplete-spec.coffee +++ b/spec/extensions/autocomplete-spec.coffee @@ -10,7 +10,7 @@ describe "Autocomplete", -> miniEditor = null beforeEach -> - editor = new Editor(editSession: fixturesProject.open('sample.js')) + editor = new Editor(editSession: fixturesProject.buildEditSessionForPath('sample.js')) autocomplete = new Autocomplete(editor) miniEditor = autocomplete.miniEditor @@ -357,7 +357,7 @@ describe "Autocomplete", -> expect(wordList).toContain "quicksort" expect(wordList).not.toContain "Some" - editor.edit(fixturesProject.open('sample.txt')) + editor.edit(fixturesProject.buildEditSessionForPath('sample.txt')) wordList = autocomplete.wordList expect(wordList).not.toContain "quicksort" @@ -365,7 +365,7 @@ describe "Autocomplete", -> it 'stops listening to previous buffers change events', -> previousBuffer = editor.getBuffer() - editor.edit(fixturesProject.open('sample.txt')) + editor.edit(fixturesProject.buildEditSessionForPath('sample.txt')) spyOn(autocomplete, "buildWordList") previousBuffer.change([[0,0],[0,1]], "sauron") diff --git a/spec/extensions/command-interpreter-spec.coffee b/spec/extensions/command-interpreter-spec.coffee index 414948c4c..143f64754 100644 --- a/spec/extensions/command-interpreter-spec.coffee +++ b/spec/extensions/command-interpreter-spec.coffee @@ -9,7 +9,7 @@ describe "CommandInterpreter", -> beforeEach -> project = new Project(fixturesProject.resolve('dir/')) interpreter = new CommandInterpreter(fixturesProject) - editSession = fixturesProject.open('sample.js') + editSession = fixturesProject.buildEditSessionForPath('sample.js') buffer = editSession.buffer afterEach -> @@ -320,7 +320,7 @@ describe "CommandInterpreter", -> runs -> expect(operations.length).toBeGreaterThan 3 for operation in operations - editSession = project.open(operation.getPath()) + editSession = project.buildEditSessionForPath(operation.getPath()) operation.execute(editSession) expect(editSession.getSelectedText()).toMatch /a+/ editSession.destroy() diff --git a/spec/extensions/fuzzy-finder-spec.coffee b/spec/extensions/fuzzy-finder-spec.coffee index fb5c1a999..d5b2d6552 100644 --- a/spec/extensions/fuzzy-finder-spec.coffee +++ b/spec/extensions/fuzzy-finder-spec.coffee @@ -122,7 +122,7 @@ describe 'FuzzyFinder', -> describe "when the active editor only contains edit sessions for anonymous buffers", -> it "does not open", -> editor = rootView.getActiveEditor() - editor.edit(rootView.project.open()) + editor.edit(rootView.project.buildEditSessionForPath()) editor.loadPreviousEditSession() editor.destroyActiveEditSession() expect(editor.getOpenBufferPaths().length).toBe 0 diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index 40fe58d83..67b1a8857 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -14,7 +14,7 @@ class EditSession @idCounter: 1 @deserialize: (state, project) -> - session = project.open(state.buffer) + session = project.buildEditSessionForPath(state.buffer) session.setScrollTop(state.scrollTop) session.setScrollLeft(state.scrollLeft) session.setCursorScreenPosition(state.cursorScreenPosition) diff --git a/src/app/project.coffee b/src/app/project.coffee index c947b20aa..503458548 100644 --- a/src/app/project.coffee +++ b/src/app/project.coffee @@ -21,6 +21,7 @@ class Project constructor: (path) -> @setPath(path) @editSessions = [] + @buffer = [] @setTabText(' ') @setAutoIndent(true) @setSoftTabs(true) @@ -84,7 +85,7 @@ class Project getSoftWrap: -> @softWrap setSoftWrap: (@softWrap) -> - open: (filePath, editSessionOptions={}) -> + buildEditSessionForPath: (filePath, editSessionOptions={}) -> @buildEditSession(@bufferForPath(filePath), editSessionOptions) buildEditSession: (buffer, editSessionOptions) -> diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index 6115f9a8d..d8dca8e63 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -96,7 +96,7 @@ class RootView extends View allowActiveEditorChange = options.allowActiveEditorChange ? false unless editSession = @openInExistingEditor(path, allowActiveEditorChange) - editSession = @project.open(path) + editSession = @project.buildEditSessionForPath(path) editor = new Editor({editSession}) pane = new Pane(editor) @panes.append(pane) @@ -120,7 +120,7 @@ class RootView extends View editor.focus() return editSession - editSession = @project.open(path) + editSession = @project.buildEditSessionForPath(path) activeEditor.edit(editSession) editSession From af62c081a13a0e25c8112dde2ea902b78da172d8 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Wed, 18 Jul 2012 11:54:23 -0700 Subject: [PATCH 56/97] Remove unneeded code --- spec/extensions/autocomplete-spec.coffee | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/extensions/autocomplete-spec.coffee b/spec/extensions/autocomplete-spec.coffee index cb8c6585a..442390f47 100644 --- a/spec/extensions/autocomplete-spec.coffee +++ b/spec/extensions/autocomplete-spec.coffee @@ -38,8 +38,7 @@ describe "Autocomplete", -> expect(Autocomplete.prototype.initialize).not.toHaveBeenCalled() - leftEditor.remove() - rightEditor.remove() + rootView.deactivate() describe 'autocomplete:attach event', -> it "shows autocomplete view and focuses its mini-editor", -> From 72750fd4a248517711f8c7ffce38fefb260345b9 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Wed, 18 Jul 2012 12:45:28 -0700 Subject: [PATCH 57/97] RootView.getEditors() only returns editors that are direct descendants of a pane --- src/app/root-view.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index d8dca8e63..11ff02dff 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -145,7 +145,7 @@ class RootView extends View document.title = title getEditors: -> - @panes.find('.editor').map(-> $(this).view()).toArray() + @panes.find('.pane > .editor').map(-> $(this).view()).toArray() getModifiedBuffers: -> modifiedBuffers = [] From b959d5aa374f130f0ffd1654205d80a1a48abcf7 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Wed, 18 Jul 2012 12:49:18 -0700 Subject: [PATCH 58/97] Buffers are memoized on project by path --- spec/app/buffer-spec.coffee | 35 ++++++++++++++++++----------------- spec/app/project-spec.coffee | 18 ++++++++++++++---- spec/spec-helper.coffee | 1 + src/app/buffer.coffee | 16 ++++++++++------ src/app/project.coffee | 18 +++++++++++------- 5 files changed, 54 insertions(+), 34 deletions(-) diff --git a/spec/app/buffer-spec.coffee b/spec/app/buffer-spec.coffee index 591870bfb..41f89ce83 100644 --- a/spec/app/buffer-spec.coffee +++ b/spec/app/buffer-spec.coffee @@ -11,11 +11,11 @@ describe 'Buffer', -> buffer = new Buffer(filePath) afterEach -> - buffer?.destroy() + buffer?.release() describe 'constructor', -> beforeEach -> - buffer.destroy() + buffer.release() describe "when given a path", -> describe "when a file exists for the path", -> @@ -56,8 +56,8 @@ describe 'Buffer', -> beforeEach -> path = "/tmp/tmp.txt" fs.write(path, "first") - buffer.destroy() - buffer = new Buffer(path) + buffer.release() + buffer = new Buffer(path).retain() afterEach -> fs.remove(path) @@ -99,7 +99,7 @@ describe 'Buffer', -> it "returns false after modified buffer is saved", -> filePath = "/tmp/atom-tmp-file" fs.write(filePath, '') - buffer.destroy() + buffer.release() buffer = new Buffer(filePath) expect(buffer.isModified()).toBe false @@ -243,7 +243,7 @@ describe 'Buffer', -> describe ".save()", -> beforeEach -> - buffer.destroy() + buffer.release() describe "when the buffer has a path", -> filePath = null @@ -283,33 +283,34 @@ describe 'Buffer', -> expect(-> buffer.save()).toThrow() describe ".saveAs(path)", -> - filePath = null + [filePath, saveAsBuffer] = [] - beforeEach -> - buffer.destroy() + afterEach -> + saveAsBuffer.release() it "saves the contents of the buffer to the path", -> filePath = '/tmp/temp.txt' fs.remove filePath if fs.exists(filePath) - buffer = new Buffer() + saveAsBuffer = new Buffer().retain() eventHandler = jasmine.createSpy('eventHandler') - buffer.on 'path-change', eventHandler + saveAsBuffer.on 'path-change', eventHandler - buffer.setText 'Buffer contents!' - buffer.saveAs(filePath) + saveAsBuffer.setText 'Buffer contents!' + saveAsBuffer.saveAs(filePath) expect(fs.read(filePath)).toEqual 'Buffer contents!' - expect(eventHandler).toHaveBeenCalledWith(buffer) + expect(eventHandler).toHaveBeenCalledWith(saveAsBuffer) it "stops listening to events on previous path and begins listening to events on new path", -> originalPath = "/tmp/original.txt" newPath = "/tmp/new.txt" fs.write(originalPath, "") - buffer = new Buffer(originalPath) + + saveAsBuffer = new Buffer(originalPath).retain() changeHandler = jasmine.createSpy('changeHandler') - buffer.on 'change', changeHandler - buffer.saveAs(newPath) + saveAsBuffer.on 'change', changeHandler + saveAsBuffer.saveAs(newPath) expect(changeHandler).not.toHaveBeenCalled() fs.write(originalPath, "should not trigger buffer event") diff --git a/spec/app/project-spec.coffee b/spec/app/project-spec.coffee index 19d0f97eb..0fa2045bc 100644 --- a/spec/app/project-spec.coffee +++ b/spec/app/project-spec.coffee @@ -13,18 +13,14 @@ describe "Project", -> it "removes edit session and calls destroy on buffer (if buffer is not referenced by other edit sessions)", -> editSession = project.buildEditSessionForPath("a") anotherEditSession = project.buildEditSessionForPath("a") - buffer = editSession.buffer - spyOn(buffer, 'destroy').andCallThrough() expect(project.editSessions.length).toBe 2 expect(editSession.buffer).toBe anotherEditSession.buffer editSession.destroy() - expect(buffer.destroy).not.toHaveBeenCalled() expect(project.editSessions.length).toBe 1 anotherEditSession.destroy() - expect(buffer.destroy).toHaveBeenCalled() expect(project.editSessions.length).toBe 0 describe ".buildEditSessionForPath(path)", -> @@ -66,6 +62,20 @@ describe "Project", -> expect(newBufferHandler).toHaveBeenCalledWith(editSession.buffer) expect(newEditSessionHandler).toHaveBeenCalledWith editSession + describe ".bufferForPath(path)", -> + describe "when opening a previously opened path", -> + it "does not create a new buffer", -> + buffer = project.bufferForPath("a").retain() + expect(project.bufferForPath("a")).toBe buffer + + alternativeBuffer = project.bufferForPath("b").retain().release() + expect(alternativeBuffer).not.toBe buffer + buffer.release() + + it "creates a new buffer if the previous buffer was destroyed", -> + buffer = project.bufferForPath("a").retain().release() + expect(project.bufferForPath("a").retain().release()).not.toBe buffer + describe ".resolve(path)", -> it "returns an absolute path based on the project's root", -> absolutePath = require.resolve('fixtures/dir/a') diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index eb82d2c3b..c559a1cce 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -26,6 +26,7 @@ afterEach -> $('#jasmine-content').empty() document.title = defaultTitle ensureNoPathSubscriptions() + window.fixturesProject.destroy() window.keymap.bindKeys '*', 'meta-w': 'close' $(document).on 'close', -> window.close() diff --git a/src/app/buffer.coffee b/src/app/buffer.coffee index 6914b835a..7a815549c 100644 --- a/src/app/buffer.coffee +++ b/src/app/buffer.coffee @@ -20,7 +20,7 @@ class Buffer anchorRanges: null refcount: 0 - constructor: (path) -> + constructor: (path, @project) -> @id = @constructor.idCounter++ @anchors = [] @anchorRanges = [] @@ -38,8 +38,9 @@ class Buffer destroy: -> throw new Error("Destroying buffer twice with path '#{@getPath()}'") if @destroyed - @destroyed = true @file?.off() + @destroyed = true + @project?.removeBuffer(this) retain: -> @refcount++ @@ -50,6 +51,12 @@ class Buffer @destroy() if @refcount <= 0 this + subscribeToFile: -> + @file?.on "contents-change", => + unless @isModified() + @setText(fs.read(@file.getPath())) + @modified = false + getPath: -> @file?.getPath() @@ -58,10 +65,7 @@ class Buffer @file?.off() @file = new File(path) - @file.on "contents-change", => - unless @isModified() - @setText(fs.read(@file.getPath())) - @modified = false + @subscribeToFile() @trigger "path-change", this getExtension: -> diff --git a/src/app/project.coffee b/src/app/project.coffee index 503458548..9098c7e26 100644 --- a/src/app/project.coffee +++ b/src/app/project.coffee @@ -21,7 +21,7 @@ class Project constructor: (path) -> @setPath(path) @editSessions = [] - @buffer = [] + @buffers = [] @setTabText(' ') @setAutoIndent(true) @setSoftTabs(true) @@ -30,6 +30,9 @@ class Project /(^|\/)\.git(\/|$)/ ] + destroy: -> + editSession.destroy() for editSession in @getEditSessions() + getPath: -> @rootDirectory?.path @@ -106,9 +109,6 @@ class Project getEditSessions: -> new Array(@editSessions...) - destroy: -> - editSession.destroy() for editSession in @getEditSessions() - removeEditSession: (editSession) -> _.remove(@editSessions, editSession) @@ -122,16 +122,20 @@ class Project bufferForPath: (filePath) -> if filePath? filePath = @resolve(filePath) - return editSession.buffer for editSession in @editSessions when editSession.buffer.getPath() == filePath - @buildBuffer(filePath) + buffer = _.find @buffers, (buffer) -> buffer.getPath() == filePath + buffer or @buildBuffer(filePath) else @buildBuffer() buildBuffer: (filePath) -> - buffer = new Buffer(filePath) + buffer = new Buffer(filePath, this) + @buffers.push buffer @trigger 'new-buffer', buffer buffer + removeBuffer: (buffer) -> + _.remove(@buffers, buffer) + scan: (regex, iterator) -> regex = new RegExp(regex.source, 'g') commands = [ From 12a576ff9564c007f801762ab8a18cf811403c7f Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Wed, 18 Jul 2012 15:19:55 -0600 Subject: [PATCH 59/97] Command panel can be detached after preview without throwing an exception --- spec/extensions/command-panel-spec.coffee | 2 ++ src/extensions/command-panel/command-panel.coffee | 1 + 2 files changed, 3 insertions(+) diff --git a/spec/extensions/command-panel-spec.coffee b/spec/extensions/command-panel-spec.coffee index f14ea5e2b..d102ae32d 100644 --- a/spec/extensions/command-panel-spec.coffee +++ b/spec/extensions/command-panel-spec.coffee @@ -138,6 +138,8 @@ describe "CommandPanel", -> expect(previewItem.preview.text()).toBe "aaa bbb" expect(previewItem.preview.find(".match").text()).toBe "aaa" + rootView.trigger 'command-panel:toggle' + describe "if the command is malformed", -> it "adds and removes an error class to the command panel and does not close it", -> rootView.trigger 'command-panel:toggle' diff --git a/src/extensions/command-panel/command-panel.coffee b/src/extensions/command-panel/command-panel.coffee index da44344bd..1f0967ec1 100644 --- a/src/extensions/command-panel/command-panel.coffee +++ b/src/extensions/command-panel/command-panel.coffee @@ -68,6 +68,7 @@ class CommandPanel extends View @rootView.focus() if @previewedOperations operation.destroy() for operation in @previewedOperations + @previewedOperations = undefined super execute: (command = @miniEditor.getText()) -> From 4b1403724b2e07ce6bfdcae7a2c67c334f30aaae Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Wed, 18 Jul 2012 16:22:21 -0600 Subject: [PATCH 60/97] Ensure that evil filenames do not hose Project.scan --- spec/app/project-spec.coffee | 7 +++++++ spec/extensions/tree-view-spec.coffee | 10 +++++----- spec/fixtures/evil-files/file with spaces.txt | 0 "spec/fixtures/evil-files/goddam\nlinefeeds" | 1 + "spec/fixtures/evil-files/quote\".js" | 1 + spec/fixtures/evil-files/utfă.md | 3 +++ src/app/project.coffee | 12 ++++++------ 7 files changed, 23 insertions(+), 11 deletions(-) create mode 100644 spec/fixtures/evil-files/file with spaces.txt create mode 100644 "spec/fixtures/evil-files/goddam\nlinefeeds" create mode 100644 "spec/fixtures/evil-files/quote\".js" create mode 100644 spec/fixtures/evil-files/utfă.md diff --git a/spec/app/project-spec.coffee b/spec/app/project-spec.coffee index 0fa2045bc..e1bd3c48b 100644 --- a/spec/app/project-spec.coffee +++ b/spec/app/project-spec.coffee @@ -135,3 +135,10 @@ describe "Project", -> path: project.resolve('a') match: ['aa', 'a'] range: [[1, 3], [1, 5]] + + it "works on evil filenames", -> + project.setPath(require.resolve('fixtures/evil-files')) + waitsForPromise -> + project.scan /(a)+/, ({path, match, range}) -> + matches.push({path, match, range}) + diff --git a/spec/extensions/tree-view-spec.coffee b/spec/extensions/tree-view-spec.coffee index 12aa90cd1..467d70757 100644 --- a/spec/extensions/tree-view-spec.coffee +++ b/spec/extensions/tree-view-spec.coffee @@ -31,12 +31,12 @@ describe "TreeView", -> expect(treeView.root.find('> .header .name')).toHaveText('fixtures/') rootEntries = treeView.root.find('.entries') - subdir1 = rootEntries.find('> li:eq(0)') - expect(subdir1.find('.disclosure-arrow')).toHaveText('▸') - expect(subdir1.find('.name')).toHaveText('dir/') - expect(subdir1.find('.entries')).not.toExist() + subdir0 = rootEntries.find('> li:eq(0)') + expect(subdir0.find('.disclosure-arrow')).toHaveText('▸') + expect(subdir0.find('.name')).toHaveText('dir/') + expect(subdir0.find('.entries')).not.toExist() - subdir2 = rootEntries.find('> li:eq(1)') + subdir2 = rootEntries.find('> li:eq(2)') expect(subdir2.find('.disclosure-arrow')).toHaveText('▸') expect(subdir2.find('.name')).toHaveText('zed/') expect(subdir2.find('.entries')).not.toExist() diff --git a/spec/fixtures/evil-files/file with spaces.txt b/spec/fixtures/evil-files/file with spaces.txt new file mode 100644 index 000000000..e69de29bb diff --git "a/spec/fixtures/evil-files/goddam\nlinefeeds" "b/spec/fixtures/evil-files/goddam\nlinefeeds" new file mode 100644 index 000000000..9092125e2 --- /dev/null +++ "b/spec/fixtures/evil-files/goddam\nlinefeeds" @@ -0,0 +1 @@ +you know how we do it diff --git "a/spec/fixtures/evil-files/quote\".js" "b/spec/fixtures/evil-files/quote\".js" new file mode 100644 index 000000000..fc0196531 --- /dev/null +++ "b/spec/fixtures/evil-files/quote\".js" @@ -0,0 +1 @@ +// I am evil because there's a " in my filename. Why you do that!? \ No newline at end of file diff --git a/spec/fixtures/evil-files/utfă.md b/spec/fixtures/evil-files/utfă.md new file mode 100644 index 000000000..aca94adc6 --- /dev/null +++ b/spec/fixtures/evil-files/utfă.md @@ -0,0 +1,3 @@ +# Hello Word + +This is markdown. diff --git a/src/app/project.coffee b/src/app/project.coffee index 9098c7e26..d7050a1c2 100644 --- a/src/app/project.coffee +++ b/src/app/project.coffee @@ -138,13 +138,13 @@ class Project scan: (regex, iterator) -> regex = new RegExp(regex.source, 'g') - commands = [ - "find \"#{@getPath()}\" -type f" - "grep --perl-regexp --invert-match --regexp=\"#{@ignorePathRegex()}\"" - "xargs grep --null --perl-regexp --with-filename --line-number --recursive --regexp=\"#{regex.source}\"" - ] + command = [ + "find \"#{@getPath()}\" -type f -print0" # find all paths in the project's working directory + "grep --text --perl-regexp --invert-match --regexp=\"#{@ignorePathRegex()}\"" # accept only non-ignored paths, separated by \0 (find doesn't support pcre) + "perl -0pi -e 's/\n$//'" # delete grep's trailing newline because it screws up xargs + "xargs -0 grep --null --perl-regexp --with-filename --line-number --recursive --regexp=\"#{regex.source}\"" # run grep on each filtered file + ].join(" | ") - command = commands.join(" | ") ChildProcess.exec command, bufferLines: true, stdout: (data) -> for grepLine in data.split('\n') when grepLine.length nullCharIndex = grepLine.indexOf('\0') From 8534df28d586136df25b90733d0577cf114e68eb Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Wed, 18 Jul 2012 18:04:39 -0600 Subject: [PATCH 61/97] Another attempt with find/grep/perl/xargs that doesn't work --- src/app/project.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/project.coffee b/src/app/project.coffee index d7050a1c2..940ad7d2c 100644 --- a/src/app/project.coffee +++ b/src/app/project.coffee @@ -141,7 +141,7 @@ class Project command = [ "find \"#{@getPath()}\" -type f -print0" # find all paths in the project's working directory "grep --text --perl-regexp --invert-match --regexp=\"#{@ignorePathRegex()}\"" # accept only non-ignored paths, separated by \0 (find doesn't support pcre) - "perl -0pi -e 's/\n$//'" # delete grep's trailing newline because it screws up xargs + "perl -0pi -e 's/\\n\\z\//'" # delete grep's trailing newline because it screws up xargs "xargs -0 grep --null --perl-regexp --with-filename --line-number --recursive --regexp=\"#{regex.source}\"" # run grep on each filtered file ].join(" | ") From f5e46e57fc3e67c5cde534f13092ea6802c581dc Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 19 Jul 2012 08:54:08 -0600 Subject: [PATCH 62/97] Use bundled ack instead of grep Our ack is slightly modified to use `'\0'` characters instead of `':'` in the --nogroup listing. This makes it easier to handle both `':'` characters and newlines in filenames, but we aren't really doing that yet. --- spec/app/project-spec.coffee | 5 +- spec/fixtures/evil-files/file with spaces.txt | 1 + "spec/fixtures/evil-files/goddam\nlinefeeds" | 1 - "spec/fixtures/evil-files/goddam\nnewlines" | 1 + "spec/fixtures/evil-files/quote\".js" | 1 - "spec/fixtures/evil-files/quote\".txt" | 1 + spec/fixtures/evil-files/utfă.md | 4 +- src/app/project.coffee | 20 +- src/stdlib/require.coffee | 4 + vendor/ack | 2784 +++++++++++++++++ 10 files changed, 2801 insertions(+), 21 deletions(-) delete mode 100644 "spec/fixtures/evil-files/goddam\nlinefeeds" create mode 100644 "spec/fixtures/evil-files/goddam\nnewlines" delete mode 100644 "spec/fixtures/evil-files/quote\".js" create mode 100644 "spec/fixtures/evil-files/quote\".txt" create mode 100755 vendor/ack diff --git a/spec/app/project-spec.coffee b/spec/app/project-spec.coffee index e1bd3c48b..8a79e3931 100644 --- a/spec/app/project-spec.coffee +++ b/spec/app/project-spec.coffee @@ -139,6 +139,5 @@ describe "Project", -> it "works on evil filenames", -> project.setPath(require.resolve('fixtures/evil-files')) waitsForPromise -> - project.scan /(a)+/, ({path, match, range}) -> - matches.push({path, match, range}) - + project.scan /evil/, ({path, match, range}) -> + #console.log path diff --git a/spec/fixtures/evil-files/file with spaces.txt b/spec/fixtures/evil-files/file with spaces.txt index e69de29bb..140c7bf61 100644 --- a/spec/fixtures/evil-files/file with spaces.txt +++ b/spec/fixtures/evil-files/file with spaces.txt @@ -0,0 +1 @@ +I am evil because there are spaces in my name diff --git "a/spec/fixtures/evil-files/goddam\nlinefeeds" "b/spec/fixtures/evil-files/goddam\nlinefeeds" deleted file mode 100644 index 9092125e2..000000000 --- "a/spec/fixtures/evil-files/goddam\nlinefeeds" +++ /dev/null @@ -1 +0,0 @@ -you know how we do it diff --git "a/spec/fixtures/evil-files/goddam\nnewlines" "b/spec/fixtures/evil-files/goddam\nnewlines" new file mode 100644 index 000000000..00c5620b8 --- /dev/null +++ "b/spec/fixtures/evil-files/goddam\nnewlines" @@ -0,0 +1 @@ +I am evil because there's a newline in my name diff --git "a/spec/fixtures/evil-files/quote\".js" "b/spec/fixtures/evil-files/quote\".js" deleted file mode 100644 index fc0196531..000000000 --- "a/spec/fixtures/evil-files/quote\".js" +++ /dev/null @@ -1 +0,0 @@ -// I am evil because there's a " in my filename. Why you do that!? \ No newline at end of file diff --git "a/spec/fixtures/evil-files/quote\".txt" "b/spec/fixtures/evil-files/quote\".txt" new file mode 100644 index 000000000..5e51d8e20 --- /dev/null +++ "b/spec/fixtures/evil-files/quote\".txt" @@ -0,0 +1 @@ +I am evil because there's a " in my filename. Why you do that!? \ No newline at end of file diff --git a/spec/fixtures/evil-files/utfă.md b/spec/fixtures/evil-files/utfă.md index aca94adc6..782a5bf5a 100644 --- a/spec/fixtures/evil-files/utfă.md +++ b/spec/fixtures/evil-files/utfă.md @@ -1,3 +1 @@ -# Hello Word - -This is markdown. +I am evil because there's a UTF-8 character in my name diff --git a/src/app/project.coffee b/src/app/project.coffee index 940ad7d2c..d8700c5c1 100644 --- a/src/app/project.coffee +++ b/src/app/project.coffee @@ -138,20 +138,14 @@ class Project scan: (regex, iterator) -> regex = new RegExp(regex.source, 'g') - command = [ - "find \"#{@getPath()}\" -type f -print0" # find all paths in the project's working directory - "grep --text --perl-regexp --invert-match --regexp=\"#{@ignorePathRegex()}\"" # accept only non-ignored paths, separated by \0 (find doesn't support pcre) - "perl -0pi -e 's/\\n\\z\//'" # delete grep's trailing newline because it screws up xargs - "xargs -0 grep --null --perl-regexp --with-filename --line-number --recursive --regexp=\"#{regex.source}\"" # run grep on each filtered file - ].join(" | ") - - ChildProcess.exec command, bufferLines: true, stdout: (data) -> + command = "#{require.resolve('ack')} --all-types --match \"#{regex.source}\" \"#{@getPath()}\"" + ChildProcess.exec command , bufferLines: true, stdout: (data) -> for grepLine in data.split('\n') when grepLine.length - nullCharIndex = grepLine.indexOf('\0') - colonIndex = grepLine.indexOf(':') - path = grepLine.substring(0, nullCharIndex) - row = parseInt(grepLine.substring(nullCharIndex + 1, colonIndex)) - 1 - line = grepLine.substring(colonIndex + 1) + pathEndIndex = grepLine.indexOf('\0') + lineNumberEndIndex = grepLine.indexOf('\0', pathEndIndex + 1) + path = grepLine.substring(0, pathEndIndex) + row = parseInt(grepLine.substring(pathEndIndex + 1, lineNumberEndIndex)) - 1 + line = grepLine.substring(lineNumberEndIndex + 1) while match = regex.exec(line) range = new Range([row, match.index], [row, match.index + match[0].length]) iterator({path, match, range}) diff --git a/src/stdlib/require.coffee b/src/stdlib/require.coffee index 15e72c976..186c79545 100644 --- a/src/stdlib/require.coffee +++ b/src/stdlib/require.coffee @@ -92,6 +92,7 @@ resolve = (file) -> return file __expand = (path) -> + return path if __isFile path for ext, handler of exts if __exists "#{path}.#{ext}" return "#{path}.#{ext}" @@ -104,6 +105,9 @@ __expand = (path) -> __exists = (path) -> $native.exists path +__isFile = (path) -> + $native.isFile path + __coffeeCache = (filePath) -> {CoffeeScript} = require 'coffee-script' tmpPath = "/tmp/atom-compiled-scripts" diff --git a/vendor/ack b/vendor/ack new file mode 100755 index 000000000..a20f9f603 --- /dev/null +++ b/vendor/ack @@ -0,0 +1,2784 @@ +#!/usr/bin/env perl +# +# This file, ack, is generated code. +# Please DO NOT EDIT or send patches for it. +# +# Please take a look at the source from +# http://github.com/petdance/ack +# and submit patches against the individual files +# that build ack. +# + +use warnings; +use strict; + +our $VERSION = '1.96'; +# Check http://betterthangrep.com/ for updates + +# These are all our globals. + + +MAIN: { + if ( $App::Ack::VERSION ne $main::VERSION ) { + App::Ack::die( "Program/library version mismatch\n\t$0 is $main::VERSION\n\t$INC{'App/Ack.pm'} is $App::Ack::VERSION" ); + } + + # Do preliminary arg checking; + my $env_is_usable = 1; + for ( @ARGV ) { + last if ( $_ eq '--' ); + + # Priorities! Get the --thpppt checking out of the way. + /^--th[pt]+t+$/ && App::Ack::_thpppt($_); + + # See if we want to ignore the environment. (Don't tell Al Gore.) + if ( /^--(no)?env$/ ) { + $env_is_usable = defined $1 ? 0 : 1; + } + } + if ( $env_is_usable ) { + unshift( @ARGV, App::Ack::read_ackrc() ); + } + else { + my @keys = ( 'ACKRC', grep { /^ACK_/ } keys %ENV ); + delete @ENV{@keys}; + } + App::Ack::load_colors(); + + if ( exists $ENV{ACK_SWITCHES} ) { + App::Ack::warn( 'ACK_SWITCHES is no longer supported. Use ACK_OPTIONS.' ); + } + + if ( !@ARGV ) { + App::Ack::show_help(); + exit 1; + } + + main(); +} + +sub main { + my $opt = App::Ack::get_command_line_options(); + + $| = 1 if $opt->{flush}; # Unbuffer the output if flush mode + + if ( App::Ack::input_from_pipe() ) { + # We're going into filter mode + for ( qw( f g l ) ) { + $opt->{$_} and App::Ack::die( "Can't use -$_ when acting as a filter." ); + } + $opt->{show_filename} = 0; + $opt->{regex} = App::Ack::build_regex( defined $opt->{regex} ? $opt->{regex} : shift @ARGV, $opt ); + if ( my $nargs = @ARGV ) { + my $s = $nargs == 1 ? '' : 's'; + App::Ack::warn( "Ignoring $nargs argument$s on the command-line while acting as a filter." ); + } + + my $res = App::Ack::Resource::Basic->new( '-' ); + my $nmatches; + if ( $opt->{count} ) { + $nmatches = App::Ack::search_and_list( $res, $opt ); + } + else { + # normal searching + $nmatches = App::Ack::search_resource( $res, $opt ); + } + $res->close(); + App::Ack::exit_from_ack( $nmatches ); + } + + my $file_matching = $opt->{f} || $opt->{lines}; + if ( $file_matching ) { + App::Ack::die( "Can't specify both a regex ($opt->{regex}) and use one of --line, -f or -g." ) if $opt->{regex}; + } + else { + $opt->{regex} = App::Ack::build_regex( defined $opt->{regex} ? $opt->{regex} : shift @ARGV, $opt ); + } + + # check that all regexes do compile fine + App::Ack::check_regex( $_ ) for ( $opt->{regex}, $opt->{G} ); + + my $what = App::Ack::get_starting_points( \@ARGV, $opt ); + my $iter = App::Ack::get_iterator( $what, $opt ); + App::Ack::filetype_setup(); + + my $nmatches = 0; + + App::Ack::set_up_pager( $opt->{pager} ) if defined $opt->{pager}; + if ( $opt->{f} ) { + $nmatches = App::Ack::print_files( $iter, $opt ); + } + elsif ( $opt->{l} || $opt->{count} ) { + $nmatches = App::Ack::print_files_with_matches( $iter, $opt ); + } + else { + $nmatches = App::Ack::print_matches( $iter, $opt ); + } + close $App::Ack::fh; + App::Ack::exit_from_ack( $nmatches ); +} + +=head1 NAME + +ack - grep-like text finder + +=head1 SYNOPSIS + + ack [options] PATTERN [FILE...] + ack -f [options] [DIRECTORY...] + +=head1 DESCRIPTION + +Ack is designed as a replacement for 99% of the uses of F. + +Ack searches the named input FILEs (or standard input if no files are +named, or the file name - is given) for lines containing a match to the +given PATTERN. By default, ack prints the matching lines. + +Ack can also list files that would be searched, without actually searching +them, to let you take advantage of ack's file-type filtering capabilities. + +=head1 FILE SELECTION + +I is intelligent about the files it searches. It knows about +certain file types, based on both the extension on the file and, +in some cases, the contents of the file. These selections can be +made with the B<--type> option. + +With no file selections, I only searches files of types that +it recognizes. If you have a file called F, and I +doesn't know what a .wango file is, I won't search it. + +The B<-a> option tells I to select all files, regardless of +type. + +Some files will never be selected by I, even with B<-a>, +including: + +=over 4 + +=item * Backup files: Files matching F<#*#> or ending with F<~>. + +=item * Coredumps: Files matching F + +=back + +However, I always searches the files given on the command line, +no matter what type. Furthermore, by specifying the B<-u> option all +files will be searched. + +=head1 DIRECTORY SELECTION + +I descends through the directory tree of the starting directories +specified. However, it will ignore the shadow directories used by +many version control systems, and the build directories used by the +Perl MakeMaker system. You may add or remove a directory from this +list with the B<--[no]ignore-dir> option. The option may be repeated +to add/remove multiple directories from the ignore list. + +For a complete list of directories that do not get searched, run +F. + +=head1 WHEN TO USE GREP + +I trumps I as an everyday tool 99% of the time, but don't +throw I away, because there are times you'll still need it. + +E.g., searching through huge files looking for regexes that can be +expressed with I syntax should be quicker with I. + +If your script or parent program uses I C<--quiet> or +C<--silent> or needs exit 2 on IO error, use I. + +=head1 OPTIONS + +=over 4 + +=item B<-a>, B<--all> + +Operate on all files, regardless of type (but still skip directories +like F, F, etc.) + +=item B<-A I>, B<--after-context=I> + +Print I lines of trailing context after matching lines. + +=item B<-B I>, B<--before-context=I> + +Print I lines of leading context before matching lines. + +=item B<-C [I]>, B<--context[=I]> + +Print I lines (default 2) of context around matching lines. + +=item B<-c>, B<--count> + +Suppress normal output; instead print a count of matching lines for +each input file. If B<-l> is in effect, it will only show the +number of lines for each file that has lines matching. Without +B<-l>, some line counts may be zeroes. + +If combined with B<-h> (B<--no-filename>) ack outputs only one total count. + +=item B<--color>, B<--nocolor> + +B<--color> highlights the matching text. B<--nocolor> supresses +the color. This is on by default unless the output is redirected. + +On Windows, this option is off by default unless the +L module is installed or the C +environment variable is used. + +=item B<--color-filename=I> + +Sets the color to be used for filenames. + +=item B<--color-match=I> + +Sets the color to be used for matches. + +=item B<--color-lineno=I> + +Sets the color to be used for line numbers. + +=item B<--column> + +Show the column number of the first match. This is helpful for editors +that can place your cursor at a given position. + +=item B<--env>, B<--noenv> + +B<--noenv> disables all environment processing. No F<.ackrc> is read +and all environment variables are ignored. By default, F considers +F<.ackrc> and settings in the environment. + +=item B<--flush> + +B<--flush> flushes output immediately. This is off by default +unless ack is running interactively (when output goes to a pipe +or file). + +=item B<-f> + +Only print the files that would be searched, without actually doing +any searching. PATTERN must not be specified, or it will be taken as +a path to search. + +=item B<--follow>, B<--nofollow> + +Follow or don't follow symlinks, other than whatever starting files +or directories were specified on the command line. + +This is off by default. + +=item B<-G I> + +Only paths matching I are included in the search. The entire +path and filename are matched against I, and I is a +Perl regular expression, not a shell glob. + +The options B<-i>, B<-w>, B<-v>, and B<-Q> do not apply to this I. + +=item B<-g I> + +Print files where the relative path + filename matches I. This option is +a convenience shortcut for B<-f> B<-G I>. + +The options B<-i>, B<-w>, B<-v>, and B<-Q> do not apply to this I. + +=item B<--group>, B<--nogroup> + +B<--group> groups matches by file name with. This is the default when +used interactively. + +B<--nogroup> prints one result per line, like grep. This is the default +when output is redirected. + +=item B<-H>, B<--with-filename> + +Print the filename for each match. + +=item B<-h>, B<--no-filename> + +Suppress the prefixing of filenames on output when multiple files are +searched. + +=item B<--help> + +Print a short help statement. + +=item B<-i>, B<--ignore-case> + +Ignore case in the search strings. + +This applies only to the PATTERN, not to the regexes given for the B<-g> +and B<-G> options. + +=item B<--[no]ignore-dir=I> + +Ignore directory (as CVS, .svn, etc are ignored). May be used multiple times +to ignore multiple directories. For example, mason users may wish to include +B<--ignore-dir=data>. The B<--noignore-dir> option allows users to search +directories which would normally be ignored (perhaps to research the contents +of F<.svn/props> directories). + +The I must always be a simple directory name. Nested directories like +F are NOT supported. You would need to specify B<--ignore-dir=foo> and +then no files from any foo directory are taken into account by ack unless given +explicitly on the command line. + +=item B<--line=I> + +Only print line I of each file. Multiple lines can be given with multiple +B<--line> options or as a comma separated list (B<--line=3,5,7>). B<--line=4-7> +also works. The lines are always output in ascending order, no matter the +order given on the command line. + +=item B<-l>, B<--files-with-matches> + +Only print the filenames of matching files, instead of the matching text. + +=item B<-L>, B<--files-without-matches> + +Only print the filenames of files that do I match. This is equivalent +to specifying B<-l> and B<-v>. + +=item B<--match I> + +Specify the I explicitly. This is helpful if you don't want to put the +regex as your first argument, e.g. when executing multiple searches over the +same set of files. + + # search for foo and bar in given files + ack file1 t/file* --match foo + ack file1 t/file* --match bar + +=item B<-m=I>, B<--max-count=I> + +Stop reading a file after I matches. + +=item B<--man> + +Print this manual page. + +=item B<-n>, B<--no-recurse> + +No descending into subdirectories. + +=item B<-o> + +Show only the part of each line matching PATTERN (turns off text +highlighting) + +=item B<--output=I> + +Output the evaluation of I for each line (turns off text +highlighting) + +=item B<--pager=I> + +Direct ack's output through I. This can also be specified +via the C and C environment variables. + +Using --pager does not suppress grouping and coloring like piping +output on the command-line does. + +=item B<--passthru> + +Prints all lines, whether or not they match the expression. Highlighting +will still work, though, so it can be used to highlight matches while +still seeing the entire file, as in: + + # Watch a log file, and highlight a certain IP address + $ tail -f ~/access.log | ack --passthru 123.45.67.89 + +=item B<--print0> + +Only works in conjunction with -f, -g, -l or -c (filename output). The filenames +are output separated with a null byte instead of the usual newline. This is +helpful when dealing with filenames that contain whitespace, e.g. + + # remove all files of type html + ack -f --html --print0 | xargs -0 rm -f + +=item B<-Q>, B<--literal> + +Quote all metacharacters in PATTERN, it is treated as a literal. + +This applies only to the PATTERN, not to the regexes given for the B<-g> +and B<-G> options. + +=item B<-r>, B<-R>, B<--recurse> + +Recurse into sub-directories. This is the default and just here for +compatibility with grep. You can also use it for turning B<--no-recurse> off. + +=item B<--smart-case>, B<--no-smart-case> + +Ignore case in the search strings if PATTERN contains no uppercase +characters. This is similar to C in vim. This option is +off by default. + +B<-i> always overrides this option. + +This applies only to the PATTERN, not to the regexes given for the +B<-g> and B<-G> options. + +=item B<--sort-files> + +Sorts the found files lexically. Use this if you want your file +listings to be deterministic between runs of I. + +=item B<--show-types> + +Outputs the filetypes that ack associates with each file. + +Works with B<-f> and B<-g> options. + +=item B<--thpppt> + +Display the all-important Bill The Cat logo. Note that the exact +spelling of B<--thpppppt> is not important. It's checked against +a regular expression. + +=item B<--type=TYPE>, B<--type=noTYPE> + +Specify the types of files to include or exclude from a search. +TYPE is a filetype, like I or I. B<--type=perl> can +also be specified as B<--perl>, and B<--type=noperl> can be done +as B<--noperl>. + +If a file is of both type "foo" and "bar", specifying --foo and +--nobar will exclude the file, because an exclusion takes precedence +over an inclusion. + +Type specifications can be repeated and are ORed together. + +See I for a list of valid types. + +=item B<--type-add I=I<.EXTENSION>[,I<.EXT2>[,...]]> + +Files with the given EXTENSION(s) are recognized as being of (the +existing) type TYPE. See also L. + + +=item B<--type-set I=I<.EXTENSION>[,I<.EXT2>[,...]]> + +Files with the given EXTENSION(s) are recognized as being of type +TYPE. This replaces an existing definition for type TYPE. See also +L. + +=item B<-u>, B<--unrestricted> + +All files and directories (including blib/, core.*, ...) are searched, +nothing is skipped. When both B<-u> and B<--ignore-dir> are used, the +B<--ignore-dir> option has no effect. + +=item B<-v>, B<--invert-match> + +Invert match: select non-matching lines + +This applies only to the PATTERN, not to the regexes given for the B<-g> +and B<-G> options. + +=item B<--version> + +Display version and copyright information. + +=item B<-w>, B<--word-regexp> + +Force PATTERN to match only whole words. The PATTERN is wrapped with +C<\b> metacharacters. + +This applies only to the PATTERN, not to the regexes given for the B<-g> +and B<-G> options. + +=item B<-1> + +Stops after reporting first match of any kind. This is different +from B<--max-count=1> or B<-m1>, where only one match per file is +shown. Also, B<-1> works with B<-f> and B<-g>, where B<-m> does +not. + +=back + +=head1 THE .ackrc FILE + +The F<.ackrc> file contains command-line options that are prepended +to the command line before processing. Multiple options may live +on multiple lines. Lines beginning with a # are ignored. A F<.ackrc> +might look like this: + + # Always sort the files + --sort-files + + # Always color, even if piping to a another program + --color + + # Use "less -r" as my pager + --pager=less -r + +Note that arguments with spaces in them do not need to be quoted, +as they are not interpreted by the shell. Basically, each I +in the F<.ackrc> file is interpreted as one element of C<@ARGV>. + +F looks in your home directory for the F<.ackrc>. You can +specify another location with the F variable, below. + +If B<--noenv> is specified on the command line, the F<.ackrc> file +is ignored. + +=head1 Defining your own types + +ack allows you to define your own types in addition to the predefined +types. This is done with command line options that are best put into +an F<.ackrc> file - then you do not have to define your types over and +over again. In the following examples the options will always be shown +on one command line so that they can be easily copy & pasted. + +I searches for foo in all perl files. I +tells you, that perl files are files ending +in .pl, .pm, .pod or .t. So what if you would like to include .xs +files as well when searching for --perl files? I +does this for you. B<--type-add> appends +additional extensions to an existing type. + +If you want to define a new type, or completely redefine an existing +type, then use B<--type-set>. I defines the type I to include files with +the extensions .e or .eiffel. So to search for all eiffel files +containing the word Bertrand use I. +As usual, you can also write B<--type=eiffel> +instead of B<--eiffel>. Negation also works, so B<--noeiffel> excludes +all eiffel files from a search. Redefining also works: I +and I<.xs> files no longer belong to the type I. + +When defining your own types in the F<.ackrc> file you have to use +the following: + + --type-set=eiffel=.e,.eiffel + +or writing on separate lines + + --type-set + eiffel=.e,.eiffel + +The following does B work in the F<.ackrc> file: + + --type-set eiffel=.e,.eiffel + + +In order to see all currently defined types, use I<--help types>, e.g. +I + +Restrictions: + +=over 4 + +=item + +The types 'skipped', 'make', 'binary' and 'text' are considered "builtin" and +cannot be altered. + +=item + +The shebang line recognition of the types 'perl', 'ruby', 'php', 'python', +'shell' and 'xml' cannot be redefined by I<--type-set>, it is always +active. However, the shebang line is only examined for files where the +extension is not recognised. Therefore it is possible to say +I and +only find your shiny new I<.perl> files (and all files with unrecognized extension +and perl on the shebang line). + +=back + +=head1 ENVIRONMENT VARIABLES + +For commonly-used ack options, environment variables can make life much easier. +These variables are ignored if B<--noenv> is specified on the command line. + +=over 4 + +=item ACKRC + +Specifies the location of the F<.ackrc> file. If this file doesn't +exist, F looks in the default location. + +=item ACK_OPTIONS + +This variable specifies default options to be placed in front of +any explicit options on the command line. + +=item ACK_COLOR_FILENAME + +Specifies the color of the filename when it's printed in B<--group> +mode. By default, it's "bold green". + +The recognized attributes are clear, reset, dark, bold, underline, +underscore, blink, reverse, concealed black, red, green, yellow, +blue, magenta, on_black, on_red, on_green, on_yellow, on_blue, +on_magenta, on_cyan, and on_white. Case is not significant. +Underline and underscore are equivalent, as are clear and reset. +The color alone sets the foreground color, and on_color sets the +background color. + +This option can also be set with B<--color-filename>. + +=item ACK_COLOR_MATCH + +Specifies the color of the matching text when printed in B<--color> +mode. By default, it's "black on_yellow". + +This option can also be set with B<--color-match>. + +See B for the color specifications. + +=item ACK_COLOR_LINENO + +Specifies the color of the line number when printed in B<--color> +mode. By default, it's "bold yellow". + +This option can also be set with B<--color-lineno>. + +See B for the color specifications. + +=item ACK_PAGER + +Specifies a pager program, such as C, C or C, to which +ack will send its output. + +Using C does not suppress grouping and coloring like +piping output on the command-line does, except that on Windows +ack will assume that C does not support color. + +C overrides C if both are specified. + +=item ACK_PAGER_COLOR + +Specifies a pager program that understands ANSI color sequences. +Using C does not suppress grouping and coloring +like piping output on the command-line does. + +If you are not on Windows, you never need to use C. + +=back + +=head1 ACK & OTHER TOOLS + +=head2 Vim integration + +F integrates easily with the Vim text editor. Set this in your +F<.vimrc> to use F instead of F: + + set grepprg=ack\ -a + +That examples uses C<-a> to search through all files, but you may +use other default flags. Now you can search with F and easily +step through the results in Vim: + + :grep Dumper perllib + +=head2 Emacs integration + +Phil Jackson put together an F extension that "provides a +simple compilation mode ... has the ability to guess what files you +want to search for based on the major-mode." + +L + +=head2 TextMate integration + +Pedro Melo is a TextMate user who writes "I spend my day mostly +inside TextMate, and the built-in find-in-project sucks with large +projects. So I hacked a TextMate command that was using find + +grep to use ack. The result is the Search in Project with ack, and +you can find it here: +L" + +=head2 Shell and Return Code + +For greater compatibility with I, I in normal use returns +shell return or exit code of 0 only if something is found and 1 if +no match is found. + +(Shell exit code 1 is C<$?=256> in perl with C or backticks.) + +The I code 2 for errors is not used. + +If C<-f> or C<-g> are specified, then 0 is returned if at least one +file is found. If no files are found, then 1 is returned. + +=cut + +=head1 DEBUGGING ACK PROBLEMS + +If ack gives you output you're not expecting, start with a few simple steps. + +=head2 Use B<--noenv> + +Your environment variables and F<.ackrc> may be doing things you're +not expecting, or forgotten you specified. Use B<--noenv> to ignore +your environment and F<.ackrc>. + +=head2 Use B<-f> to see what files you're scanning + +The reason I created B<-f> in the first place was as a debugging +tool. If ack is not finding matches you think it should find, run +F to see what files are being checked. + +=head1 TIPS + +=head2 Use the F<.ackrc> file. + +The F<.ackrc> is the place to put all your options you use most of +the time but don't want to remember. Put all your --type-add and +--type-set definitions in it. If you like --smart-case, set it +there, too. I also set --sort-files there. + +=head2 Use F<-f> for working with big codesets + +Ack does more than search files. C will create a +list of all the Perl files in a tree, ideal for sending into F. +For example: + + # Change all "this" to "that" in all Perl files in a tree. + ack -f --perl | xargs perl -p -i -e's/this/that/g' + +or if you prefer: + + perl -p -i -e's/this/thatg/' $(ack -f --perl) + +=head2 Use F<-Q> when in doubt about metacharacters + +If you're searching for something with a regular expression +metacharacter, most often a period in a filename or IP address, add +the -Q to avoid false positives without all the backslashing. See +the following example for more... + +=head2 Use ack to watch log files + +Here's one I used the other day to find trouble spots for a website +visitor. The user had a problem loading F, so I +took the access log and scanned it with ack twice. + + ack -Q aa.bb.cc.dd /path/to/access.log | ack -Q -B5 troublesome.gif + +The first ack finds only the lines in the Apache log for the given +IP. The second finds the match on my troublesome GIF, and shows +the previous five lines from the log in each case. + +=head2 Share your knowledge + +Join the ack-users mailing list. Send me your tips and I may add +them here. + +=head1 FAQ + +=head2 Why isn't ack finding a match in (some file)? + +Probably because it's of a type that ack doesn't recognize. ack's +searching behavior is driven by filetype. B + +Use the C<-f> switch to see a list of files that ack will search +for you. + +If you want ack to search files that it doesn't recognize, use the +C<-a> switch. + +If you want ack to search every file, even ones that it always +ignores like coredumps and backup files, use the C<-u> switch. + +=head2 Why does ack ignore unknown files by default? + +ack is designed by a programmer, for programmers, for searching +large trees of code. Most codebases have a lot files in them which +aren't source files (like compiled object files, source control +metadata, etc), and grep wastes a lot of time searching through all +of those as well and returning matches from those files. + +That's why ack's behavior of not searching things it doesn't recognize +is one of its greatest strengths: the speed you get from only +searching the things that you want to be looking at. + +=head2 Wouldn't it be great if F did search & replace? + +No, ack will always be read-only. Perl has a perfectly good way +to do search & replace in files, using the C<-i>, C<-p> and C<-n> +switches. + +You can certainly use ack to select your files to update. For +example, to change all "foo" to "bar" in all PHP files, you can do +this from the Unix shell: + + $ perl -i -p -e's/foo/bar/g' $(ack -f --php) + +=head2 Can you make ack recognize F<.xyz> files? + +That's an enhancement. Please see the section in the manual about +enhancements. + +=head2 There's already a program/package called ack. + +Yes, I know. + +=head2 Why is it called ack if it's called ack-grep? + +The name of the program is "ack". Some packagers have called it +"ack-grep" when creating packages because there's already a package +out there called "ack" that has nothing to do with this ack. + +I suggest you make a symlink named F that points to F +because one of the crucial benefits of ack is having a name that's +so short and simple to type. + +To do that, run this with F or as root: + + ln -s /usr/bin/ack-grep /usr/bin/ack + +=head2 What does F mean? + +Nothing. I wanted a name that was easy to type and that you could +pronounce as a single syllable. + +=head2 Can I do multi-line regexes? + +No, ack does not support regexes that match multiple lines. Doing +so would require reading in the entire file at a time. + +If you want to see lines near your match, use the C<--A>, C<--B> +and C<--C> switches for displaying context. + +=head1 AUTHOR + +Andy Lester, C<< >> + +=head1 BUGS + +Please report any bugs or feature requests to the issues list at +Github: L + +=head1 ENHANCEMENTS + +All enhancement requests MUST first be posted to the ack-users +mailing list at L. I +will not consider a request without it first getting seen by other +ack users. This includes requests for new filetypes. + +There is a list of enhancements I want to make to F in the ack +issues list at Github: L + +Patches are always welcome, but patches with tests get the most +attention. + +=head1 SUPPORT + +Support for and information about F can be found at: + +=over 4 + +=item * The ack homepage + +L + +=item * The ack issues list at Github + +L + +=item * AnnoCPAN: Annotated CPAN documentation + +L + +=item * CPAN Ratings + +L + +=item * Search CPAN + +L + +=item * Git source repository + +L + +=back + +=head1 ACKNOWLEDGEMENTS + +How appropriate to have Inowledgements! + +Thanks to everyone who has contributed to ack in any way, including +Matthew Wild, +Scott Kyle, +Nick Hooey, +Bo Borgerson, +Mark Szymanski, +Marq Schneider, +Packy Anderson, +JR Boyens, +Dan Sully, +Ryan Niebur, +Kent Fredric, +Mike Morearty, +Ingmar Vanhassel, +Eric Van Dewoestine, +Sitaram Chamarty, +Adam James, +Richard Carlsson, +Pedro Melo, +AJ Schuster, +Phil Jackson, +Michael Schwern, +Jan Dubois, +Christopher J. Madsen, +Matthew Wickline, +David Dyck, +Jason Porritt, +Jjgod Jiang, +Thomas Klausner, +Uri Guttman, +Peter Lewis, +Kevin Riggle, +Ori Avtalion, +Torsten Blix, +Nigel Metheringham, +GEbor SzabE, +Tod Hagan, +Michael Hendricks, +Evar ArnfjErE Bjarmason, +Piers Cawley, +Stephen Steneker, +Elias Lutfallah, +Mark Leighton Fisher, +Matt Diephouse, +Christian Jaeger, +Bill Sully, +Bill Ricker, +David Golden, +Nilson Santos F. Jr, +Elliot Shank, +Merijn Broeren, +Uwe Voelker, +Rick Scott, +Ask BjErn Hansen, +Jerry Gay, +Will Coleda, +Mike O'Regan, +Slaven ReziE<0x107>, +Mark Stosberg, +David Alan Pisoni, +Adriano Ferreira, +James Keenan, +Leland Johnson, +Ricardo Signes +and Pete Krawczyk. + +=head1 COPYRIGHT & LICENSE + +Copyright 2005-2011 Andy Lester. + +This program is free software; you can redistribute it and/or modify +it under the terms of the Artistic License v2.0. + +=cut +package File::Next; + +use strict; +use warnings; + + +our $VERSION = '1.06'; + + + +use File::Spec (); + + +our $name; # name of the current file +our $dir; # dir of the current file + +our %files_defaults; +our %skip_dirs; + +BEGIN { + %files_defaults = ( + file_filter => undef, + descend_filter => undef, + error_handler => sub { CORE::die @_ }, + sort_files => undef, + follow_symlinks => 1, + ); + %skip_dirs = map {($_,1)} (File::Spec->curdir, File::Spec->updir); +} + + +sub files { + ($_[0] eq __PACKAGE__) && die 'File::Next::files must not be invoked as File::Next->files'; + + my ($parms,@queue) = _setup( \%files_defaults, @_ ); + my $filter = $parms->{file_filter}; + + return sub { + while (@queue) { + my ($dir,$file,$fullpath) = splice( @queue, 0, 3 ); + if ( -f $fullpath ) { + if ( $filter ) { + local $_ = $file; + local $File::Next::dir = $dir; + local $File::Next::name = $fullpath; + next if not $filter->(); + } + return wantarray ? ($dir,$file,$fullpath) : $fullpath; + } + elsif ( -d _ ) { + unshift( @queue, _candidate_files( $parms, $fullpath ) ); + } + } # while + + return; + }; # iterator +} + + + + + + + +sub sort_standard($$) { return $_[0]->[1] cmp $_[1]->[1] } +sub sort_reverse($$) { return $_[1]->[1] cmp $_[0]->[1] } + +sub reslash { + my $path = shift; + + my @parts = split( /\//, $path ); + + return $path if @parts < 2; + + return File::Spec->catfile( @parts ); +} + + + +sub _setup { + my $defaults = shift; + my $passed_parms = ref $_[0] eq 'HASH' ? {%{+shift}} : {}; # copy parm hash + + my %passed_parms = %{$passed_parms}; + + my $parms = {}; + for my $key ( keys %{$defaults} ) { + $parms->{$key} = + exists $passed_parms{$key} + ? delete $passed_parms{$key} + : $defaults->{$key}; + } + + # Any leftover keys are bogus + for my $badkey ( keys %passed_parms ) { + my $sub = (caller(1))[3]; + $parms->{error_handler}->( "Invalid option passed to $sub(): $badkey" ); + } + + # If it's not a code ref, assume standard sort + if ( $parms->{sort_files} && ( ref($parms->{sort_files}) ne 'CODE' ) ) { + $parms->{sort_files} = \&sort_standard; + } + my @queue; + + for ( @_ ) { + my $start = reslash( $_ ); + if (-d $start) { + push @queue, ($start,undef,$start); + } + else { + push @queue, (undef,$start,$start); + } + } + + return ($parms,@queue); +} + + +sub _candidate_files { + my $parms = shift; + my $dir = shift; + + my $dh; + if ( !opendir $dh, $dir ) { + $parms->{error_handler}->( "$dir: $!" ); + return; + } + + my @newfiles; + my $descend_filter = $parms->{descend_filter}; + my $follow_symlinks = $parms->{follow_symlinks}; + my $sort_sub = $parms->{sort_files}; + + for my $file ( grep { !exists $skip_dirs{$_} } readdir $dh ) { + my $has_stat; + + # Only do directory checking if we have a descend_filter + my $fullpath = File::Spec->catdir( $dir, $file ); + if ( !$follow_symlinks ) { + next if -l $fullpath; + $has_stat = 1; + } + + if ( $descend_filter ) { + if ( $has_stat ? (-d _) : (-d $fullpath) ) { + local $File::Next::dir = $fullpath; + local $_ = $file; + next if not $descend_filter->(); + } + } + if ( $sort_sub ) { + push( @newfiles, [ $dir, $file, $fullpath ] ); + } + else { + push( @newfiles, $dir, $file, $fullpath ); + } + } + closedir $dh; + + if ( $sort_sub ) { + return map { @{$_} } sort $sort_sub @newfiles; + } + + return @newfiles; +} + + +1; # End of File::Next +package App::Ack; + +use warnings; +use strict; + + + + +our $VERSION; +our $COPYRIGHT; +BEGIN { + $VERSION = '1.96'; + $COPYRIGHT = 'Copyright 2005-2011 Andy Lester.'; +} + +our $fh; + +BEGIN { + $fh = *STDOUT; +} + + +our %types; +our %type_wanted; +our %mappings; +our %ignore_dirs; + +our $input_from_pipe; +our $output_to_pipe; + +our $dir_sep_chars; +our $is_cygwin; +our $is_windows; + +use File::Spec (); +use File::Glob ':glob'; +use Getopt::Long (); + +BEGIN { + %ignore_dirs = ( + '.bzr' => 'Bazaar', + '.cdv' => 'Codeville', + '~.dep' => 'Interface Builder', + '~.dot' => 'Interface Builder', + '~.nib' => 'Interface Builder', + '~.plst' => 'Interface Builder', + '.git' => 'Git', + '.hg' => 'Mercurial', + '.pc' => 'quilt', + '.svn' => 'Subversion', + _MTN => 'Monotone', + blib => 'Perl module building', + CVS => 'CVS', + RCS => 'RCS', + SCCS => 'SCCS', + _darcs => 'darcs', + _sgbak => 'Vault/Fortress', + 'autom4te.cache' => 'autoconf', + 'cover_db' => 'Devel::Cover', + _build => 'Module::Build', + ); + + %mappings = ( + actionscript => [qw( as mxml )], + ada => [qw( ada adb ads )], + asm => [qw( asm s )], + batch => [qw( bat cmd )], + binary => q{Binary files, as defined by Perl's -B op (default: off)}, + cc => [qw( c h xs )], + cfmx => [qw( cfc cfm cfml )], + clojure => [qw( clj )], + cpp => [qw( cpp cc cxx m hpp hh h hxx )], + csharp => [qw( cs )], + css => [qw( css )], + delphi => [qw( pas int dfm nfm dof dpk dproj groupproj bdsgroup bdsproj )], + elisp => [qw( el )], + erlang => [qw( erl hrl )], + fortran => [qw( f f77 f90 f95 f03 for ftn fpp )], + go => [qw( go )], + groovy => [qw( groovy gtmpl gpp grunit )], + haskell => [qw( hs lhs )], + hh => [qw( h )], + html => [qw( htm html shtml xhtml )], + java => [qw( java properties )], + js => [qw( js )], + jsp => [qw( jsp jspx jhtm jhtml )], + lisp => [qw( lisp lsp )], + lua => [qw( lua )], + make => q{Makefiles (including *.mk and *.mak)}, + mason => [qw( mas mhtml mpl mtxt )], + objc => [qw( m h )], + objcpp => [qw( mm h )], + ocaml => [qw( ml mli )], + parrot => [qw( pir pasm pmc ops pod pg tg )], + perl => [qw( pl pm pm6 pod t )], + php => [qw( php phpt php3 php4 php5 phtml)], + plone => [qw( pt cpt metadata cpy py )], + python => [qw( py )], + rake => q{Rakefiles}, + ruby => [qw( rb rhtml rjs rxml erb rake spec )], + scala => [qw( scala )], + scheme => [qw( scm ss )], + shell => [qw( sh bash csh tcsh ksh zsh )], + skipped => q{Files, but not directories, normally skipped by ack (default: off)}, + smalltalk => [qw( st )], + sql => [qw( sql ctl )], + tcl => [qw( tcl itcl itk )], + tex => [qw( tex cls sty )], + text => q{Text files, as defined by Perl's -T op (default: off)}, + tt => [qw( tt tt2 ttml )], + vb => [qw( bas cls frm ctl vb resx )], + verilog => [qw( v vh sv )], + vhdl => [qw( vhd vhdl )], + vim => [qw( vim )], + yaml => [qw( yaml yml )], + xml => [qw( xml dtd xsl xslt ent )], + ); + + while ( my ($type,$exts) = each %mappings ) { + if ( ref $exts ) { + for my $ext ( @{$exts} ) { + push( @{$types{$ext}}, $type ); + } + } + } + # add manually Makefile extensions + push @{$types{$_}}, 'make' for qw{ mk mak }; + + # These have to be checked before any filehandle diddling. + $output_to_pipe = not -t *STDOUT; + $input_from_pipe = -p STDIN; + + $is_cygwin = ($^O eq 'cygwin'); + $is_windows = ($^O =~ /MSWin32/); + $dir_sep_chars = $is_windows ? quotemeta( '\\/' ) : quotemeta( File::Spec->catfile( '', '' ) ); +} + + +sub read_ackrc { + my @files = ( $ENV{ACKRC} ); + my @dirs = + $is_windows + ? ( $ENV{HOME}, $ENV{USERPROFILE} ) + : ( '~', $ENV{HOME} ); + for my $dir ( grep { defined } @dirs ) { + for my $file ( '.ackrc', '_ackrc' ) { + push( @files, bsd_glob( "$dir/$file", GLOB_TILDE ) ); + } + } + for my $filename ( @files ) { + if ( defined $filename && -e $filename ) { + open( my $fh, '<', $filename ) or App::Ack::die( "$filename: $!\n" ); + my @lines = grep { /./ && !/^\s*#/ } <$fh>; + chomp @lines; + close $fh or App::Ack::die( "$filename: $!\n" ); + + # get rid of leading and trailing whitespaces + for ( @lines ) { + s/^\s+//; + s/\s+$//; + } + + return @lines; + } + } + + return; +} + + +sub get_command_line_options { + my %opt = ( + pager => $ENV{ACK_PAGER_COLOR} || $ENV{ACK_PAGER}, + ); + + my $getopt_specs = { + 1 => sub { $opt{1} = $opt{m} = 1 }, + 'A|after-context=i' => \$opt{after_context}, + 'B|before-context=i' => \$opt{before_context}, + 'C|context:i' => sub { shift; my $val = shift; $opt{before_context} = $opt{after_context} = ($val || 2) }, + 'a|all-types' => \$opt{all}, + 'break!' => \$opt{break}, + c => \$opt{count}, + 'color|colour!' => \$opt{color}, + 'color-match=s' => \$ENV{ACK_COLOR_MATCH}, + 'color-filename=s' => \$ENV{ACK_COLOR_FILENAME}, + 'color-lineno=s' => \$ENV{ACK_COLOR_LINENO}, + 'column!' => \$opt{column}, + count => \$opt{count}, + 'env!' => sub { }, # ignore this option, it is handled beforehand + f => \$opt{f}, + flush => \$opt{flush}, + 'follow!' => \$opt{follow}, + 'g=s' => sub { shift; $opt{G} = shift; $opt{f} = 1 }, + 'G=s' => \$opt{G}, + 'group!' => sub { shift; $opt{heading} = $opt{break} = shift }, + 'heading!' => \$opt{heading}, + 'h|no-filename' => \$opt{h}, + 'H|with-filename' => \$opt{H}, + 'i|ignore-case' => \$opt{i}, + 'invert-file-match' => \$opt{invert_file_match}, + 'lines=s' => sub { shift; my $val = shift; push @{$opt{lines}}, $val }, + 'l|files-with-matches' => \$opt{l}, + 'L|files-without-matches' => sub { $opt{l} = $opt{v} = 1 }, + 'm|max-count=i' => \$opt{m}, + 'match=s' => \$opt{regex}, + 'n|no-recurse' => \$opt{n}, + o => sub { $opt{output} = '$&' }, + 'output=s' => \$opt{output}, + 'pager=s' => \$opt{pager}, + 'nopager' => sub { $opt{pager} = undef }, + 'passthru' => \$opt{passthru}, + 'print0' => \$opt{print0}, + 'Q|literal' => \$opt{Q}, + 'r|R|recurse' => sub { $opt{n} = 0 }, + 'show-types' => \$opt{show_types}, + 'smart-case!' => \$opt{smart_case}, + 'sort-files' => \$opt{sort_files}, + 'u|unrestricted' => \$opt{u}, + 'v|invert-match' => \$opt{v}, + 'w|word-regexp' => \$opt{w}, + + 'ignore-dirs=s' => sub { shift; my $dir = remove_dir_sep( shift ); $ignore_dirs{$dir} = '--ignore-dirs' }, + 'noignore-dirs=s' => sub { shift; my $dir = remove_dir_sep( shift ); delete $ignore_dirs{$dir} }, + + 'version' => sub { print_version_statement(); exit; }, + 'help|?:s' => sub { shift; show_help(@_); exit; }, + 'help-types'=> sub { show_help_types(); exit; }, + 'man' => sub { + require Pod::Usage; + Pod::Usage::pod2usage({ + -verbose => 2, + -exitval => 0, + }); + }, + + 'type=s' => sub { + # Whatever --type=xxx they specify, set it manually in the hash + my $dummy = shift; + my $type = shift; + my $wanted = ($type =~ s/^no//) ? 0 : 1; # must not be undef later + + if ( exists $type_wanted{ $type } ) { + $type_wanted{ $type } = $wanted; + } + else { + App::Ack::die( qq{Unknown --type "$type"} ); + } + }, # type sub + }; + + # Stick any default switches at the beginning, so they can be overridden + # by the command line switches. + unshift @ARGV, split( ' ', $ENV{ACK_OPTIONS} ) if defined $ENV{ACK_OPTIONS}; + + # first pass through options, looking for type definitions + def_types_from_ARGV(); + + for my $i ( filetypes_supported() ) { + $getopt_specs->{ "$i!" } = \$type_wanted{ $i }; + } + + + my $parser = Getopt::Long::Parser->new(); + $parser->configure( 'bundling', 'no_ignore_case', ); + $parser->getoptions( %{$getopt_specs} ) or + App::Ack::die( 'See ack --help, ack --help-types or ack --man for options.' ); + + my $to_screen = not output_to_pipe(); + my %defaults = ( + all => 0, + color => $to_screen, + follow => 0, + break => $to_screen, + heading => $to_screen, + before_context => 0, + after_context => 0, + ); + if ( $is_windows && $defaults{color} && not $ENV{ACK_PAGER_COLOR} ) { + if ( $ENV{ACK_PAGER} || not eval { require Win32::Console::ANSI } ) { + $defaults{color} = 0; + } + } + if ( $to_screen && $ENV{ACK_PAGER_COLOR} ) { + $defaults{color} = 1; + } + + while ( my ($key,$value) = each %defaults ) { + if ( not defined $opt{$key} ) { + $opt{$key} = $value; + } + } + + if ( defined $opt{m} && $opt{m} <= 0 ) { + App::Ack::die( '-m must be greater than zero' ); + } + + for ( qw( before_context after_context ) ) { + if ( defined $opt{$_} && $opt{$_} < 0 ) { + App::Ack::die( "--$_ may not be negative" ); + } + } + + if ( defined( my $val = $opt{output} ) ) { + $opt{output} = eval qq[ sub { "$val" } ]; + } + if ( defined( my $l = $opt{lines} ) ) { + # --line=1 --line=5 is equivalent to --line=1,5 + my @lines = split( /,/, join( ',', @{$l} ) ); + + # --line=1-3 is equivalent to --line=1,2,3 + @lines = map { + my @ret; + if ( /-/ ) { + my ($from, $to) = split /-/, $_; + if ( $from > $to ) { + App::Ack::warn( "ignoring --line=$from-$to" ); + @ret = (); + } + else { + @ret = ( $from .. $to ); + } + } + else { + @ret = ( $_ ); + }; + @ret + } @lines; + + if ( @lines ) { + my %uniq; + @uniq{ @lines } = (); + $opt{lines} = [ sort { $a <=> $b } keys %uniq ]; # numerical sort and each line occurs only once! + } + else { + # happens if there are only ignored --line directives + App::Ack::die( 'All --line options are invalid.' ); + } + } + + return \%opt; +} + + +sub def_types_from_ARGV { + my @typedef; + + my $parser = Getopt::Long::Parser->new(); + # pass_through => leave unrecognized command line arguments alone + # no_auto_abbrev => otherwise -c is expanded and not left alone + $parser->configure( 'no_ignore_case', 'pass_through', 'no_auto_abbrev' ); + $parser->getoptions( + 'type-set=s' => sub { shift; push @typedef, ['c', shift] }, + 'type-add=s' => sub { shift; push @typedef, ['a', shift] }, + ) or App::Ack::die( 'See ack --help or ack --man for options.' ); + + for my $td (@typedef) { + my ($type, $ext) = split /=/, $td->[1]; + + if ( $td->[0] eq 'c' ) { + # type-set + if ( exists $mappings{$type} ) { + # can't redefine types 'make', 'skipped', 'text' and 'binary' + App::Ack::die( qq{--type-set: Builtin type "$type" cannot be changed.} ) + if ref $mappings{$type} ne 'ARRAY'; + + delete_type($type); + } + } + else { + # type-add + + # can't append to types 'make', 'skipped', 'text' and 'binary' + App::Ack::die( qq{--type-add: Builtin type "$type" cannot be changed.} ) + if exists $mappings{$type} && ref $mappings{$type} ne 'ARRAY'; + + App::Ack::warn( qq{--type-add: Type "$type" does not exist, creating with "$ext" ...} ) + unless exists $mappings{$type}; + } + + my @exts = split /,/, $ext; + s/^\.// for @exts; + + if ( !exists $mappings{$type} || ref($mappings{$type}) eq 'ARRAY' ) { + push @{$mappings{$type}}, @exts; + for my $e ( @exts ) { + push @{$types{$e}}, $type; + } + } + else { + App::Ack::die( qq{Cannot append to type "$type".} ); + } + } + + return; +} + + +sub delete_type { + my $type = shift; + + App::Ack::die( qq{Internal error: Cannot delete builtin type "$type".} ) + unless ref $mappings{$type} eq 'ARRAY'; + + delete $mappings{$type}; + delete $type_wanted{$type}; + for my $ext ( keys %types ) { + $types{$ext} = [ grep { $_ ne $type } @{$types{$ext}} ]; + } +} + + +sub ignoredir_filter { + return !exists $ignore_dirs{$_} && !exists $ignore_dirs{$File::Next::dir}; +} + + +sub remove_dir_sep { + my $path = shift; + $path =~ s/[$dir_sep_chars]$//; + + return $path; +} + + +use constant TEXT => 'text'; + +sub filetypes { + my $filename = shift; + + my $basename = $filename; + $basename =~ s{.*[$dir_sep_chars]}{}; + + return 'skipped' unless is_searchable( $basename ); + + my $lc_basename = lc $basename; + return ('make',TEXT) if $lc_basename eq 'makefile' || $lc_basename eq 'gnumakefile'; + return ('rake','ruby',TEXT) if $lc_basename eq 'rakefile'; + + # If there's an extension, look it up + if ( $filename =~ m{\.([^\.$dir_sep_chars]+)$}o ) { + my $ref = $types{lc $1}; + return (@{$ref},TEXT) if $ref; + } + + # At this point, we can't tell from just the name. Now we have to + # open it and look inside. + + return unless -e $filename; + # From Elliot Shank: + # I can't see any reason that -r would fail on these-- the ACLs look + # fine, and no program has any of them open, so the busted Windows + # file locking model isn't getting in there. If I comment the if + # statement out, everything works fine + # So, for cygwin, don't bother trying to check for readability. + if ( !$is_cygwin ) { + if ( !-r $filename ) { + App::Ack::warn( "$filename: Permission denied" ); + return; + } + } + + return 'binary' if -B $filename; + + # If there's no extension, or we don't recognize it, check the shebang line + my $fh; + if ( !open( $fh, '<', $filename ) ) { + App::Ack::warn( "$filename: $!" ); + return; + } + my $header = <$fh>; + close $fh; + + if ( $header =~ /^#!/ ) { + return ($1,TEXT) if $header =~ /\b(ruby|lua|p(?:erl|hp|ython))-?(\d[\d.]*)?\b/; + return ('shell',TEXT) if $header =~ /\b(?:ba|t?c|k|z)?sh\b/; + } + else { + return ('xml',TEXT) if $header =~ /\Q{Q}; + if ( $opt->{w} ) { + $str = "\\b$str" if $str =~ /^\w/; + $str = "$str\\b" if $str =~ /\w$/; + } + + my $regex_is_lc = $str eq lc $str; + if ( $opt->{i} || ($opt->{smart_case} && $regex_is_lc) ) { + $str = "(?i)$str"; + } + + return $str; +} + + +sub check_regex { + my $regex = shift; + + return unless defined $regex; + + eval { qr/$regex/ }; + if ($@) { + (my $error = $@) =~ s/ at \S+ line \d+.*//; + chomp($error); + App::Ack::die( "Invalid regex '$regex':\n $error" ); + } + + return; +} + + + + +sub warn { + return CORE::warn( _my_program(), ': ', @_, "\n" ); +} + + +sub die { + return CORE::die( _my_program(), ': ', @_, "\n" ); +} + +sub _my_program { + require File::Basename; + return File::Basename::basename( $0 ); +} + + + +sub filetypes_supported { + return keys %mappings; +} + +sub _get_thpppt { + my $y = q{_ /|,\\'!.x',=(www)=, U }; + $y =~ tr/,x!w/\nOo_/; + return $y; +} + +sub _thpppt { + my $y = _get_thpppt(); + App::Ack::print( "$y ack $_[0]!\n" ); + exit 0; +} + +sub _key { + my $str = lc shift; + $str =~ s/[^a-z]//g; + + return $str; +} + + +sub show_help { + my $help_arg = shift || 0; + + return show_help_types() if $help_arg =~ /^types?/; + + my $ignore_dirs = _listify( sort { _key($a) cmp _key($b) } keys %ignore_dirs ); + + App::Ack::print( <<"END_OF_HELP" ); +Usage: ack [OPTION]... PATTERN [FILE] + +Search for PATTERN in each source file in the tree from cwd on down. +If [FILES] is specified, then only those files/directories are checked. +ack may also search STDIN, but only if no FILE are specified, or if +one of FILES is "-". + +Default switches may be specified in ACK_OPTIONS environment variable or +an .ackrc file. If you want no dependency on the environment, turn it +off with --noenv. + +Example: ack -i select + +Searching: + -i, --ignore-case Ignore case distinctions in PATTERN + --[no]smart-case Ignore case distinctions in PATTERN, + only if PATTERN contains no upper case + Ignored if -i is specified + -v, --invert-match Invert match: select non-matching lines + -w, --word-regexp Force PATTERN to match only whole words + -Q, --literal Quote all metacharacters; PATTERN is literal + +Search output: + --line=NUM Only print line(s) NUM of each file + -l, --files-with-matches + Only print filenames containing matches + -L, --files-without-matches + Only print filenames with no matches + -o Show only the part of a line matching PATTERN + (turns off text highlighting) + --passthru Print all lines, whether matching or not + --output=expr Output the evaluation of expr for each line + (turns off text highlighting) + --match PATTERN Specify PATTERN explicitly. + -m, --max-count=NUM Stop searching in each file after NUM matches + -1 Stop searching after one match of any kind + -H, --with-filename Print the filename for each match + -h, --no-filename Suppress the prefixing filename on output + -c, --count Show number of lines matching per file + --column Show the column number of the first match + + -A NUM, --after-context=NUM + Print NUM lines of trailing context after matching + lines. + -B NUM, --before-context=NUM + Print NUM lines of leading context before matching + lines. + -C [NUM], --context[=NUM] + Print NUM lines (default 2) of output context. + + --print0 Print null byte as separator between filenames, + only works with -f, -g, -l, -L or -c. + +File presentation: + --pager=COMMAND Pipes all ack output through COMMAND. For example, + --pager="less -R". Ignored if output is redirected. + --nopager Do not send output through a pager. Cancels any + setting in ~/.ackrc, ACK_PAGER or ACK_PAGER_COLOR. + --[no]heading Print a filename heading above each file's results. + (default: on when used interactively) + --[no]break Print a break between results from different files. + (default: on when used interactively) + --group Same as --heading --break + --nogroup Same as --noheading --nobreak + --[no]color Highlight the matching text (default: on unless + output is redirected, or on Windows) + --[no]colour Same as --[no]color + --color-filename=COLOR + --color-match=COLOR + --color-lineno=COLOR Set the color for filenames, matches, and line numbers. + --flush Flush output immediately, even when ack is used + non-interactively (when output goes to a pipe or + file). + +File finding: + -f Only print the files found, without searching. + The PATTERN must not be specified. + -g REGEX Same as -f, but only print files matching REGEX. + --sort-files Sort the found files lexically. + --invert-file-match Print/search handle files that do not match -g/-G. + --show-types Show which types each file has. + +File inclusion/exclusion: + -a, --all-types All file types searched; + Ignores CVS, .svn and other ignored directories + -u, --unrestricted All files and directories searched + --[no]ignore-dir=name Add/Remove directory from the list of ignored dirs + -r, -R, --recurse Recurse into subdirectories (ack's default behavior) + -n, --no-recurse No descending into subdirectories + -G REGEX Only search files that match REGEX + + --perl Include only Perl files. + --type=perl Include only Perl files. + --noperl Exclude Perl files. + --type=noperl Exclude Perl files. + See "ack --help type" for supported filetypes. + + --type-set TYPE=.EXTENSION[,.EXT2[,...]] + Files with the given EXTENSION(s) are recognized as + being of type TYPE. This replaces an existing + definition for type TYPE. + --type-add TYPE=.EXTENSION[,.EXT2[,...]] + Files with the given EXTENSION(s) are recognized as + being of (the existing) type TYPE + + --[no]follow Follow symlinks. Default is off. + + Directories ignored by default: + $ignore_dirs + + Files not checked for type: + /~\$/ - Unix backup files + /#.+#\$/ - Emacs swap files + /[._].*\\.swp\$/ - Vi(m) swap files + /core\\.\\d+\$/ - core dumps + /[.-]min\\.js\$/ - Minified javascript files + +Miscellaneous: + --noenv Ignore environment variables and ~/.ackrc + --help This help + --man Man page + --version Display version & copyright + --thpppt Bill the Cat + +Exit status is 0 if match, 1 if no match. + +This is version $VERSION of ack. +END_OF_HELP + + return; + } + + + +sub show_help_types { + App::Ack::print( <<'END_OF_HELP' ); +Usage: ack [OPTION]... PATTERN [FILES] + +The following is the list of filetypes supported by ack. You can +specify a file type with the --type=TYPE format, or the --TYPE +format. For example, both --type=perl and --perl work. + +Note that some extensions may appear in multiple types. For example, +.pod files are both Perl and Parrot. + +END_OF_HELP + + my @types = filetypes_supported(); + my $maxlen = 0; + for ( @types ) { + $maxlen = length if $maxlen < length; + } + for my $type ( sort @types ) { + next if $type =~ /^-/; # Stuff to not show + my $ext_list = $mappings{$type}; + + if ( ref $ext_list ) { + $ext_list = join( ' ', map { ".$_" } @{$ext_list} ); + } + App::Ack::print( sprintf( " --[no]%-*.*s %s\n", $maxlen, $maxlen, $type, $ext_list ) ); + } + + return; +} + +sub _listify { + my @whats = @_; + + return '' if !@whats; + + my $end = pop @whats; + my $str = @whats ? join( ', ', @whats ) . " and $end" : $end; + + no warnings 'once'; + require Text::Wrap; + $Text::Wrap::columns = 75; + return Text::Wrap::wrap( '', ' ', $str ); +} + + +sub get_version_statement { + require Config; + + my $copyright = get_copyright(); + my $this_perl = $Config::Config{perlpath}; + if ($^O ne 'VMS') { + my $ext = $Config::Config{_exe}; + $this_perl .= $ext unless $this_perl =~ m/$ext$/i; + } + my $ver = sprintf( '%vd', $^V ); + + return <<"END_OF_VERSION"; +ack $VERSION +Running under Perl $ver at $this_perl + +$copyright + +This program is free software. You may modify or distribute it +under the terms of the Artistic License v2.0. +END_OF_VERSION +} + + +sub print_version_statement { + App::Ack::print( get_version_statement() ); + + return; +} + + +sub get_copyright { + return $COPYRIGHT; +} + + +sub load_colors { + eval 'use Term::ANSIColor ()'; + + $ENV{ACK_COLOR_MATCH} ||= 'black on_yellow'; + $ENV{ACK_COLOR_FILENAME} ||= 'bold green'; + $ENV{ACK_COLOR_LINENO} ||= 'bold yellow'; + + return; +} + + +sub is_interesting { + return if /^\./; + + my $include; + + for my $type ( filetypes( $File::Next::name ) ) { + if ( defined $type_wanted{$type} ) { + if ( $type_wanted{$type} ) { + $include = 1; + } + else { + return; + } + } + } + + return $include; +} + + + +# print subs added in order to make it easy for a third party +# module (such as App::Wack) to redefine the display methods +# and show the results in a different way. +sub print { print {$fh} @_ } +sub print_first_filename { App::Ack::print( $_[0], "\n" ) } +sub print_blank_line { App::Ack::print( "\n" ) } +sub print_separator { App::Ack::print( "--\n" ) } +sub print_filename { App::Ack::print( $_[0], $_[1] ) } +sub print_line_no { App::Ack::print( $_[0], $_[1] ) } +sub print_column_no { App::Ack::print( $_[0], $_[1] ) } +sub print_count { + my $filename = shift; + my $nmatches = shift; + my $ors = shift; + my $count = shift; + my $show_filename = shift; + + if ($show_filename) { + App::Ack::print( $filename ); + App::Ack::print( ':', $nmatches ) if $count; + } + else { + App::Ack::print( $nmatches ) if $count; + } + App::Ack::print( $ors ); +} + +sub print_count0 { + my $filename = shift; + my $ors = shift; + my $show_filename = shift; + + if ($show_filename) { + App::Ack::print( $filename, ':0', $ors ); + } + else { + App::Ack::print( '0', $ors ); + } +} + + + +{ + my $filename; + my $regex; + my $display_filename; + + my $keep_context; + + my $last_output_line; # number of the last line that has been output + my $any_output; # has there been any output for the current file yet + my $context_overall_output_count; # has there been any output at all + +sub search_resource { + my $res = shift; + my $opt = shift; + + $filename = $res->name(); + + my $v = $opt->{v}; + my $passthru = $opt->{passthru}; + my $max = $opt->{m}; + my $nmatches = 0; + + $display_filename = undef; + + # for --line processing + my $has_lines = 0; + my @lines; + if ( defined $opt->{lines} ) { + $has_lines = 1; + @lines = ( @{$opt->{lines}}, -1 ); + undef $regex; # Don't match when printing matching line + } + else { + $regex = qr/$opt->{regex}/; + } + + # for context processing + $last_output_line = -1; + $any_output = 0; + my $before_context = $opt->{before_context}; + my $after_context = $opt->{after_context}; + + $keep_context = ($before_context || $after_context) && !$passthru; + + my @before; + my $before_starts_at_line; + my $after = 0; # number of lines still to print after a match + + while ( $res->next_text ) { + # XXX Optimize away the case when there are no more @lines to find. + # XXX $has_lines, $passthru and $v never change. Optimize. + if ( $has_lines + ? $. != $lines[0] # $lines[0] should be a scalar + : $v ? m/$regex/ : !m/$regex/ ) { + if ( $passthru ) { + App::Ack::print( $_ ); + next; + } + + if ( $keep_context ) { + if ( $after ) { + print_match_or_context( $opt, 0, $., $-[0], $+[0], $_ ); + $after--; + } + elsif ( $before_context ) { + if ( @before ) { + if ( @before >= $before_context ) { + shift @before; + ++$before_starts_at_line; + } + } + else { + $before_starts_at_line = $.; + } + push @before, $_; + } + last if $max && ( $nmatches >= $max ) && !$after; + } + next; + } # not a match + + ++$nmatches; + + # print an empty line as a divider before first line in each file (not before the first file) + if ( !$any_output && $opt->{show_filename} && $opt->{break} && defined( $context_overall_output_count ) ) { + App::Ack::print_blank_line(); + } + + shift @lines if $has_lines; + + if ( $res->is_binary ) { + App::Ack::print( "Binary file $filename matches\n" ); + last; + } + if ( $keep_context ) { + if ( @before ) { + print_match_or_context( $opt, 0, $before_starts_at_line, $-[0], $+[0], @before ); + @before = (); + $before_starts_at_line = 0; + } + if ( $max && $nmatches > $max ) { + --$after; + } + else { + $after = $after_context; + } + } + print_match_or_context( $opt, 1, $., $-[0], $+[0], $_ ); + + last if $max && ( $nmatches >= $max ) && !$after; + } # while + + return $nmatches; +} # search_resource() + + + +sub print_match_or_context { + my $opt = shift; # opts array + my $is_match = shift; # is there a match on the line? + my $line_no = shift; + my $match_start = shift; + my $match_end = shift; + + my $color = $opt->{color}; + my $heading = $opt->{heading}; + my $show_filename = $opt->{show_filename}; + my $show_column = $opt->{column}; + + if ( $show_filename ) { + if ( not defined $display_filename ) { + $display_filename = + $color + ? Term::ANSIColor::colored( $filename, $ENV{ACK_COLOR_FILENAME} ) + : $filename; + if ( $heading && !$any_output ) { + App::Ack::print_first_filename($display_filename); + } + } + } + + # Modified for Atom by Nathan Sobo to print a "\0" instead of a ":" as a separator + my $sep = $is_match ? "\0" : '-'; + my $output_func = $opt->{output}; + for ( @_ ) { + if ( $keep_context && !$output_func ) { + if ( ( $last_output_line != $line_no - 1 ) && + ( $any_output || ( !$heading && defined( $context_overall_output_count ) ) ) ) { + App::Ack::print_separator(); + } + # to ensure separators between different files when --noheading + + $last_output_line = $line_no; + } + + if ( $show_filename ) { + App::Ack::print_filename($display_filename, $sep) if not $heading; + my $display_line_no = + $color + ? Term::ANSIColor::colored( $line_no, $ENV{ACK_COLOR_LINENO} ) + : $line_no; + App::Ack::print_line_no($display_line_no, $sep); + } + + if ( $output_func ) { + while ( /$regex/go ) { + App::Ack::print( $output_func->() . "\n" ); + } + } + else { + if ( $color && $is_match && $regex && + s/$regex/Term::ANSIColor::colored( substr($_, $-[0], $+[0] - $-[0]), $ENV{ACK_COLOR_MATCH} )/eg ) { + # At the end of the line reset the color and remove newline + s/[\r\n]*\z/\e[0m\e[K/; + } + else { + # remove any kind of newline at the end of the line + s/[\r\n]*\z//; + } + if ( $show_column ) { + App::Ack::print_column_no( $match_start+1, $sep ); + } + App::Ack::print($_ . "\n"); + } + $any_output = 1; + ++$context_overall_output_count; + ++$line_no; + } + + return; +} # print_match_or_context() + +} # scope around search_resource() and print_match_or_context() + + +TOTAL_COUNT_SCOPE: { +my $total_count; + +sub get_total_count { + return $total_count; +} + +sub reset_total_count { + $total_count = 0; +} + + +sub search_and_list { + my $res = shift; + my $opt = shift; + + my $nmatches = 0; + my $count = $opt->{count}; + my $ors = $opt->{print0} ? "\0" : "\n"; # output record separator + my $show_filename = $opt->{show_filename}; + + my $regex = qr/$opt->{regex}/; + + if ( $opt->{v} ) { + while ( $res->next_text ) { + if ( /$regex/ ) { + return 0 unless $count; + } + else { + ++$nmatches; + } + } + } + else { + while ( $res->next_text ) { + if ( /$regex/ ) { + ++$nmatches; + last unless $count; + } + } + } + + if ( $opt->{show_total} ) { + $total_count += $nmatches; + } + else { + if ( $nmatches ) { + App::Ack::print_count( $res->name, $nmatches, $ors, $count, $show_filename ); + } + elsif ( $count && !$opt->{l} ) { + App::Ack::print_count0( $res->name, $ors, $show_filename ); + } + } + + return $nmatches ? 1 : 0; +} # search_and_list() + +} # scope around $total_count + + + +sub filetypes_supported_set { + return grep { defined $type_wanted{$_} && ($type_wanted{$_} == 1) } filetypes_supported(); +} + + + +sub print_files { + my $iter = shift; + my $opt = shift; + + my $ors = $opt->{print0} ? "\0" : "\n"; + + my $nmatches = 0; + while ( defined ( my $file = $iter->() ) ) { + App::Ack::print $file, $opt->{show_types} ? " => " . join( ',', filetypes( $file ) ) : (), $ors; + $nmatches++; + last if $opt->{1}; + } + + return $nmatches; +} + + +sub print_files_with_matches { + my $iter = shift; + my $opt = shift; + + # if we have -l and only 1 file given on command line (this means + # show_filename is set to 0), we want to see the filename nevertheless + $opt->{show_filename} = 1 if $opt->{l}; + + $opt->{show_filename} = 0 if $opt->{h}; + $opt->{show_filename} = 1 if $opt->{H}; + + # abuse options to hand in the show_total parameter to search_and_list + $opt->{show_total} = $opt->{count} && !$opt->{show_filename}; + reset_total_count(); + + my $nmatches = 0; + while ( defined ( my $filename = $iter->() ) ) { + my $repo = App::Ack::Repository::Basic->new( $filename ); + my $res; + while ( $res = $repo->next_resource() ) { + $nmatches += search_and_list( $res, $opt ); + $res->close(); + last if $nmatches && $opt->{1}; + } + $repo->close(); + } + + if ( $nmatches && $opt->{show_total} ) { + App::Ack::print_count('', get_total_count(), "\n", 1, 0 ) + } + + return $nmatches; +} + + +sub print_matches { + my $iter = shift; + my $opt = shift; + + $opt->{show_filename} = 0 if $opt->{h}; + $opt->{show_filename} = 1 if $opt->{H}; + + my $nmatches = 0; + while ( defined ( my $filename = $iter->() ) ) { + my $repo; + my $tarballs_work = 0; + if ( $tarballs_work && $filename =~ /\.tar\.gz$/ ) { + App::Ack::die( 'Not working here yet' ); + require App::Ack::Repository::Tar; # XXX Error checking + $repo = App::Ack::Repository::Tar->new( $filename ); + } + else { + $repo = App::Ack::Repository::Basic->new( $filename ); + } + $repo or next; + + while ( my $res = $repo->next_resource() ) { + my $needs_line_scan; + if ( $opt->{regex} && !$opt->{passthru} ) { + $needs_line_scan = $res->needs_line_scan( $opt ); + if ( $needs_line_scan ) { + $res->reset(); + } + } + else { + $needs_line_scan = 1; + } + if ( $needs_line_scan ) { + $nmatches += search_resource( $res, $opt ); + } + $res->close(); + } + last if $nmatches && $opt->{1}; + $repo->close(); + } + return $nmatches; +} + + +sub filetype_setup { + my $filetypes_supported_set = filetypes_supported_set(); + # If anyone says --no-whatever, we assume all other types must be on. + if ( !$filetypes_supported_set ) { + for my $i ( keys %type_wanted ) { + $type_wanted{$i} = 1 unless ( defined( $type_wanted{$i} ) || $i eq 'binary' || $i eq 'text' || $i eq 'skipped' ); + } + } + return; +} + + +EXPAND_FILENAMES_SCOPE: { + my $filter; + + sub expand_filenames { + my $argv = shift; + + my $attr; + my @files; + + foreach my $pattern ( @{$argv} ) { + my @results = bsd_glob( $pattern ); + + if (@results == 0) { + @results = $pattern; # Glob didn't match, pass it thru unchanged + } + elsif ( (@results > 1) or ($results[0] ne $pattern) ) { + if (not defined $filter) { + eval 'require Win32::File;'; + if ($@) { + $filter = 0; + } + else { + $filter = Win32::File::HIDDEN()|Win32::File::SYSTEM(); + } + } # end unless we've tried to load Win32::File + if ( $filter ) { + # Filter out hidden and system files: + @results = grep { not(Win32::File::GetAttributes($_, $attr) and $attr & $filter) } @results; + App::Ack::warn( "$pattern: Matched only hidden files" ) unless @results; + } # end if we can filter by file attributes + } # end elsif this pattern got expanded + + push @files, @results; + } # end foreach pattern + + return \@files; + } # end expand_filenames +} # EXPAND_FILENAMES_SCOPE + + + +sub get_starting_points { + my $argv = shift; + my $opt = shift; + + my @what; + + if ( @{$argv} ) { + @what = @{ $is_windows ? expand_filenames($argv) : $argv }; + $_ = File::Next::reslash( $_ ) for @what; + + # Show filenames unless we've specified one single file + $opt->{show_filename} = (@what > 1) || (!-f $what[0]); + } + else { + @what = '.'; # Assume current directory + $opt->{show_filename} = 1; + } + + for my $start_point (@what) { + App::Ack::warn( "$start_point: No such file or directory" ) unless -e $start_point; + } + return \@what; +} + +sub _match { + my ( $target, $expression, $invert_flag ) = @_; + + if ( $invert_flag ) { + return $target !~ $expression; + } + else { + return $target =~ $expression; + } +} + + +sub get_iterator { + my $what = shift; + my $opt = shift; + + # Starting points are always searched, no matter what + my %starting_point = map { ($_ => 1) } @{$what}; + + my $g_regex = defined $opt->{G} ? qr/$opt->{G}/ : undef; + my $file_filter; + + if ( $g_regex ) { + $file_filter + = $opt->{u} ? sub { _match( $File::Next::name, qr/$g_regex/, $opt->{invert_file_match} ) } # XXX Maybe this should be a 1, no? + : $opt->{all} ? sub { $starting_point{ $File::Next::name } || ( _match( $File::Next::name, qr/$g_regex/, $opt->{invert_file_match} ) && is_searchable( $_ ) ) } + : sub { $starting_point{ $File::Next::name } || ( _match( $File::Next::name, qr/$g_regex/, $opt->{invert_file_match} ) && is_interesting( @ _) ) } + ; + } + else { + $file_filter + = $opt->{u} ? sub {1} + : $opt->{all} ? sub { $starting_point{ $File::Next::name } || is_searchable( $_ ) } + : sub { $starting_point{ $File::Next::name } || is_interesting( @_ ) } + ; + } + + my $descend_filter + = $opt->{n} ? sub {0} + : $opt->{u} ? sub {1} + : \&ignoredir_filter; + + my $iter = + File::Next::files( { + file_filter => $file_filter, + descend_filter => $descend_filter, + error_handler => sub { my $msg = shift; App::Ack::warn( $msg ) }, + sort_files => $opt->{sort_files}, + follow_symlinks => $opt->{follow}, + }, @{$what} ); + return $iter; +} + + +sub set_up_pager { + my $command = shift; + + return if App::Ack::output_to_pipe(); + + my $pager; + if ( not open( $pager, '|-', $command ) ) { + App::Ack::die( qq{Unable to pipe to pager "$command": $!} ); + } + $fh = $pager; + + return; +} + + +sub input_from_pipe { + return $input_from_pipe; +} + + + +sub output_to_pipe { + return $output_to_pipe; +} + + +sub exit_from_ack { + my $nmatches = shift; + + my $rc = $nmatches ? 0 : 1; + exit $rc; +} + + + +1; # End of App::Ack +package App::Ack::Repository; + + +use warnings; +use strict; + +sub FAIL { + require Carp; + Carp::confess( 'Must be overloaded' ); +} + + +sub new { + FAIL(); +} + + +sub next_resource { + FAIL(); +} + + +sub close { + FAIL(); +} + +1; +package App::Ack::Resource; + + +use warnings; +use strict; + +sub FAIL { + require Carp; + Carp::confess( 'Must be overloaded' ); +} + + +sub new { + FAIL(); +} + + +sub name { + FAIL(); +} + + +sub is_binary { + FAIL(); +} + + + +sub needs_line_scan { + FAIL(); +} + + +sub reset { + FAIL(); +} + + +sub next_text { + FAIL(); +} + + +sub close { + FAIL(); +} + +1; +package App::Ack::Plugin::Basic; + + + +package App::Ack::Resource::Basic; + + +use warnings; +use strict; + + +our @ISA = qw( App::Ack::Resource ); + + +sub new { + my $class = shift; + my $filename = shift; + + my $self = bless { + filename => $filename, + fh => undef, + could_be_binary => undef, + opened => undef, + id => undef, + }, $class; + + if ( $self->{filename} eq '-' ) { + $self->{fh} = *STDIN; + $self->{could_be_binary} = 0; + } + else { + if ( !open( $self->{fh}, '<', $self->{filename} ) ) { + App::Ack::warn( "$self->{filename}: $!" ); + return; + } + $self->{could_be_binary} = 1; + } + + return $self; +} + + +sub name { + my $self = shift; + + return $self->{filename}; +} + + +sub is_binary { + my $self = shift; + + if ( $self->{could_be_binary} ) { + return -B $self->{filename}; + } + + return 0; +} + + + +sub needs_line_scan { + my $self = shift; + my $opt = shift; + + return 1 if $opt->{v}; + + my $size = -s $self->{fh}; + if ( $size == 0 ) { + return 0; + } + elsif ( $size > 100_000 ) { + return 1; + } + + my $buffer; + my $rc = sysread( $self->{fh}, $buffer, $size ); + if ( not defined $rc ) { + App::Ack::warn( "$self->{filename}: $!" ); + return 1; + } + return 0 unless $rc && ( $rc == $size ); + + my $regex = $opt->{regex}; + return $buffer =~ /$regex/m; +} + + +sub reset { + my $self = shift; + + seek( $self->{fh}, 0, 0 ) + or App::Ack::warn( "$self->{filename}: $!" ); + + return; +} + + +sub next_text { + if ( defined ($_ = readline $_[0]->{fh}) ) { + $. = ++$_[0]->{line}; + return 1; + } + + return; +} + + +sub close { + my $self = shift; + + if ( not close $self->{fh} ) { + App::Ack::warn( $self->name() . ": $!" ); + } + + return; +} + +package App::Ack::Repository::Basic; + + +our @ISA = qw( App::Ack::Repository ); + + +use warnings; +use strict; + +sub new { + my $class = shift; + my $filename = shift; + + my $self = bless { + filename => $filename, + nexted => 0, + }, $class; + + return $self; +} + + +sub next_resource { + my $self = shift; + + return if $self->{nexted}; + $self->{nexted} = 1; + + return App::Ack::Resource::Basic->new( $self->{filename} ); +} + + +sub close { +} + + + +1; From 31c3cb14f0a271faba8eccb854d250e93e01cb6a Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 19 Jul 2012 09:17:52 -0600 Subject: [PATCH 63/97] :lipstick: --- spec/extensions/command-panel-spec.coffee | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/spec/extensions/command-panel-spec.coffee b/spec/extensions/command-panel-spec.coffee index d102ae32d..54c31e63c 100644 --- a/spec/extensions/command-panel-spec.coffee +++ b/spec/extensions/command-panel-spec.coffee @@ -126,8 +126,6 @@ describe "CommandPanel", -> rootView.trigger 'command-panel:toggle' - commandPanel.miniEditor.insertText - waitsForPromise -> commandPanel.execute('X x/a+/') runs -> @@ -138,7 +136,7 @@ describe "CommandPanel", -> expect(previewItem.preview.text()).toBe "aaa bbb" expect(previewItem.preview.find(".match").text()).toBe "aaa" - rootView.trigger 'command-panel:toggle' + rootView.trigger 'command-panel:toggle' # ensure we can close panel without problems describe "if the command is malformed", -> it "adds and removes an error class to the command panel and does not close it", -> From a61064eeeb46782fbaa0a315198846898f298ea1 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 19 Jul 2012 09:20:45 -0600 Subject: [PATCH 64/97] Throw an exception when creating a `File` object for a path that isn't a file This exception is getting thrown inside of `Project.scan` when it incorrectly parses a path with newlines in it. But the exception gets swallowed by ChildProcess.exec. At least it prevents a file from being created that we don't end up unsubscribing from. But `Project.scan` needs to be improved. --- src/app/file.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/file.coffee b/src/app/file.coffee index c19c54a22..4c3826f0e 100644 --- a/src/app/file.coffee +++ b/src/app/file.coffee @@ -9,6 +9,7 @@ class File md5: null constructor: (@path) -> + throw "Creating file with path that is not a file: #{@path}" unless fs.isFile(@path) @updateMd5() getPath: -> From 7a85e07cf1e9ebd025d75d3db1deb1d3afa0f4ee Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 19 Jul 2012 10:27:42 -0600 Subject: [PATCH 65/97] Project.scan handles paths with newlines --- spec/app/project-spec.coffee | 7 ++++++- src/app/project.coffee | 27 +++++++++++++++++++-------- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/spec/app/project-spec.coffee b/spec/app/project-spec.coffee index 8a79e3931..a645d29c6 100644 --- a/spec/app/project-spec.coffee +++ b/spec/app/project-spec.coffee @@ -138,6 +138,11 @@ describe "Project", -> it "works on evil filenames", -> project.setPath(require.resolve('fixtures/evil-files')) + paths = [] waitsForPromise -> project.scan /evil/, ({path, match, range}) -> - #console.log path + paths.push(path) + + runs -> + expect(paths[0]).toMatch /file with spaces.txt$/ + expect(paths[1]).toMatch /goddam\nnewlines$/m diff --git a/src/app/project.coffee b/src/app/project.coffee index d8700c5c1..ca9c677b3 100644 --- a/src/app/project.coffee +++ b/src/app/project.coffee @@ -138,16 +138,27 @@ class Project scan: (regex, iterator) -> regex = new RegExp(regex.source, 'g') - command = "#{require.resolve('ack')} --all-types --match \"#{regex.source}\" \"#{@getPath()}\"" - ChildProcess.exec command , bufferLines: true, stdout: (data) -> - for grepLine in data.split('\n') when grepLine.length - pathEndIndex = grepLine.indexOf('\0') - lineNumberEndIndex = grepLine.indexOf('\0', pathEndIndex + 1) - path = grepLine.substring(0, pathEndIndex) - row = parseInt(grepLine.substring(pathEndIndex + 1, lineNumberEndIndex)) - 1 - line = grepLine.substring(lineNumberEndIndex + 1) + command = "#{require.resolve('ack')} --all --match \"#{regex.source}\" \"#{@getPath()}\"" + bufferedData = "" + promise = ChildProcess.exec command , bufferLines: true, stdout: (data) -> + bufferedData += data + currentIndex = 0 + while currentIndex < bufferedData.length + pathEndIndex = bufferedData.indexOf('\0', currentIndex) + break unless pathEndIndex >= 0 + lineNumberEndIndex = bufferedData.indexOf('\0', pathEndIndex + 1) + lineEndIndex = bufferedData.indexOf('\n', lineNumberEndIndex + 1) + + path = bufferedData.substring(currentIndex, pathEndIndex) + row = parseInt(bufferedData.substring(pathEndIndex + 1, lineNumberEndIndex)) - 1 + line = bufferedData.substring(lineNumberEndIndex + 1, lineEndIndex) + while match = regex.exec(line) range = new Range([row, match.index], [row, match.index + match[0].length]) iterator({path, match, range}) + currentIndex = lineEndIndex + 1 + + bufferedData = bufferedData.substring(currentIndex) + _.extend Project.prototype, EventEmitter From b1b6b3a3108864ebd9d78fe477e4f1657c340851 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 19 Jul 2012 11:14:25 -0600 Subject: [PATCH 66/97] Convert NSString to std::string with a length appropriate to the UTF8 encoding --- Atom/src/native_handler.mm | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Atom/src/native_handler.mm b/Atom/src/native_handler.mm index c3e096c68..ed426a327 100644 --- a/Atom/src/native_handler.mm +++ b/Atom/src/native_handler.mm @@ -8,6 +8,8 @@ #import #import +#import + #define MY_EXCEPTION_TRY @try { #define MY_EXCEPTION_HANDLE } @catch (NSException *localException) {} @@ -448,12 +450,12 @@ bool NativeHandler::Execute(const CefString& name, NSData *data = [fileHandle availableData]; NSString *contents = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; - + CefV8ValueList args; CefRefPtr retval = CefV8Value::CreateBool(YES); CefRefPtr e; - args.push_back(CefV8Value::CreateString(std::string([contents UTF8String], [contents length]))); - + + args.push_back(CefV8Value::CreateString(std::string([contents UTF8String], [contents lengthOfBytesUsingEncoding:NSUTF8StringEncoding]))); function->ExecuteFunction(function, args, retval, e, false); [contents release]; context->Exit(); From 12f765cc4716646a24a7314ecbbbc23d8d47e30e Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 19 Jul 2012 11:14:38 -0600 Subject: [PATCH 67/97] :lipstick: --- src/app/project.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/project.coffee b/src/app/project.coffee index ca9c677b3..733929605 100644 --- a/src/app/project.coffee +++ b/src/app/project.coffee @@ -140,7 +140,7 @@ class Project regex = new RegExp(regex.source, 'g') command = "#{require.resolve('ack')} --all --match \"#{regex.source}\" \"#{@getPath()}\"" bufferedData = "" - promise = ChildProcess.exec command , bufferLines: true, stdout: (data) -> + ChildProcess.exec command , bufferLines: true, stdout: (data) -> bufferedData += data currentIndex = 0 while currentIndex < bufferedData.length From f0417e72870f795888a8e0dd2e3c2a3f6496c1d0 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 19 Jul 2012 11:15:08 -0600 Subject: [PATCH 68/97] Ensure that `Project.scan` handles evil file fixtures correctly --- spec/app/project-spec.coffee | 13 +++++++++++-- spec/fixtures/evil-files/a_file_with_utf8.txt | 1 + 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 spec/fixtures/evil-files/a_file_with_utf8.txt diff --git a/spec/app/project-spec.coffee b/spec/app/project-spec.coffee index a645d29c6..ae0232e1c 100644 --- a/spec/app/project-spec.coffee +++ b/spec/app/project-spec.coffee @@ -139,10 +139,19 @@ describe "Project", -> it "works on evil filenames", -> project.setPath(require.resolve('fixtures/evil-files')) paths = [] + matches = [] waitsForPromise -> project.scan /evil/, ({path, match, range}) -> paths.push(path) + matches.push(match) runs -> - expect(paths[0]).toMatch /file with spaces.txt$/ - expect(paths[1]).toMatch /goddam\nnewlines$/m + expect(paths.length).toBe 5 + matches.forEach (match) -> expect(match).toEqual ['evil'] + expect(paths[0]).toMatch /a_file_with_utf8.txt$/ + expect(paths[1]).toMatch /file with spaces.txt$/ + expect(paths[2]).toMatch /goddam\nnewlines$/m + expect(paths[3]).toMatch /quote".txt$/m + expect(fs.base(paths[4])).toBe "utfa\u0306.md" + + diff --git a/spec/fixtures/evil-files/a_file_with_utf8.txt b/spec/fixtures/evil-files/a_file_with_utf8.txt new file mode 100644 index 000000000..a811bd702 --- /dev/null +++ b/spec/fixtures/evil-files/a_file_with_utf8.txt @@ -0,0 +1 @@ +I am evil because there's a UTF-8 character right here: ă From 9e4f4014851bf99281d8c573c99f785cb4e08b92 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 19 Jul 2012 11:28:41 -0600 Subject: [PATCH 69/97] WIP: trying to make preview faster --- src/app/project.coffee | 7 ++++++- src/app/window.coffee | 6 ++++++ src/extensions/command-panel/command-panel.coffee | 14 +++++++++++--- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/app/project.coffee b/src/app/project.coffee index 733929605..918de78e6 100644 --- a/src/app/project.coffee +++ b/src/app/project.coffee @@ -140,7 +140,9 @@ class Project regex = new RegExp(regex.source, 'g') command = "#{require.resolve('ack')} --all --match \"#{regex.source}\" \"#{@getPath()}\"" bufferedData = "" - ChildProcess.exec command , bufferLines: true, stdout: (data) -> + + console.log command + promise = ChildProcess.exec command , bufferLines: true, stdout: (data) -> bufferedData += data currentIndex = 0 while currentIndex < bufferedData.length @@ -161,4 +163,7 @@ class Project bufferedData = bufferedData.substring(currentIndex) + promise.done -> console.log "DONE" + + _.extend Project.prototype, EventEmitter diff --git a/src/app/window.coffee b/src/app/window.coffee index 35382f5a0..93f748833 100644 --- a/src/app/window.coffee +++ b/src/app/window.coffee @@ -83,6 +83,12 @@ windowAdditions = onerror: -> $native.showDevTools() + measure: (description, fn) -> + start = new Date().getTime() + fn() + result = new Date().getTime() - start + console.log description, result + window[key] = value for key, value of windowAdditions window.setUpKeymap() diff --git a/src/extensions/command-panel/command-panel.coffee b/src/extensions/command-panel/command-panel.coffee index 1f0967ec1..9a7d0f30a 100644 --- a/src/extensions/command-panel/command-panel.coffee +++ b/src/extensions/command-panel/command-panel.coffee @@ -1,4 +1,4 @@ -{View} = require 'space-pen' +{View, $$$} = require 'space-pen' CommandInterpreter = require 'command-panel/command-interpreter' RegexAddress = require 'command-panel/commands/regex-address' CompositeCommand = require 'command-panel/commands/composite-command' @@ -90,8 +90,16 @@ class CommandPanel extends View populatePreviewList: (operations) -> @previewedOperations = operations @previewList.empty() - for operation in operations - @previewList.append(new PreviewItem(operation)) + @previewList.html $$$ -> + for operation in operations + {prefix, suffix, match} = operation.preview() + @li => + @span operation.getPath(), outlet: "path", class: "path" + @span outlet: "preview", class: "preview", => + @span prefix + @span match, class: 'match' + @span suffix + @previewList.show() navigateBackwardInHistory: -> From f37330a21867a4a0471972f760fdf1d0dd51e417 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 19 Jul 2012 17:27:51 -0600 Subject: [PATCH 70/97] Fix spec to now that we don't build PreviewItems --- spec/extensions/command-panel-spec.coffee | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/extensions/command-panel-spec.coffee b/spec/extensions/command-panel-spec.coffee index 54c31e63c..23e63dddd 100644 --- a/spec/extensions/command-panel-spec.coffee +++ b/spec/extensions/command-panel-spec.coffee @@ -131,10 +131,10 @@ describe "CommandPanel", -> runs -> expect(commandPanel).toBeVisible() expect(commandPanel.previewList).toBeVisible() - previewItem = commandPanel.previewList.find("li:contains(dir/a)").view() - expect(previewItem.path.text()).toBe "dir/a" - expect(previewItem.preview.text()).toBe "aaa bbb" - expect(previewItem.preview.find(".match").text()).toBe "aaa" + previewItem = commandPanel.previewList.find("li:contains(dir/a):first") + expect(previewItem.find('.path').text()).toBe "dir/a" + expect(previewItem.find('.preview').text()).toBe "aaa bbb" + expect(previewItem.find('.preview > .match').text()).toBe "aaa" rootView.trigger 'command-panel:toggle' # ensure we can close panel without problems From 239cba25e462df65535d5bf183e9267907a96e67 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 19 Jul 2012 17:28:46 -0600 Subject: [PATCH 71/97] Use the silver searcher to implement global search Parsing uses a simple state machine in `Project.scan`. --- spec/app/project-spec.coffee | 8 ++--- src/app/project.coffee | 64 ++++++++++++++++++++++++----------- vendor/ag | Bin 0 -> 38956 bytes 3 files changed, 48 insertions(+), 24 deletions(-) create mode 100755 vendor/ag diff --git a/spec/app/project-spec.coffee b/spec/app/project-spec.coffee index ae0232e1c..a8a50a0cc 100644 --- a/spec/app/project-spec.coffee +++ b/spec/app/project-spec.coffee @@ -128,12 +128,12 @@ describe "Project", -> runs -> expect(matches[0]).toEqual path: project.resolve('a') - match: ['aaa', 'a'] + match: 'aaa' range: [[0, 0], [0, 3]] expect(matches[1]).toEqual path: project.resolve('a') - match: ['aa', 'a'] + match: 'aa' range: [[1, 3], [1, 5]] it "works on evil filenames", -> @@ -147,11 +147,9 @@ describe "Project", -> runs -> expect(paths.length).toBe 5 - matches.forEach (match) -> expect(match).toEqual ['evil'] + matches.forEach (match) -> expect(match).toEqual 'evil' expect(paths[0]).toMatch /a_file_with_utf8.txt$/ expect(paths[1]).toMatch /file with spaces.txt$/ expect(paths[2]).toMatch /goddam\nnewlines$/m expect(paths[3]).toMatch /quote".txt$/m expect(fs.base(paths[4])).toBe "utfa\u0306.md" - - diff --git a/src/app/project.coffee b/src/app/project.coffee index 918de78e6..72745f1e9 100644 --- a/src/app/project.coffee +++ b/src/app/project.coffee @@ -138,32 +138,58 @@ class Project scan: (regex, iterator) -> regex = new RegExp(regex.source, 'g') - command = "#{require.resolve('ack')} --all --match \"#{regex.source}\" \"#{@getPath()}\"" + command = "#{require.resolve('ag')} --ackmate \"#{regex.source}\" \"#{@getPath()}\"" bufferedData = "" - console.log command - promise = ChildProcess.exec command , bufferLines: true, stdout: (data) -> - bufferedData += data - currentIndex = 0 - while currentIndex < bufferedData.length - pathEndIndex = bufferedData.indexOf('\0', currentIndex) - break unless pathEndIndex >= 0 - lineNumberEndIndex = bufferedData.indexOf('\0', pathEndIndex + 1) - lineEndIndex = bufferedData.indexOf('\n', lineNumberEndIndex + 1) + state = 'readingPath' + path = null + + readPath = (line) -> + if /^[0-9,; ]+:/.test(line) + state = 'readingLines' + else if /^:/.test line + path = line.substr(1) + else + path += ('\n' + line) + + readLine = (line) -> + if line.length == 0 + state = 'readingPath' + path = null + else + colonIndex = line.indexOf(':') + matchInfo = line.substring(0, colonIndex) + lineText = line.substring(colonIndex + 1) + readMatches(matchInfo, lineText) + + readMatches = (matchInfo, lineText) -> + [lineNumber, matchPositionsText] = matchInfo.match(/(\d+);(.+)/)[1..] + row = parseInt(lineNumber) - 1 + + + + matchPositions = matchPositionsText.split(',').map (positionText) -> positionText.split(' ').map (pos) -> parseInt(pos) + + for [column, length] in matchPositions + range = new Range([row, column], [row, column + length]) + match = lineText.substr(column, length) + iterator({path, range, match}) + + ChildProcess.exec command , bufferLines: true, stdout: (data) -> + lines = data.split('\n') + try + for line in lines + readPath(line) if state is 'readingPath' + readLine(line) if state is 'readingLines' + catch e + console.log e.stack + + - path = bufferedData.substring(currentIndex, pathEndIndex) - row = parseInt(bufferedData.substring(pathEndIndex + 1, lineNumberEndIndex)) - 1 - line = bufferedData.substring(lineNumberEndIndex + 1, lineEndIndex) - while match = regex.exec(line) - range = new Range([row, match.index], [row, match.index + match[0].length]) - iterator({path, match, range}) - currentIndex = lineEndIndex + 1 - bufferedData = bufferedData.substring(currentIndex) - promise.done -> console.log "DONE" _.extend Project.prototype, EventEmitter diff --git a/vendor/ag b/vendor/ag new file mode 100755 index 0000000000000000000000000000000000000000..9fe011bbdc9cac890d40933136fcfe838c6e02bf GIT binary patch literal 38956 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 Date: Thu, 19 Jul 2012 19:13:28 -0600 Subject: [PATCH 72/97] Don't swallow all command panel errors as if they were SyntaxErrors This wasn't our fault. SyntaxError's implementation is weird and so every error ends up being an instance of it. Boo. --- src/extensions/command-panel/command-panel.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extensions/command-panel/command-panel.coffee b/src/extensions/command-panel/command-panel.coffee index 9a7d0f30a..f299e4e03 100644 --- a/src/extensions/command-panel/command-panel.coffee +++ b/src/extensions/command-panel/command-panel.coffee @@ -81,7 +81,7 @@ class CommandPanel extends View else @detach() catch error - if error instanceof SyntaxError + if error.name is "SyntaxError" @flashError() return else From d24f9667f8859f65ba024e53a1f2dbc04bcd566d Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 19 Jul 2012 19:14:05 -0600 Subject: [PATCH 73/97] Add exception warning to stdout handler of native child process exec --- Atom/src/native_handler.mm | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Atom/src/native_handler.mm b/Atom/src/native_handler.mm index ed426a327..179f15cf6 100644 --- a/Atom/src/native_handler.mm +++ b/Atom/src/native_handler.mm @@ -457,6 +457,11 @@ bool NativeHandler::Execute(const CefString& name, args.push_back(CefV8Value::CreateString(std::string([contents UTF8String], [contents lengthOfBytesUsingEncoding:NSUTF8StringEncoding]))); function->ExecuteFunction(function, args, retval, e, false); + + if (e.get()) { + NSLog(@"Error thrown in OutputHandle %s", e->GetMessage().ToString().c_str()); + } + [contents release]; context->Exit(); }; From b27a793b920ab9bb8de4c5e707f01ede8368b28f Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 19 Jul 2012 19:17:31 -0600 Subject: [PATCH 74/97] Fix bug in Project.scan that was getting the state machine into a bad state Chunks of stdout data are being buffered by lines. But then we were splitting these chunks on \n and the last fragment was therefore always "". This caused problems. Still need to test this. --- src/app/project.coffee | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/src/app/project.coffee b/src/app/project.coffee index 72745f1e9..b2aa0f006 100644 --- a/src/app/project.coffee +++ b/src/app/project.coffee @@ -165,9 +165,6 @@ class Project readMatches = (matchInfo, lineText) -> [lineNumber, matchPositionsText] = matchInfo.match(/(\d+);(.+)/)[1..] row = parseInt(lineNumber) - 1 - - - matchPositions = matchPositionsText.split(',').map (positionText) -> positionText.split(' ').map (pos) -> parseInt(pos) for [column, length] in matchPositions @@ -177,19 +174,9 @@ class Project ChildProcess.exec command , bufferLines: true, stdout: (data) -> lines = data.split('\n') - try - for line in lines - readPath(line) if state is 'readingPath' - readLine(line) if state is 'readingLines' - catch e - console.log e.stack - - - - - - - - + lines.pop() # the last segment is a spurios '' because data always ends in \n due to bufferLines: true + for line in lines + readPath(line) if state is 'readingPath' + readLine(line) if state is 'readingLines' _.extend Project.prototype, EventEmitter From 411215f6c8acdfaf33d2b1b32de673394f1c9d62 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 20 Jul 2012 10:44:49 -0600 Subject: [PATCH 75/97] Fix bug where cursor occasionally precedes '/' after pressing meta-f Now we always move cursor to end when setting the command panel's mini-editor text. --- spec/extensions/command-panel-spec.coffee | 6 +++++- src/extensions/command-panel/command-panel.coffee | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/spec/extensions/command-panel-spec.coffee b/spec/extensions/command-panel-spec.coffee index 23e63dddd..cd481e68b 100644 --- a/spec/extensions/command-panel-spec.coffee +++ b/spec/extensions/command-panel-spec.coffee @@ -97,10 +97,14 @@ describe "CommandPanel", -> expect(commandInterpreter.lastRelativeAddress.subcommands[0].regex.toString()).toEqual "/\\(items\\)/" describe "when command-panel:find-in-file is triggered on an editor", -> - it "pre-populates command panel's editor with /", -> + it "pre-populates command panel's editor with / and moves the cursor to column 1", -> + commandPanel.miniEditor.setText("foo") + commandPanel.miniEditor.setCursorBufferPosition([0, 0]) + rootView.getActiveEditor().trigger "command-panel:find-in-file" expect(commandPanel.parent).not.toBeEmpty() expect(commandPanel.miniEditor.getText()).toBe "/" + expect(commandPanel.miniEditor.getCursorBufferPosition()).toEqual [0, 1] describe "when esc is pressed in the command panel", -> it "closes the command panel", -> diff --git a/src/extensions/command-panel/command-panel.coffee b/src/extensions/command-panel/command-panel.coffee index f299e4e03..c79dd9f38 100644 --- a/src/extensions/command-panel/command-panel.coffee +++ b/src/extensions/command-panel/command-panel.coffee @@ -63,6 +63,7 @@ class CommandPanel extends View @previewList.hide() @miniEditor.focus() @miniEditor.setText(text) + @miniEditor.setCursorBufferPosition([0, Infinity]) detach: -> @rootView.focus() From 73654a5e3f00dad02e0bff16d0e168fdeb543758 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 20 Jul 2012 13:56:55 -0600 Subject: [PATCH 76/97] Improve command panel meta-f spec --- spec/extensions/command-panel-spec.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/extensions/command-panel-spec.coffee b/spec/extensions/command-panel-spec.coffee index cd481e68b..62e7b94f2 100644 --- a/spec/extensions/command-panel-spec.coffee +++ b/spec/extensions/command-panel-spec.coffee @@ -97,11 +97,13 @@ describe "CommandPanel", -> expect(commandInterpreter.lastRelativeAddress.subcommands[0].regex.toString()).toEqual "/\\(items\\)/" describe "when command-panel:find-in-file is triggered on an editor", -> - it "pre-populates command panel's editor with / and moves the cursor to column 1", -> + it "pre-populates the command panel's editor with / and moves the cursor to the last column", -> + spyOn(commandPanel, 'attach').andCallThrough() commandPanel.miniEditor.setText("foo") commandPanel.miniEditor.setCursorBufferPosition([0, 0]) rootView.getActiveEditor().trigger "command-panel:find-in-file" + expect(commandPanel.attach).toHaveBeenCalled() expect(commandPanel.parent).not.toBeEmpty() expect(commandPanel.miniEditor.getText()).toBe "/" expect(commandPanel.miniEditor.getCursorBufferPosition()).toEqual [0, 1] From d16328c533f65952e51b6f67af1a151955217f93 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 20 Jul 2012 13:57:33 -0600 Subject: [PATCH 77/97] Meta-shift-f opens the command panel pre-populated with Xx/ to find in project --- spec/extensions/command-panel-spec.coffee | 12 ++++++++++++ src/extensions/command-panel/command-panel.coffee | 1 + src/extensions/command-panel/keymap.coffee | 1 + 3 files changed, 14 insertions(+) diff --git a/spec/extensions/command-panel-spec.coffee b/spec/extensions/command-panel-spec.coffee index 62e7b94f2..232278fd8 100644 --- a/spec/extensions/command-panel-spec.coffee +++ b/spec/extensions/command-panel-spec.coffee @@ -108,6 +108,18 @@ describe "CommandPanel", -> expect(commandPanel.miniEditor.getText()).toBe "/" expect(commandPanel.miniEditor.getCursorBufferPosition()).toEqual [0, 1] + describe "when command-panel:find-in-project is triggered on the root view", -> + it "pre-populates the command panel's editor with Xx/ and moves the cursor to the last column", -> + spyOn(commandPanel, 'attach').andCallThrough() + commandPanel.miniEditor.setText("foo") + commandPanel.miniEditor.setCursorBufferPosition([0, 0]) + + rootView.trigger "command-panel:find-in-project" + expect(commandPanel.attach).toHaveBeenCalled() + expect(commandPanel.parent).not.toBeEmpty() + expect(commandPanel.miniEditor.getText()).toBe "Xx/" + expect(commandPanel.miniEditor.getCursorBufferPosition()).toEqual [0, 3] + describe "when esc is pressed in the command panel", -> it "closes the command panel", -> rootView.trigger 'command-panel:toggle' diff --git a/src/extensions/command-panel/command-panel.coffee b/src/extensions/command-panel/command-panel.coffee index c79dd9f38..b1780029b 100644 --- a/src/extensions/command-panel/command-panel.coffee +++ b/src/extensions/command-panel/command-panel.coffee @@ -47,6 +47,7 @@ class CommandPanel extends View @rootView.on 'command-panel:toggle', => @toggle() @rootView.on 'command-panel:execute', => @execute() @rootView.on 'command-panel:find-in-file', => @attach("/") + @rootView.on 'command-panel:find-in-project', => @attach("Xx/") @rootView.on 'command-panel:repeat-relative-address', => @repeatRelativeAddress() @rootView.on 'command-panel:repeat-relative-address-in-reverse', => @repeatRelativeAddressInReverse() @rootView.on 'command-panel:set-selection-as-regex-address', => @setSelectionAsLastRelativeAddress() diff --git a/src/extensions/command-panel/keymap.coffee b/src/extensions/command-panel/keymap.coffee index 0f7d1a7be..942df5212 100644 --- a/src/extensions/command-panel/keymap.coffee +++ b/src/extensions/command-panel/keymap.coffee @@ -10,3 +10,4 @@ window.keymap.bindKeys '.editor', 'meta-G': 'command-panel:repeat-relative-address-in-reverse' 'meta-e': 'command-panel:set-selection-as-regex-address' 'meta-f': 'command-panel:find-in-file' + 'meta-F': 'command-panel:find-in-project' From 713b5a9620f877ed90ce7163e480f4e1a7ebb0e7 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Fri, 20 Jul 2012 14:15:29 -0700 Subject: [PATCH 78/97] Better $native.exec errors --- Atom/src/native_handler.mm | 18 +++++++++++++++--- src/stdlib/child-process.coffee | 12 ++++++++---- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/Atom/src/native_handler.mm b/Atom/src/native_handler.mm index 179f15cf6..74ebe18d6 100644 --- a/Atom/src/native_handler.mm +++ b/Atom/src/native_handler.mm @@ -18,6 +18,18 @@ NSString *stringFromCefV8Value(const CefRefPtr& value) { return [NSString stringWithUTF8String:cc_value.c_str()]; } +void throwException(const CefRefPtr& global, CefRefPtr& exception, NSString *message) { + CefV8ValueList arguments; + CefRefPtr retval; + CefRefPtr e; + + message = [message stringByAppendingFormat:@"\n%s", exception->GetMessage().ToString().c_str()]; + arguments.push_back(CefV8Value::CreateString(std::string([message UTF8String], [message lengthOfBytesUsingEncoding:NSUTF8StringEncoding]))); + + CefRefPtr console = global->GetValue("console"); + console->GetValue("error")->ExecuteFunction(console, arguments, retval, e, false); +} + NativeHandler::NativeHandler() : CefV8Handler() { std::string extensionCode = "var $native = {}; (function() {"; @@ -459,7 +471,7 @@ bool NativeHandler::Execute(const CefString& name, function->ExecuteFunction(function, args, retval, e, false); if (e.get()) { - NSLog(@"Error thrown in OutputHandle %s", e->GetMessage().ToString().c_str()); + throwException(context->GetGlobal(), e, @"Error thrown in OutputHandle"); } [contents release]; @@ -472,7 +484,7 @@ bool NativeHandler::Execute(const CefString& name, NSString *errorOutput = [[NSString alloc] initWithData:[[task.standardError fileHandleForReading] readDataToEndOfFile] encoding:NSUTF8StringEncoding]; CefV8ValueList args; - CefRefPtr retval = CefV8Value::CreateBool(YES); + CefRefPtr retval; CefRefPtr e; args.push_back(CefV8Value::CreateInt([task terminationStatus])); @@ -482,7 +494,7 @@ bool NativeHandler::Execute(const CefString& name, callback->ExecuteFunction(callback, args, retval, e, false); if (e.get()) { - NSLog(@"Error thrown in TaskTerminatedHandle %s", e->GetMessage().ToString().c_str()); + throwException(context->GetGlobal(), e, @"Error thrown in TaskTerminatedHandle"); } context->Exit(); diff --git a/src/stdlib/child-process.coffee b/src/stdlib/child-process.coffee index 4f8db76bf..9bddda62e 100644 --- a/src/stdlib/child-process.coffee +++ b/src/stdlib/child-process.coffee @@ -14,10 +14,14 @@ class ChildProccess options.stderr = @bufferLines(options.stderr) if options.stderr $native.exec command, options, (exitStatus, stdout, stderr) -> - if exitStatus != 0 - deferred.reject({command, exitStatus, stderr}) - else - deferred.resolve(stdout, stderr) + try + if exitStatus != 0 + deferred.reject({command, exitStatus, stderr}) + else + deferred.resolve(stdout, stderr) + catch e + console.error "In ChildProccess termination callback: ", e.message + console.error e.stack deferred From cc292b604dddb76aed1a27c8f7c9ac589fb0ebd6 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Fri, 20 Jul 2012 14:17:14 -0700 Subject: [PATCH 79/97] Select preview item when preview list is shown --- spec/extensions/command-panel-spec.coffee | 40 ++++++++++--------- .../command-panel/command-panel.coffee | 23 +++-------- .../command-panel/preview-item.coffee | 15 ------- .../command-panel/preview-list.coffee | 35 ++++++++++++++++ static/command-panel.css | 4 ++ 5 files changed, 66 insertions(+), 51 deletions(-) delete mode 100644 src/extensions/command-panel/preview-item.coffee create mode 100644 src/extensions/command-panel/preview-list.coffee diff --git a/spec/extensions/command-panel-spec.coffee b/spec/extensions/command-panel-spec.coffee index 232278fd8..75deade22 100644 --- a/spec/extensions/command-panel-spec.coffee +++ b/spec/extensions/command-panel-spec.coffee @@ -137,25 +137,6 @@ describe "CommandPanel", -> expect(buffer.lineForRow(0)).toMatch /quicktorta/ expect(buffer.lineForRow(1)).toMatch /var torta/ - describe "when the command returns operations to be previewed", -> - it "displays a preview of the operations above the mini-editor", -> - rootView.attachToDom() - editor.remove() - - rootView.trigger 'command-panel:toggle' - - waitsForPromise -> commandPanel.execute('X x/a+/') - - runs -> - expect(commandPanel).toBeVisible() - expect(commandPanel.previewList).toBeVisible() - previewItem = commandPanel.previewList.find("li:contains(dir/a):first") - expect(previewItem.find('.path').text()).toBe "dir/a" - expect(previewItem.find('.preview').text()).toBe "aaa bbb" - expect(previewItem.find('.preview > .match').text()).toBe "aaa" - - rootView.trigger 'command-panel:toggle' # ensure we can close panel without problems - describe "if the command is malformed", -> it "adds and removes an error class to the command panel and does not close it", -> rootView.trigger 'command-panel:toggle' @@ -187,6 +168,27 @@ describe "CommandPanel", -> commandPanel.miniEditor.trigger 'move-down' expect(commandPanel.miniEditor.getText()).toBe '' + describe "when the command returns operations to be previewed", -> + it "displays a preview of the operations above the mini-editor", -> + rootView.attachToDom() + editor.remove() + + rootView.trigger 'command-panel:toggle' + + waitsForPromise -> commandPanel.execute('X x/a+/') + + runs -> + expect(commandPanel).toBeVisible() + expect(commandPanel.previewList).toBeVisible() + previewItem = commandPanel.previewList.find("li:contains(dir/a):first") + expect(previewItem.find('.path').text()).toBe "dir/a" + expect(previewItem.find('.preview').text()).toBe "aaa bbb" + expect(previewItem.find('.preview > .match').text()).toBe "aaa" + + expect(commandPanel.previewList.find("li:first")).toHaveClass('selected') + + rootView.trigger 'command-panel:toggle' # ensure we can close panel without problems + describe ".execute()", -> it "executes the command and closes the command panel", -> rootView.getActiveEditor().setText("i hate love") diff --git a/src/extensions/command-panel/command-panel.coffee b/src/extensions/command-panel/command-panel.coffee index b1780029b..26eeb6860 100644 --- a/src/extensions/command-panel/command-panel.coffee +++ b/src/extensions/command-panel/command-panel.coffee @@ -2,7 +2,7 @@ CommandInterpreter = require 'command-panel/command-interpreter' RegexAddress = require 'command-panel/commands/regex-address' CompositeCommand = require 'command-panel/commands/composite-command' -PreviewItem = require 'command-panel/preview-item' +PreviewList = require 'command-panel/preview-list' Editor = require 'editor' {SyntaxError} = require('pegjs').parser @@ -31,7 +31,7 @@ class CommandPanel extends View @content: -> @div class: 'command-panel', => - @ol class: 'preview-list', outlet: 'previewList' + @subview 'previewList', new PreviewList() @div class: 'prompt-and-editor', => @div ':', class: 'prompt', outlet: 'prompt' @subview 'miniEditor', new Editor(mini: true) @@ -75,11 +75,11 @@ class CommandPanel extends View execute: (command = @miniEditor.getText()) -> try - @commandInterpreter.eval(command, @rootView.getActiveEditSession()).done (operations) => + @commandInterpreter.eval(command, @rootView.getActiveEditSession()).done (operationsToPreview) => @history.push(command) @historyIndex = @history.length - if operations?.length - @populatePreviewList(operations) + if operationsToPreview?.length + @populatePreviewList(operationsToPreview) else @detach() catch error @@ -91,18 +91,7 @@ class CommandPanel extends View populatePreviewList: (operations) -> @previewedOperations = operations - @previewList.empty() - @previewList.html $$$ -> - for operation in operations - {prefix, suffix, match} = operation.preview() - @li => - @span operation.getPath(), outlet: "path", class: "path" - @span outlet: "preview", class: "preview", => - @span prefix - @span match, class: 'match' - @span suffix - - @previewList.show() + @previewList.populate(operations) navigateBackwardInHistory: -> return if @historyIndex == 0 diff --git a/src/extensions/command-panel/preview-item.coffee b/src/extensions/command-panel/preview-item.coffee deleted file mode 100644 index dc72d2371..000000000 --- a/src/extensions/command-panel/preview-item.coffee +++ /dev/null @@ -1,15 +0,0 @@ -{View} = require 'space-pen' - -module.exports = -class PreviewItem extends View - @content: (operation) -> - {prefix, suffix, match} = operation.preview() - - @li => - @span operation.getPath(), outlet: "path", class: "path" - @span outlet: "preview", class: "preview", => - @span prefix - @span match, class: 'match' - @span suffix - - diff --git a/src/extensions/command-panel/preview-list.coffee b/src/extensions/command-panel/preview-list.coffee new file mode 100644 index 000000000..01740c11f --- /dev/null +++ b/src/extensions/command-panel/preview-list.coffee @@ -0,0 +1,35 @@ +{$$$, View} = require 'space-pen' + +module.exports = +class PreviewList extends View + @content: -> + @ol class: 'preview-list', -> + + selectedOperationIndex: 0 + operations: null + + initialize: -> + + populate: (@operations) -> + @empty() + @html $$$ -> + for operation in operations + {prefix, suffix, match} = operation.preview() + @li => + @span operation.getPath(), outlet: "path", class: "path" + @span outlet: "preview", class: "preview", => + @span prefix + @span match, class: 'match' + @span suffix + + @setSelectedOperationIndex(0) + + @show() + + setSelectedOperationIndex: (index) -> + @children(".selected").removeClass('selected') + console.log @children("li:eq(#{index})") + @children("li:eq(#{index})").addClass('selected') + + #getSelectedOperation: -> + #@operations[@selectedOperationIndex] diff --git a/static/command-panel.css b/static/command-panel.css index 89ffd8dd0..0bc89281f 100644 --- a/static/command-panel.css +++ b/static/command-panel.css @@ -36,3 +36,7 @@ font-weight: bold; padding: .2em; } + +.command-panel .preview-list li.selected { + background-color: green; +} \ No newline at end of file From 4f401b2ada518ba9d88eb3a07f824662af0f400a Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 20 Jul 2012 19:11:40 -0600 Subject: [PATCH 80/97] :lipstick: --- src/extensions/command-panel/preview-list.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/src/extensions/command-panel/preview-list.coffee b/src/extensions/command-panel/preview-list.coffee index 01740c11f..a10a0982b 100644 --- a/src/extensions/command-panel/preview-list.coffee +++ b/src/extensions/command-panel/preview-list.coffee @@ -28,7 +28,6 @@ class PreviewList extends View setSelectedOperationIndex: (index) -> @children(".selected").removeClass('selected') - console.log @children("li:eq(#{index})") @children("li:eq(#{index})").addClass('selected') #getSelectedOperation: -> From 7f5c5887238d3ebe7414c97ae261f05941822901 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 21 Jul 2012 17:21:49 -0600 Subject: [PATCH 81/97] Statically link the silver searcher binary against PCRE so it isn't required --- vendor/ag | Bin 38956 -> 226672 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/vendor/ag b/vendor/ag index 9fe011bbdc9cac890d40933136fcfe838c6e02bf..63e16c228b9f49ca1257e9b516c651a187356e16 100755 GIT binary patch literal 226672 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
JEt#ZZM?@a_}J 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 From a0e3ce5f1ce8541fdd59de8a463a98dfceca4bb9 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 23 Jul 2012 11:31:26 -0600 Subject: [PATCH 82/97] Merge branch 'master' into global-find Conflicts: spec/extensions/command-panel-spec.coffee --- spec/extensions/command-panel-spec.coffee | 60 +++++++++++-------- .../command-panel/command-panel.coffee | 9 ++- src/extensions/command-panel/keymap.coffee | 2 +- 3 files changed, 42 insertions(+), 29 deletions(-) diff --git a/spec/extensions/command-panel-spec.coffee b/spec/extensions/command-panel-spec.coffee index 1fae1a3c2..c1c8814f9 100644 --- a/spec/extensions/command-panel-spec.coffee +++ b/spec/extensions/command-panel-spec.coffee @@ -27,28 +27,43 @@ describe "CommandPanel", -> newRootView.remove() - describe "when toggle-command-panel is triggered on the root view", -> - it "toggles the command panel", -> + describe "when command-panel:toggle is triggered on the root view", -> + beforeEach -> rootView.attachToDom() - expect(rootView.find('.command-panel')).not.toExist() - expect(rootView.getActiveEditor().isFocused).toBeTruthy() - expect(commandPanel.miniEditor.isFocused).toBeFalsy() - rootView.trigger 'command-panel:toggle' - expect(rootView.find('.command-panel').view()).toBe commandPanel - expect(commandPanel.miniEditor.isFocused).toBeTruthy() - commandPanel.miniEditor.insertText 's/war/peace/g' + describe "when the command panel is visible", -> + beforeEach -> + commandPanel.attach() - rootView.trigger 'command-panel:toggle' - expect(rootView.find('.command-panel')).not.toExist() - expect(rootView.getActiveEditor().isFocused).toBeTruthy() - expect(commandPanel.miniEditor.isFocused).toBeFalsy() + describe "when the command panel is focused", -> + it "closes the command panel", -> + expect(commandPanel.miniEditor.hiddenInput).toMatchSelector ':focus' + rootView.trigger 'command-panel:toggle' + expect(commandPanel.hasParent()).toBeFalsy() + + describe "when the command panel is not focused", -> + it "focuses the command panel", -> + rootView.focus() + expect(commandPanel.miniEditor.hiddenInput).not.toMatchSelector ':focus' + rootView.trigger 'command-panel:toggle' + expect(commandPanel.hasParent()).toBeTruthy() + expect(commandPanel.miniEditor.hiddenInput).toMatchSelector ':focus' + + describe "when the command panel is not visible", -> + it "shows and focuses the command panel", -> + expect(commandPanel.hasParent()).toBeFalsy() + rootView.trigger 'command-panel:toggle' + expect(commandPanel.hasParent()).toBeTruthy() + + describe "when command-panel:unfocus is triggered on the command panel", -> + it "returns focus to the root view but does not hide the command panel", -> + rootView.attachToDom() + commandPanel.attach() + expect(commandPanel.miniEditor.hiddenInput).toMatchSelector ':focus' + commandPanel.trigger 'command-panel:unfocus' + expect(commandPanel.hasParent()).toBeTruthy() + expect(commandPanel.miniEditor.hiddenInput).not.toMatchSelector ':focus' - rootView.trigger 'command-panel:toggle' - expect(rootView.find('.command-panel').view()).toBe commandPanel - expect(commandPanel.miniEditor.isFocused).toBeTruthy() - expect(commandPanel.miniEditor.getText()).toBe '' - expect(commandPanel.miniEditor.getCursorScreenPosition()).toEqual [0, 0] describe "when command-panel:repeat-relative-address is triggered on the root view", -> it "repeats the last search command if there is one", -> @@ -73,7 +88,7 @@ describe "CommandPanel", -> rootView.trigger 'command-panel:repeat-relative-address' expect(editor.getSelection().getBufferRange()).toEqual [[3,31], [3,38]] - describe "when command-pane:repeat-relative-address-in-reverse is triggered on the root view", -> + describe "when command-panel:repeat-relative-address-in-reverse is triggered on the root view", -> it "it repeats the last relative address in the reverse direction", -> rootView.trigger 'command-panel:repeat-relative-address-in-reverse' @@ -120,13 +135,6 @@ describe "CommandPanel", -> expect(commandPanel.miniEditor.getText()).toBe "Xx/" expect(commandPanel.miniEditor.getCursorBufferPosition()).toEqual [0, 3] - describe "when esc is pressed in the command panel", -> - it "closes the command panel", -> - rootView.trigger 'command-panel:toggle' - expect(rootView.find('.command-panel').view()).toBe commandPanel - commandPanel.miniEditor.hiddenInput.trigger keydownEvent('escape') - expect(rootView.find('.command-panel')).not.toExist() - describe "when return is pressed on the panel's editor", -> describe "if the command has an immediate effect", -> it "executes it immediately on the current buffer", -> diff --git a/src/extensions/command-panel/command-panel.coffee b/src/extensions/command-panel/command-panel.coffee index d3360e497..a0a1a32d3 100644 --- a/src/extensions/command-panel/command-panel.coffee +++ b/src/extensions/command-panel/command-panel.coffee @@ -44,6 +44,7 @@ class CommandPanel extends View @commandInterpreter = new CommandInterpreter(@rootView.project) @history = [] + @on 'command-panel:unfocus', => @rootView.focus() @rootView.on 'command-panel:toggle', => @toggle() @rootView.on 'command-panel:execute', => @execute() @rootView.on 'command-panel:find-in-file', => @attach("/") @@ -57,8 +58,12 @@ class CommandPanel extends View @miniEditor.on 'move-down', => @navigateForwardInHistory() toggle: -> - if @parent().length then @detach() else @attach() - false + if @miniEditor.isFocused + @detach() + @rootView.focus() + else + @attach() unless @hasParent() + @miniEditor.focus() attach: (text='') -> @rootView.append(this) diff --git a/src/extensions/command-panel/keymap.coffee b/src/extensions/command-panel/keymap.coffee index 9e27674a8..438c9117e 100644 --- a/src/extensions/command-panel/keymap.coffee +++ b/src/extensions/command-panel/keymap.coffee @@ -6,7 +6,7 @@ window.keymap.bindKeys '*' window.keymap.bindKeys '.command-panel .editor input', 'meta-w': 'command-panel:toggle' - escape: 'command-panel:toggle' + escape: 'command-panel:unfocus' enter: 'command-panel:execute' window.keymap.bindKeys '.editor', From 318ddd814812cb793c1ee597405951e86875c536 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 23 Jul 2012 12:19:54 -0600 Subject: [PATCH 83/97] Ctrl-2 & ctrl-0 mediate visibility focus of command-panel and the preview list If the command panel is toggled with ctrl-2, then the preview list will show and become focused. If it's toggled with ctrl-0, the preview list won't be shown. But if it's already showing, focus will just switch to the editor, leaving it visible. It's kind of hard to explain but feels intuitive to me to use. --- spec/extensions/command-panel-spec.coffee | 53 ++++++++++++++----- .../command-panel/command-panel.coffee | 13 ++++- src/extensions/command-panel/keymap.coffee | 2 +- .../command-panel/preview-list.coffee | 2 +- 4 files changed, 55 insertions(+), 15 deletions(-) diff --git a/spec/extensions/command-panel-spec.coffee b/spec/extensions/command-panel-spec.coffee index c1c8814f9..7376c8ef3 100644 --- a/spec/extensions/command-panel-spec.coffee +++ b/spec/extensions/command-panel-spec.coffee @@ -64,7 +64,6 @@ describe "CommandPanel", -> expect(commandPanel.hasParent()).toBeTruthy() expect(commandPanel.miniEditor.hiddenInput).not.toMatchSelector ':focus' - describe "when command-panel:repeat-relative-address is triggered on the root view", -> it "repeats the last search command if there is one", -> rootView.trigger 'command-panel:repeat-relative-address' @@ -146,23 +145,53 @@ describe "CommandPanel", -> expect(buffer.lineForRow(1)).toMatch /var torta/ describe "when the command returns operations to be previewed", -> - it "displays a preview of the operations above the mini-editor", -> + beforeEach -> rootView.attachToDom() editor.remove() - rootView.trigger 'command-panel:toggle' - waitsForPromise -> commandPanel.execute('X x/a+/') - runs -> - expect(commandPanel).toBeVisible() - expect(commandPanel.previewList).toBeVisible() - previewItem = commandPanel.previewList.find("li:contains(dir/a):first") - expect(previewItem.find('.path').text()).toBe "dir/a" - expect(previewItem.find('.preview').text()).toBe "aaa bbb" - expect(previewItem.find('.preview > .match').text()).toBe "aaa" + it "displays and focuses the operation preview list", -> + expect(commandPanel).toBeVisible() + expect(commandPanel.previewList).toBeVisible() + expect(commandPanel.previewList).toMatchSelector ':focus' + previewItem = commandPanel.previewList.find("li:contains(dir/a):first") + expect(previewItem.find('.path').text()).toBe "dir/a" + expect(previewItem.find('.preview').text()).toBe "aaa bbb" + expect(previewItem.find('.preview > .match').text()).toBe "aaa" - rootView.trigger 'command-panel:toggle' # ensure we can close panel without problems + rootView.trigger 'command-panel:toggle-preview' # ensure we can close panel without problems + expect(commandPanel).toBeHidden() + + it "shifts focus between the preview and the mini editor on 'toggle' and 'toggle-preview' events", -> + rootView.trigger 'command-panel:toggle' + expect(commandPanel.miniEditor.hiddenInput).toMatchSelector ':focus' + rootView.trigger 'command-panel:toggle-preview' + expect(commandPanel.previewList).toMatchSelector ':focus' + + rootView.trigger 'command-panel:toggle-preview' + expect(commandPanel.hasParent()).toBeFalsy() + + rootView.trigger 'command-panel:toggle' + rootView.trigger 'command-panel:toggle' + expect(commandPanel.hasParent()).toBeFalsy() + expect(commandPanel.previewList).toBeHidden() + + # preview should be hidden if we toggle command panel on normally + rootView.trigger 'command-panel:toggle' + expect(commandPanel.hasParent()).toBeTruthy() + expect(commandPanel.previewList).toBeHidden() + + rootView.trigger 'command-panel:toggle-preview' + expect(commandPanel.previewList).toBeVisible() + + rootView.trigger 'command-panel:toggle-preview' + expect(commandPanel.hasParent()).toBeFalsy() + + # preview should be visible if we toggle-preview the command panel + rootView.trigger 'command-panel:toggle-preview' + expect(commandPanel.hasParent()).toBeTruthy() + expect(commandPanel.previewList).toBeVisible() describe "if the command is malformed", -> it "adds and removes an error class to the command panel and does not close it", -> diff --git a/src/extensions/command-panel/command-panel.coffee b/src/extensions/command-panel/command-panel.coffee index a0a1a32d3..f9bbc626f 100644 --- a/src/extensions/command-panel/command-panel.coffee +++ b/src/extensions/command-panel/command-panel.coffee @@ -46,6 +46,7 @@ class CommandPanel extends View @on 'command-panel:unfocus', => @rootView.focus() @rootView.on 'command-panel:toggle', => @toggle() + @rootView.on 'command-panel:toggle-preview', => @togglePreview() @rootView.on 'command-panel:execute', => @execute() @rootView.on 'command-panel:find-in-file', => @attach("/") @rootView.on 'command-panel:find-in-project', => @attach("Xx/") @@ -65,15 +66,24 @@ class CommandPanel extends View @attach() unless @hasParent() @miniEditor.focus() + togglePreview: -> + if @previewList.is(':focus') + @previewList.hide() + @detach() + @rootView.focus() + else + @attach() unless @hasParent() + @previewList.show().focus() + attach: (text='') -> @rootView.append(this) - @previewList.hide() @miniEditor.focus() @miniEditor.setText(text) @miniEditor.setCursorBufferPosition([0, Infinity]) detach: -> @rootView.focus() + @previewList.hide() if @previewedOperations operation.destroy() for operation in @previewedOperations @previewedOperations = undefined @@ -98,6 +108,7 @@ class CommandPanel extends View populatePreviewList: (operations) -> @previewedOperations = operations @previewList.populate(operations) + @previewList.focus() navigateBackwardInHistory: -> return if @historyIndex == 0 diff --git a/src/extensions/command-panel/keymap.coffee b/src/extensions/command-panel/keymap.coffee index 438c9117e..2200d4936 100644 --- a/src/extensions/command-panel/keymap.coffee +++ b/src/extensions/command-panel/keymap.coffee @@ -1,6 +1,6 @@ window.keymap.bindKeys '*' 'ctrl-0': 'command-panel:toggle' - 'ctrl-meta-0': 'command-panel:toggle-preview' + 'ctrl-2': 'command-panel:toggle-preview' 'meta-:': 'command-panel:toggle' 'meta-F': 'command-panel:find-in-project' diff --git a/src/extensions/command-panel/preview-list.coffee b/src/extensions/command-panel/preview-list.coffee index a10a0982b..a735bd104 100644 --- a/src/extensions/command-panel/preview-list.coffee +++ b/src/extensions/command-panel/preview-list.coffee @@ -3,7 +3,7 @@ module.exports = class PreviewList extends View @content: -> - @ol class: 'preview-list', -> + @ol class: 'preview-list', tabindex: -1, -> selectedOperationIndex: 0 operations: null From 9d938d84ad9ca71e510b17774e0a4a6dc113bc72 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 23 Jul 2012 12:59:06 -0600 Subject: [PATCH 84/97] Test the 'command-panel:focus-preview' event directly This is more tidy, breaking out all the cases into a describe hierarchy. The previous test was kinda tacked on in an odd spot. --- spec/extensions/command-panel-spec.coffee | 112 ++++++++++++------ .../command-panel/command-panel.coffee | 7 +- .../command-panel/preview-list.coffee | 2 +- 3 files changed, 86 insertions(+), 35 deletions(-) diff --git a/spec/extensions/command-panel-spec.coffee b/spec/extensions/command-panel-spec.coffee index 7376c8ef3..2c638fccc 100644 --- a/spec/extensions/command-panel-spec.coffee +++ b/spec/extensions/command-panel-spec.coffee @@ -35,14 +35,14 @@ describe "CommandPanel", -> beforeEach -> commandPanel.attach() - describe "when the command panel is focused", -> + describe "when the mini editor is focused", -> it "closes the command panel", -> expect(commandPanel.miniEditor.hiddenInput).toMatchSelector ':focus' rootView.trigger 'command-panel:toggle' expect(commandPanel.hasParent()).toBeFalsy() - describe "when the command panel is not focused", -> - it "focuses the command panel", -> + describe "when the mini editor is not focused", -> + it "focuses the mini editor", -> rootView.focus() expect(commandPanel.miniEditor.hiddenInput).not.toMatchSelector ':focus' rootView.trigger 'command-panel:toggle' @@ -55,6 +55,82 @@ describe "CommandPanel", -> rootView.trigger 'command-panel:toggle' expect(commandPanel.hasParent()).toBeTruthy() + describe "when command-panel:toggle-preview is triggered on the root view", -> + beforeEach -> + rootView.attachToDom() + + describe "when the preview list is/was previously visible", -> + beforeEach -> + rootView.trigger 'command-panel:toggle' + waitsForPromise -> commandPanel.execute('X x/a+/') + + describe "when the command panel is visible", -> + beforeEach -> + expect(commandPanel.hasParent()).toBeTruthy() + + describe "when the preview list is visible", -> + beforeEach -> + expect(commandPanel.previewList).toBeVisible() + + describe "when the preview list is focused", -> + it "hides the command panel", -> + expect(commandPanel.previewList).toMatchSelector(':focus') + rootView.trigger 'command-panel:toggle-preview' + expect(commandPanel.hasParent()).toBeFalsy() + + describe "when the preview list is not focused", -> + it "focuses the preview list", -> + commandPanel.miniEditor.focus() + rootView.trigger 'command-panel:toggle-preview' + expect(commandPanel.previewList).toMatchSelector(':focus') + + describe "when the preview list is not visible", -> + beforeEach -> + commandPanel.miniEditor.focus() + rootView.trigger 'command-panel:toggle' + rootView.trigger 'command-panel:toggle' + expect(commandPanel.hasParent()).toBeTruthy() + expect(commandPanel.previewList).toBeHidden() + + it "shows and focuses the preview list", -> + rootView.trigger 'command-panel:toggle-preview' + expect(commandPanel.previewList).toBeVisible() + expect(commandPanel.previewList).toMatchSelector(':focus') + + describe "when the command panel is not visible", -> + it "shows the command panel and the preview list, and focuses the preview list", -> + commandPanel.miniEditor.focus() + rootView.trigger 'command-panel:toggle' + expect(commandPanel.hasParent()).toBeFalsy() + + rootView.trigger 'command-panel:toggle-preview' + expect(commandPanel.hasParent()).toBeTruthy() + expect(commandPanel.previewList).toBeVisible() + expect(commandPanel.previewList).toMatchSelector(':focus') + + describe "when the preview list has never been opened", -> + describe "when the command panel is visible", -> + beforeEach -> + rootView.trigger 'command-panel:toggle' + expect(commandPanel.hasParent()).toBeTruthy() + + describe "when the mini editor is focused", -> + it "retains focus on the mini editor and does not show the preview list", -> + expect(commandPanel.miniEditor.isFocused).toBeTruthy() + rootView.trigger 'command-panel:toggle-preview' + expect(commandPanel.previewList).toBeHidden() + expect(commandPanel.miniEditor.isFocused).toBeTruthy() + + describe "when the mini editor is not focused", -> + it "focuses the mini editor and does not show the preview list", -> + rootView.focus() + rootView.trigger 'command-panel:toggle-preview' + expect(commandPanel.previewList).toBeHidden() + expect(commandPanel.miniEditor.isFocused).toBeTruthy() + + describe "when the command panel is not visible", -> + it "shows the command panel and focuses the mini editor, but does not show the preview list", -> + describe "when command-panel:unfocus is triggered on the command panel", -> it "returns focus to the root view but does not hide the command panel", -> rootView.attachToDom() @@ -163,36 +239,6 @@ describe "CommandPanel", -> rootView.trigger 'command-panel:toggle-preview' # ensure we can close panel without problems expect(commandPanel).toBeHidden() - it "shifts focus between the preview and the mini editor on 'toggle' and 'toggle-preview' events", -> - rootView.trigger 'command-panel:toggle' - expect(commandPanel.miniEditor.hiddenInput).toMatchSelector ':focus' - rootView.trigger 'command-panel:toggle-preview' - expect(commandPanel.previewList).toMatchSelector ':focus' - - rootView.trigger 'command-panel:toggle-preview' - expect(commandPanel.hasParent()).toBeFalsy() - - rootView.trigger 'command-panel:toggle' - rootView.trigger 'command-panel:toggle' - expect(commandPanel.hasParent()).toBeFalsy() - expect(commandPanel.previewList).toBeHidden() - - # preview should be hidden if we toggle command panel on normally - rootView.trigger 'command-panel:toggle' - expect(commandPanel.hasParent()).toBeTruthy() - expect(commandPanel.previewList).toBeHidden() - - rootView.trigger 'command-panel:toggle-preview' - expect(commandPanel.previewList).toBeVisible() - - rootView.trigger 'command-panel:toggle-preview' - expect(commandPanel.hasParent()).toBeFalsy() - - # preview should be visible if we toggle-preview the command panel - rootView.trigger 'command-panel:toggle-preview' - expect(commandPanel.hasParent()).toBeTruthy() - expect(commandPanel.previewList).toBeVisible() - describe "if the command is malformed", -> it "adds and removes an error class to the command panel and does not close it", -> rootView.trigger 'command-panel:toggle' diff --git a/src/extensions/command-panel/command-panel.coffee b/src/extensions/command-panel/command-panel.coffee index f9bbc626f..5fc135892 100644 --- a/src/extensions/command-panel/command-panel.coffee +++ b/src/extensions/command-panel/command-panel.coffee @@ -58,6 +58,8 @@ class CommandPanel extends View @miniEditor.on 'move-up', => @navigateBackwardInHistory() @miniEditor.on 'move-down', => @navigateForwardInHistory() + @previewList.hide() + toggle: -> if @miniEditor.isFocused @detach() @@ -73,7 +75,10 @@ class CommandPanel extends View @rootView.focus() else @attach() unless @hasParent() - @previewList.show().focus() + if @previewList.hasOperations() + @previewList.show().focus() + else + @miniEditor.focus() attach: (text='') -> @rootView.append(this) diff --git a/src/extensions/command-panel/preview-list.coffee b/src/extensions/command-panel/preview-list.coffee index a735bd104..cdfed41ea 100644 --- a/src/extensions/command-panel/preview-list.coffee +++ b/src/extensions/command-panel/preview-list.coffee @@ -8,7 +8,7 @@ class PreviewList extends View selectedOperationIndex: 0 operations: null - initialize: -> + hasOperations: -> @operations? populate: (@operations) -> @empty() From c1c055319cd5aa0fc34ca8a3f4ab3c3e1d23cd2b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 23 Jul 2012 13:17:42 -0600 Subject: [PATCH 85/97] Remove redundant `describe` block This is now tested via pressing enter on the mini-editor --- spec/extensions/command-panel-spec.coffee | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spec/extensions/command-panel-spec.coffee b/spec/extensions/command-panel-spec.coffee index 2c638fccc..68bbbe08c 100644 --- a/spec/extensions/command-panel-spec.coffee +++ b/spec/extensions/command-panel-spec.coffee @@ -279,3 +279,11 @@ describe "CommandPanel", -> commandPanel.execute() expect(rootView.getActiveEditor().getText()).toBe "i love love" expect(rootView.find('.command-panel')).not.toExist() + + describe "when the preview list is focused", -> + beforeEach -> + rootView.trigger 'command-panel:toggle' + waitsForPromise -> commandPanel.execute('X x/a+/') + + describe "when move-down and move-up are triggered on the preview list", -> + it "selects the next/previous operation", -> From 8f0c8633f81ad4e9de34020158249a1a190f5242 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 23 Jul 2012 15:01:03 -0600 Subject: [PATCH 86/97] Move-up / move-down on command panel change the selected operation --- spec/extensions/command-panel-spec.coffee | 46 ++++++++++++++----- .../command-panel/preview-list.coffee | 37 +++++++++++++-- static/command-panel.css | 15 +++--- 3 files changed, 75 insertions(+), 23 deletions(-) diff --git a/spec/extensions/command-panel-spec.coffee b/spec/extensions/command-panel-spec.coffee index 68bbbe08c..9be916c5f 100644 --- a/spec/extensions/command-panel-spec.coffee +++ b/spec/extensions/command-panel-spec.coffee @@ -1,5 +1,6 @@ RootView = require 'root-view' CommandPanel = require 'command-panel' +_ = require 'underscore' describe "CommandPanel", -> [rootView, editor, buffer, commandPanel] = [] @@ -270,20 +271,43 @@ describe "CommandPanel", -> commandPanel.miniEditor.trigger 'move-down' expect(commandPanel.miniEditor.getText()).toBe '' - describe ".execute()", -> - it "executes the command and closes the command panel", -> - rootView.getActiveEditor().setText("i hate love") - rootView.getActiveEditor().getSelection().setBufferRange [[0,0], [0,Infinity]] - rootView.trigger 'command-panel:toggle' - commandPanel.miniEditor.insertText 's/hate/love/' - commandPanel.execute() - expect(rootView.getActiveEditor().getText()).toBe "i love love" - expect(rootView.find('.command-panel')).not.toExist() - describe "when the preview list is focused", -> + previewList = null + beforeEach -> + previewList = commandPanel.previewList rootView.trigger 'command-panel:toggle' waitsForPromise -> commandPanel.execute('X x/a+/') describe "when move-down and move-up are triggered on the preview list", -> - it "selects the next/previous operation", -> + it "selects the next/previous operation (if there is one), and scrolls the list if needed", -> + rootView.attachToDom() + expect(previewList.find('li:eq(0)')).toHaveClass 'selected' + expect(previewList.getSelectedOperation()).toBe previewList.getOperations()[0] + + previewList.trigger 'move-up' + expect(previewList.find('li:eq(0)')).toHaveClass 'selected' + expect(previewList.getSelectedOperation()).toBe previewList.getOperations()[0] + + previewList.trigger 'move-down' + expect(previewList.find('li:eq(1)')).toHaveClass 'selected' + expect(previewList.getSelectedOperation()).toBe previewList.getOperations()[1] + + previewList.trigger 'move-down' + expect(previewList.find('li:eq(2)')).toHaveClass 'selected' + expect(previewList.getSelectedOperation()).toBe previewList.getOperations()[2] + + previewList.trigger 'move-up' + expect(previewList.find('li:eq(1)')).toHaveClass 'selected' + expect(previewList.getSelectedOperation()).toBe previewList.getOperations()[1] + + _.times previewList.getOperations().length, -> previewList.trigger 'move-down' + + expect(previewList.find('li:last')).toHaveClass 'selected' + expect(previewList.getSelectedOperation()).toBe _.last(previewList.getOperations()) + + expect(previewList.scrollBottom()).toBe previewList.prop('scrollHeight') + + _.times previewList.getOperations().length, -> previewList.trigger 'move-up' + + console.log previewList.find('li:first').position().top diff --git a/src/extensions/command-panel/preview-list.coffee b/src/extensions/command-panel/preview-list.coffee index cdfed41ea..1a91cc667 100644 --- a/src/extensions/command-panel/preview-list.coffee +++ b/src/extensions/command-panel/preview-list.coffee @@ -8,6 +8,10 @@ class PreviewList extends View selectedOperationIndex: 0 operations: null + initialize: -> + @on 'move-down', => @selectNextOperation() + @on 'move-up', => @selectPreviousOperation() + hasOperations: -> @operations? populate: (@operations) -> @@ -26,9 +30,32 @@ class PreviewList extends View @show() - setSelectedOperationIndex: (index) -> - @children(".selected").removeClass('selected') - @children("li:eq(#{index})").addClass('selected') + selectNextOperation: -> + @setSelectedOperationIndex(@selectedOperationIndex + 1) - #getSelectedOperation: -> - #@operations[@selectedOperationIndex] + selectPreviousOperation: -> + @setSelectedOperationIndex(@selectedOperationIndex - 1) + + setSelectedOperationIndex: (index) -> + index = Math.max(0, index) + index = Math.min(@operations.length - 1, index) + @children(".selected").removeClass('selected') + element = @children("li:eq(#{index})") + element.addClass('selected') + @scrollToElement(element) + @selectedOperationIndex = index + + getOperations: -> + new Array(@operations...) + + getSelectedOperation: -> + @operations[@selectedOperationIndex] + + scrollToElement: (element) -> + top = @scrollTop() + element.position().top + bottom = top + element.outerHeight() + + if bottom > @scrollBottom() + @scrollBottom(bottom) + if top < @scrollTop() + @scrollTop(top) diff --git a/static/command-panel.css b/static/command-panel.css index 0bc89281f..e2c0dcc9f 100644 --- a/static/command-panel.css +++ b/static/command-panel.css @@ -9,10 +9,11 @@ .command-panel .preview-list { max-height: 300px; overflow: auto; - padding-bottom: 3px; + margin-bottom: 3px; + position: relative; } -.command-panel .preview-list .path{ +.command-panel .preview-list .path { padding-left: 3px; color: #00ffff; } @@ -22,11 +23,15 @@ color: #f6f3e8; } -.command-panel .preview-list .preview .match{ +.command-panel .preview-list .preview .match { background-color: #8E8A8A; padding: 1px; } +.command-panel .preview-list li.selected { + background-color: green; +} + .command-panel .prompt-and-editor { display: -webkit-box; } @@ -36,7 +41,3 @@ font-weight: bold; padding: .2em; } - -.command-panel .preview-list li.selected { - background-color: green; -} \ No newline at end of file From c3c4e07a3ffc33d9eb2cd2d089b19d7b0d4ea91a Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 23 Jul 2012 18:16:37 -0600 Subject: [PATCH 87/97] Pressing 'enter' on a global search operation selects it in the active editor --- spec/extensions/command-panel-spec.coffee | 21 +++++++++++++++++-- .../command-panel/command-panel.coffee | 4 ++-- src/extensions/command-panel/keymap.coffee | 2 +- .../command-panel/preview-list.coffee | 9 +++++++- 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/spec/extensions/command-panel-spec.coffee b/spec/extensions/command-panel-spec.coffee index 9be916c5f..e5450e544 100644 --- a/spec/extensions/command-panel-spec.coffee +++ b/spec/extensions/command-panel-spec.coffee @@ -3,12 +3,13 @@ CommandPanel = require 'command-panel' _ = require 'underscore' describe "CommandPanel", -> - [rootView, editor, buffer, commandPanel] = [] + [rootView, editor, buffer, commandPanel, project] = [] beforeEach -> rootView = new RootView rootView.open(require.resolve 'fixtures/sample.js') rootView.enableKeymap() + project = rootView.project editor = rootView.getActiveEditor() buffer = editor.activeEditSession.buffer commandPanel = requireExtension('command-panel') @@ -271,7 +272,7 @@ describe "CommandPanel", -> commandPanel.miniEditor.trigger 'move-down' expect(commandPanel.miniEditor.getText()).toBe '' - describe "when the preview list is focused", -> + describe "when the preview list is focused with search operations", -> previewList = null beforeEach -> @@ -311,3 +312,19 @@ describe "CommandPanel", -> _.times previewList.getOperations().length, -> previewList.trigger 'move-up' console.log previewList.find('li:first').position().top + + describe "when command-panel:execute is triggered on the preview list", -> + it "opens a new editor with the operation's buffer and selects the search result", -> + executeHandler = jasmine.createSpy('executeHandler') + commandPanel.on 'command-panel:execute', executeHandler + + _.times 4, -> previewList.trigger 'move-down' + operation = previewList.getSelectedOperation() + + previewList.trigger 'command-panel:execute' + + editSession = rootView.getActiveEditSession() + expect(editSession.buffer.getPath()).toBe project.resolve(operation.getPath()) + expect(editSession.getSelectedBufferRange()).toEqual operation.getBufferRange() + + expect(executeHandler).not.toHaveBeenCalled() diff --git a/src/extensions/command-panel/command-panel.coffee b/src/extensions/command-panel/command-panel.coffee index 5fc135892..b8054f374 100644 --- a/src/extensions/command-panel/command-panel.coffee +++ b/src/extensions/command-panel/command-panel.coffee @@ -29,9 +29,9 @@ class CommandPanel extends View commandPanel.attach(state.text) if state.visible commandPanel - @content: -> + @content: (rootView) -> @div class: 'command-panel', => - @subview 'previewList', new PreviewList() + @subview 'previewList', new PreviewList(rootView) @div class: 'prompt-and-editor', => @div ':', class: 'prompt', outlet: 'prompt' @subview 'miniEditor', new Editor(mini: true) diff --git a/src/extensions/command-panel/keymap.coffee b/src/extensions/command-panel/keymap.coffee index 2200d4936..7235f6ebb 100644 --- a/src/extensions/command-panel/keymap.coffee +++ b/src/extensions/command-panel/keymap.coffee @@ -4,7 +4,7 @@ window.keymap.bindKeys '*' 'meta-:': 'command-panel:toggle' 'meta-F': 'command-panel:find-in-project' -window.keymap.bindKeys '.command-panel .editor input', +window.keymap.bindKeys '.command-panel, .command-panel .editor input', 'meta-w': 'command-panel:toggle' escape: 'command-panel:unfocus' enter: 'command-panel:execute' diff --git a/src/extensions/command-panel/preview-list.coffee b/src/extensions/command-panel/preview-list.coffee index 1a91cc667..5b3c8f9d5 100644 --- a/src/extensions/command-panel/preview-list.coffee +++ b/src/extensions/command-panel/preview-list.coffee @@ -8,9 +8,10 @@ class PreviewList extends View selectedOperationIndex: 0 operations: null - initialize: -> + initialize: (@rootView) -> @on 'move-down', => @selectNextOperation() @on 'move-up', => @selectPreviousOperation() + @on 'command-panel:execute', => @executeSelectedOperation() hasOperations: -> @operations? @@ -45,6 +46,12 @@ class PreviewList extends View @scrollToElement(element) @selectedOperationIndex = index + executeSelectedOperation: -> + operation = @getSelectedOperation() + editSession = @rootView.open(operation.getPath()) + operation.execute(editSession) + false + getOperations: -> new Array(@operations...) From 5659f9bab1173b0b3957fd5cd33faaa4c4a1b131 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 23 Jul 2012 19:05:37 -0600 Subject: [PATCH 88/97] Destroy preview list's previous operations when populating new ones --- spec/extensions/command-panel-spec.coffee | 4 ++++ src/extensions/command-panel/preview-list.coffee | 9 +++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/spec/extensions/command-panel-spec.coffee b/spec/extensions/command-panel-spec.coffee index e5450e544..e66da536a 100644 --- a/spec/extensions/command-panel-spec.coffee +++ b/spec/extensions/command-panel-spec.coffee @@ -241,6 +241,10 @@ describe "CommandPanel", -> rootView.trigger 'command-panel:toggle-preview' # ensure we can close panel without problems expect(commandPanel).toBeHidden() + it "destroys previously previewed operations if there are any", -> + waitsForPromise -> commandPanel.execute('X x/b+/') + # there shouldn't be any dangling operations after this + describe "if the command is malformed", -> it "adds and removes an error class to the command panel and does not close it", -> rootView.trigger 'command-panel:toggle' diff --git a/src/extensions/command-panel/preview-list.coffee b/src/extensions/command-panel/preview-list.coffee index 5b3c8f9d5..c52bd3239 100644 --- a/src/extensions/command-panel/preview-list.coffee +++ b/src/extensions/command-panel/preview-list.coffee @@ -15,7 +15,9 @@ class PreviewList extends View hasOperations: -> @operations? - populate: (@operations) -> + populate: (operations) -> + @destroyOperations() if @operations + @operations = operations @empty() @html $$$ -> for operation in operations @@ -28,7 +30,6 @@ class PreviewList extends View @span suffix @setSelectedOperationIndex(0) - @show() selectNextOperation: -> @@ -55,6 +56,10 @@ class PreviewList extends View getOperations: -> new Array(@operations...) + destroyOperations: -> + operation.destroy() for operation in @getOperations() + @operations = null + getSelectedOperation: -> @operations[@selectedOperationIndex] From 94251a2fad79594533a5627506ea6af95d4e311d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 23 Jul 2012 19:10:07 -0600 Subject: [PATCH 89/97] Only store operations to preview on the PreviewList --- .../command-panel/command-panel.coffee | 16 ++++++---------- src/extensions/command-panel/preview-list.coffee | 3 +++ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/extensions/command-panel/command-panel.coffee b/src/extensions/command-panel/command-panel.coffee index b8054f374..452199d2b 100644 --- a/src/extensions/command-panel/command-panel.coffee +++ b/src/extensions/command-panel/command-panel.coffee @@ -18,7 +18,7 @@ class CommandPanel extends View @instance = new CommandPanel(rootView) @deactivate: -> - @instance.detach() + @instance.destroy() @serialize: -> text: @instance.miniEditor.getText() @@ -60,6 +60,9 @@ class CommandPanel extends View @previewList.hide() + destroy: -> + @previewList.destroy() + toggle: -> if @miniEditor.isFocused @detach() @@ -89,9 +92,6 @@ class CommandPanel extends View detach: -> @rootView.focus() @previewList.hide() - if @previewedOperations - operation.destroy() for operation in @previewedOperations - @previewedOperations = undefined super execute: (command = @miniEditor.getText()) -> @@ -100,7 +100,8 @@ class CommandPanel extends View @history.push(command) @historyIndex = @history.length if operationsToPreview?.length - @populatePreviewList(operationsToPreview) + @previewList.populate(operationsToPreview) + @previewList.focus() else @detach() catch error @@ -110,11 +111,6 @@ class CommandPanel extends View else throw error - populatePreviewList: (operations) -> - @previewedOperations = operations - @previewList.populate(operations) - @previewList.focus() - navigateBackwardInHistory: -> return if @historyIndex == 0 @historyIndex-- diff --git a/src/extensions/command-panel/preview-list.coffee b/src/extensions/command-panel/preview-list.coffee index c52bd3239..573ca794b 100644 --- a/src/extensions/command-panel/preview-list.coffee +++ b/src/extensions/command-panel/preview-list.coffee @@ -13,6 +13,9 @@ class PreviewList extends View @on 'move-up', => @selectPreviousOperation() @on 'command-panel:execute', => @executeSelectedOperation() + destroy: -> + @destroyOperations() if @operations + hasOperations: -> @operations? populate: (operations) -> From a2522f9b19c8840cbaab3bb2c6196729f330f227 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 23 Jul 2012 19:25:29 -0600 Subject: [PATCH 90/97] Clicking on previewed operations selects their buffer range in active editor --- spec/extensions/command-panel-spec.coffee | 10 ++++++++++ src/extensions/command-panel/preview-list.coffee | 14 +++++++++----- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/spec/extensions/command-panel-spec.coffee b/spec/extensions/command-panel-spec.coffee index e66da536a..15a1783c5 100644 --- a/spec/extensions/command-panel-spec.coffee +++ b/spec/extensions/command-panel-spec.coffee @@ -332,3 +332,13 @@ describe "CommandPanel", -> expect(editSession.getSelectedBufferRange()).toEqual operation.getBufferRange() expect(executeHandler).not.toHaveBeenCalled() + + describe "when an operation in the preview list is clicked", -> + it "opens a new editor with the operation's buffer and selects the search result", -> + operation = previewList.getOperations()[4] + + previewList.find('li:eq(4)').mousedown() + + editSession = rootView.getActiveEditSession() + expect(editSession.buffer.getPath()).toBe project.resolve(operation.getPath()) + expect(editSession.getSelectedBufferRange()).toEqual operation.getBufferRange() diff --git a/src/extensions/command-panel/preview-list.coffee b/src/extensions/command-panel/preview-list.coffee index 573ca794b..a8b471f8b 100644 --- a/src/extensions/command-panel/preview-list.coffee +++ b/src/extensions/command-panel/preview-list.coffee @@ -1,3 +1,4 @@ +$ = require 'jquery' {$$$, View} = require 'space-pen' module.exports = @@ -11,7 +12,11 @@ class PreviewList extends View initialize: (@rootView) -> @on 'move-down', => @selectNextOperation() @on 'move-up', => @selectPreviousOperation() - @on 'command-panel:execute', => @executeSelectedOperation() + @on 'command-panel:execute', => @execute() + + @on 'mousedown', 'li', (e) => + index = $(e.target).data('index') + @execute(@getOperations()[index]) destroy: -> @destroyOperations() if @operations @@ -23,9 +28,9 @@ class PreviewList extends View @operations = operations @empty() @html $$$ -> - for operation in operations + for operation, index in operations {prefix, suffix, match} = operation.preview() - @li => + @li 'data-index': index, => @span operation.getPath(), outlet: "path", class: "path" @span outlet: "preview", class: "preview", => @span prefix @@ -50,8 +55,7 @@ class PreviewList extends View @scrollToElement(element) @selectedOperationIndex = index - executeSelectedOperation: -> - operation = @getSelectedOperation() + execute: (operation = @getSelectedOperation()) -> editSession = @rootView.open(operation.getPath()) operation.execute(editSession) false From da7e1b050932e3765bf0f1e5a25d18d8e11ef68b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 23 Jul 2012 19:32:18 -0600 Subject: [PATCH 91/97] Meta-w on the preview list closes the command panel, not the entire window --- spec/extensions/command-panel-spec.coffee | 7 +++++++ src/extensions/command-panel/command-panel.coffee | 1 + src/extensions/command-panel/keymap.coffee | 4 ++-- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/spec/extensions/command-panel-spec.coffee b/spec/extensions/command-panel-spec.coffee index 15a1783c5..739c1d974 100644 --- a/spec/extensions/command-panel-spec.coffee +++ b/spec/extensions/command-panel-spec.coffee @@ -29,6 +29,13 @@ describe "CommandPanel", -> newRootView.remove() + + describe "when command-panel:close is triggered on the command panel", -> + it "detaches the command panel", -> + commandPanel.attach() + commandPanel.trigger('command-panel:close') + expect(commandPanel.hasParent()).toBeFalsy() + describe "when command-panel:toggle is triggered on the root view", -> beforeEach -> rootView.attachToDom() diff --git a/src/extensions/command-panel/command-panel.coffee b/src/extensions/command-panel/command-panel.coffee index 452199d2b..9bf17f96a 100644 --- a/src/extensions/command-panel/command-panel.coffee +++ b/src/extensions/command-panel/command-panel.coffee @@ -45,6 +45,7 @@ class CommandPanel extends View @history = [] @on 'command-panel:unfocus', => @rootView.focus() + @on 'command-panel:close', => @detach() @rootView.on 'command-panel:toggle', => @toggle() @rootView.on 'command-panel:toggle-preview', => @togglePreview() @rootView.on 'command-panel:execute', => @execute() diff --git a/src/extensions/command-panel/keymap.coffee b/src/extensions/command-panel/keymap.coffee index 7235f6ebb..8b515ddcb 100644 --- a/src/extensions/command-panel/keymap.coffee +++ b/src/extensions/command-panel/keymap.coffee @@ -4,8 +4,8 @@ window.keymap.bindKeys '*' 'meta-:': 'command-panel:toggle' 'meta-F': 'command-panel:find-in-project' -window.keymap.bindKeys '.command-panel, .command-panel .editor input', - 'meta-w': 'command-panel:toggle' +window.keymap.bindKeys '.command-panel .preview-list, .command-panel .editor input', + 'meta-w': 'command-panel:close' escape: 'command-panel:unfocus' enter: 'command-panel:execute' From 07cd01571efd3b8f5c275f9255390302898d6a9f Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 23 Jul 2012 20:02:32 -0600 Subject: [PATCH 92/97] Apply some more styling, for better or worse :-) --- static/command-panel.css | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/static/command-panel.css b/static/command-panel.css index e2c0dcc9f..1f2aee7e2 100644 --- a/static/command-panel.css +++ b/static/command-panel.css @@ -11,25 +11,30 @@ overflow: auto; margin-bottom: 3px; position: relative; + background: #161616; } .command-panel .preview-list .path { padding-left: 3px; - color: #00ffff; + color: #f9ee98; + margin-right: 1ex; } .command-panel .preview-list .preview { - padding-left: 1em; color: #f6f3e8; } .command-panel .preview-list .preview .match { - background-color: #8E8A8A; + background-color: rgba(255,255,255,.25); padding: 1px; } .command-panel .preview-list li.selected { - background-color: green; + background: #444; +} + +.command-panel .preview-list:focus li.selected { + background: #223555; } .command-panel .prompt-and-editor { From c493272be132bae5982c39d70ba4872301523c81 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 23 Jul 2012 20:12:06 -0600 Subject: [PATCH 93/97] Fix click behavior when the event target is a child of the li instead of the li --- spec/extensions/command-panel-spec.coffee | 3 ++- src/extensions/command-panel/preview-list.coffee | 9 +++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/spec/extensions/command-panel-spec.coffee b/spec/extensions/command-panel-spec.coffee index 739c1d974..d89cb2f95 100644 --- a/spec/extensions/command-panel-spec.coffee +++ b/spec/extensions/command-panel-spec.coffee @@ -344,8 +344,9 @@ describe "CommandPanel", -> it "opens a new editor with the operation's buffer and selects the search result", -> operation = previewList.getOperations()[4] - previewList.find('li:eq(4)').mousedown() + previewList.find('li:eq(4) span').mousedown() + expect(previewList.getSelectedOperation()).toBe operation editSession = rootView.getActiveEditSession() expect(editSession.buffer.getPath()).toBe project.resolve(operation.getPath()) expect(editSession.getSelectedBufferRange()).toEqual operation.getBufferRange() diff --git a/src/extensions/command-panel/preview-list.coffee b/src/extensions/command-panel/preview-list.coffee index a8b471f8b..31d338f3f 100644 --- a/src/extensions/command-panel/preview-list.coffee +++ b/src/extensions/command-panel/preview-list.coffee @@ -12,11 +12,11 @@ class PreviewList extends View initialize: (@rootView) -> @on 'move-down', => @selectNextOperation() @on 'move-up', => @selectPreviousOperation() - @on 'command-panel:execute', => @execute() + @on 'command-panel:execute', => @executeSelectedOperation() @on 'mousedown', 'li', (e) => - index = $(e.target).data('index') - @execute(@getOperations()[index]) + @setSelectedOperationIndex(parseInt($(e.target).closest('li').data('index'))) + @executeSelectedOperation() destroy: -> @destroyOperations() if @operations @@ -55,7 +55,8 @@ class PreviewList extends View @scrollToElement(element) @selectedOperationIndex = index - execute: (operation = @getSelectedOperation()) -> + executeSelectedOperation: -> + operation = @getSelectedOperation() editSession = @rootView.open(operation.getPath()) operation.execute(editSession) false From 5ebcabb813a4a38a6eba595c55ce0a10f24bda43 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 23 Jul 2012 20:14:15 -0600 Subject: [PATCH 94/97] Fix preview list cursor styling --- static/command-panel.css | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/static/command-panel.css b/static/command-panel.css index 1f2aee7e2..cfb7f835e 100644 --- a/static/command-panel.css +++ b/static/command-panel.css @@ -14,6 +14,18 @@ background: #161616; } +.command-panel .preview-list { + cursor: default; +} + +.command-panel .preview-list li.selected { + background: #444; +} + +.command-panel .preview-list:focus li.selected { + background: #223555; +} + .command-panel .preview-list .path { padding-left: 3px; color: #f9ee98; @@ -29,14 +41,6 @@ padding: 1px; } -.command-panel .preview-list li.selected { - background: #444; -} - -.command-panel .preview-list:focus li.selected { - background: #223555; -} - .command-panel .prompt-and-editor { display: -webkit-box; } From 66f80c2dd8c0fc516850a04804da4c61b762dd51 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 24 Jul 2012 10:07:07 -0600 Subject: [PATCH 95/97] Backfill coverage for handling of breaks in Project.scan subprocess output If the stdout callback is fired with partial data, and it ends right after a path, we need to ensure we don't handle it as if there's an extra newline. --- spec/app/project-spec.coffee | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/spec/app/project-spec.coffee b/spec/app/project-spec.coffee index a8a50a0cc..bea202ef1 100644 --- a/spec/app/project-spec.coffee +++ b/spec/app/project-spec.coffee @@ -153,3 +153,23 @@ describe "Project", -> expect(paths[2]).toMatch /goddam\nnewlines$/m expect(paths[3]).toMatch /quote".txt$/m expect(fs.base(paths[4])).toBe "utfa\u0306.md" + + it "handles breaks in the search subprocess's output following the filename", -> + spyOn $native, 'exec' + + iterator = jasmine.createSpy('iterator') + project.scan /a+/, iterator + + stdout = $native.exec.argsForCall[0][1].stdout + stdout ":#{require.resolve('fixtures/dir/a')}\n" + stdout "1;0 3:aaa bbb\n2;3 2:cc aa cc\n" + + expect(iterator.argsForCall[0][0]).toEqual + path: project.resolve('a') + match: 'aaa' + range: [[0, 0], [0, 3]] + + expect(iterator.argsForCall[1][0]).toEqual + path: project.resolve('a') + match: 'aa' + range: [[1, 3], [1, 5]] From 454557b50270bfed94333d9afaa4ed79d612a04c Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 24 Jul 2012 17:44:24 -0600 Subject: [PATCH 96/97] Merge branch 'master' into global-find Conflicts: spec/extensions/command-panel-spec.coffee src/app/buffer.coffee src/extensions/command-panel/command-panel.coffee src/extensions/command-panel/keymap.coffee --- .atom/atom.coffee | 4 +- Atom/src/PathWatcher.h | 2 +- Atom/src/PathWatcher.mm | 48 ++++++++--- Atom/src/native_handler.mm | 12 +-- Rakefile | 3 +- docs/flexbox-test-old.html | 84 +++++++++++++++++++ spec/app/buffer-spec.coffee | 50 +++++++++-- spec/app/file-spec.coffee | 66 +++++++++++++-- spec/extensions/command-panel-spec.coffee | 1 - spec/extensions/tree-view-spec.coffee | 15 ++-- src/app/buffer.coffee | 7 ++ src/app/directory.coffee | 4 +- src/app/file.coffee | 16 +++- src/app/root-view.coffee | 3 +- .../command-panel/command-panel.coffee | 2 +- static/atom.css | 31 ++++--- static/command-panel.css | 2 - static/editor.css | 11 ++- 18 files changed, 293 insertions(+), 68 deletions(-) create mode 100644 docs/flexbox-test-old.html diff --git a/.atom/atom.coffee b/.atom/atom.coffee index 9571718da..462e010ec 100644 --- a/.atom/atom.coffee +++ b/.atom/atom.coffee @@ -5,6 +5,4 @@ requireExtension 'tree-view' requireExtension 'command-panel' requireExtension 'keybindings-view' requireExtension 'snippets' - -# status-bar is a bit broken until webkit gets a decent flexbox implementation -# requireExtension 'status-bar' +requireExtension 'status-bar' diff --git a/Atom/src/PathWatcher.h b/Atom/src/PathWatcher.h index a26d33916..88e98693b 100644 --- a/Atom/src/PathWatcher.h +++ b/Atom/src/PathWatcher.h @@ -2,7 +2,7 @@ #import "include/cef_v8.h" #import -typedef void (^WatchCallback)(NSArray *); +typedef void (^WatchCallback)(NSString *, NSString *); @interface PathWatcher : NSObject { int _kq; diff --git a/Atom/src/PathWatcher.mm b/Atom/src/PathWatcher.mm index ff5d45938..3b1db1f13 100644 --- a/Atom/src/PathWatcher.mm +++ b/Atom/src/PathWatcher.mm @@ -2,6 +2,7 @@ #import #import +#import #import static NSMutableArray *gPathWatchers; @@ -11,6 +12,8 @@ static NSMutableArray *gPathWatchers; - (void)watchFileDescriptor:(int)fd; - (NSString *)watchPath:(NSString *)path callback:(WatchCallback)callback callbackId:(NSString *)callbackId; - (void)stopWatching; +- (void)reassignFileDescriptorFor:(NSString *)path to:(NSString *)newPath; +- (bool)isAtomicWrite:(struct kevent)event; @end @implementation PathWatcher @@ -125,8 +128,10 @@ static NSMutableArray *gPathWatchers; if (!fdNumber) { NSString *message = [NSString stringWithFormat:@"Trying to unwatch %@, which we aren't watching", path]; NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:message, NSLocalizedDescriptionKey, nil]; - NSError *e = [NSError errorWithDomain:@"PathWatcher" code:0 userInfo:userInfo]; - *error = e; + if (error) { + NSError *e = [NSError errorWithDomain:@"PathWatcher" code:0 userInfo:userInfo]; + *error = e; + } return; } @@ -162,7 +167,7 @@ static NSMutableArray *gPathWatchers; struct kevent event; int filter = EVFILT_VNODE; int flags = EV_ADD | EV_ENABLE | EV_CLEAR; - int filterFlags = NOTE_WRITE | NOTE_DELETE | NOTE_ATTRIB | NOTE_EXTEND | NOTE_RENAME | NOTE_REVOKE; + int filterFlags = NOTE_WRITE | NOTE_DELETE | NOTE_RENAME; EV_SET(&event, fd, filter, flags, filterFlags, 0, 0); kevent(_kq, &event, 1, NULL, 0, &timeout); } @@ -193,33 +198,44 @@ static NSMutableArray *gPathWatchers; } NSNumber *fdNumber = [NSNumber numberWithInt:event.ident]; - NSMutableArray *eventFlags = [NSMutableArray array]; + NSString *eventFlag = nil; + NSString *path = [self pathForFileDescriptor:fdNumber]; if (event.fflags & NOTE_WRITE) { - [eventFlags addObject:@"modified"]; + eventFlag = @"contents-change"; } else if ([self isAtomicWrite:event]) { + eventFlag = @"contents-change"; + // The fd for the path has changed. Remove references to old fd and // make sure the path and callbacks are linked with new fd. @synchronized(self) { - NSDictionary *callbacks = [NSDictionary dictionaryWithDictionary:[_callbacksByFileDescriptor objectForKey:fdNumber]]; - NSString *path = [self pathForFileDescriptor:fdNumber]; - + NSDictionary *callbacks = [NSDictionary dictionaryWithDictionary:[_callbacksByFileDescriptor objectForKey:fdNumber]]; [self unwatchPath:path callbackId:nil error:nil]; for (NSString *callbackId in [callbacks allKeys]) { [self watchPath:path callback:[callbacks objectForKey:callbackId] callbackId:callbackId]; - } - - [eventFlags addObject:@"modified"]; + } } } + else if (event.fflags & NOTE_DELETE) { + eventFlag = @"remove"; + } + else if (event.fflags & NOTE_RENAME) { + eventFlag = @"move"; + + char pathBuffer[MAXPATHLEN]; + fcntl((int)event.ident, F_GETPATH, &pathBuffer); + NSString *newPath = [NSString stringWithUTF8String:pathBuffer]; + [self reassignFileDescriptorFor:path to:newPath]; + path = newPath; + } @synchronized(self) { NSDictionary *callbacks = [_callbacksByFileDescriptor objectForKey:fdNumber]; for (NSString *key in callbacks) { WatchCallback callback = [callbacks objectForKey:key]; dispatch_async(dispatch_get_main_queue(), ^{ - callback(eventFlags); + callback(eventFlag, path); }); } } @@ -241,4 +257,12 @@ static NSMutableArray *gPathWatchers; return NO; } +- (void)reassignFileDescriptorFor:(NSString *)path to:(NSString *)newPath { + @synchronized(self) { + NSNumber *fdNumber = [_fileDescriptorsByPath objectForKey:path]; + [_fileDescriptorsByPath removeObjectForKey:path]; + [_fileDescriptorsByPath setObject:fdNumber forKey:newPath]; + } +} + @end diff --git a/Atom/src/native_handler.mm b/Atom/src/native_handler.mm index 74ebe18d6..e900e37b7 100644 --- a/Atom/src/native_handler.mm +++ b/Atom/src/native_handler.mm @@ -319,19 +319,15 @@ bool NativeHandler::Execute(const CefString& name, CefRefPtr context = CefV8Context::GetCurrentContext(); - WatchCallback callback = ^(NSArray *eventList) { + WatchCallback callback = ^(NSString *eventType, NSString *path) { context->Enter(); CefV8ValueList args; CefRefPtr retval; CefRefPtr e; - - CefRefPtr eventObject = CefV8Value::CreateObject(NULL, NULL); - for (NSString *event in eventList) { - eventObject->SetValue([event UTF8String], CefV8Value::CreateBool(true), V8_PROPERTY_ATTRIBUTE_NONE); - } - - args.push_back(eventObject); + + args.push_back(CefV8Value::CreateString(std::string([eventType UTF8String], [eventType lengthOfBytesUsingEncoding:NSUTF8StringEncoding]))); + args.push_back(CefV8Value::CreateString(std::string([path UTF8String], [path lengthOfBytesUsingEncoding:NSUTF8StringEncoding]))); function->ExecuteFunction(function, args, retval, e, true); context->Exit(); diff --git a/Rakefile b/Rakefile index ad546f40d..9062ad55c 100644 --- a/Rakefile +++ b/Rakefile @@ -3,7 +3,8 @@ require 'fileutils' $ATOM_ARGS = [] ENV['PATH'] = "#{ENV['PATH']}:/usr/local/bin/" -BUILD_DIR = '/tmp/atom-build' +BUILD_DIR = 'atom-build' +mkdir_p BUILD_DIR desc "Build Atom via `xcodebuild`" task :build => :"verify-prerequisites" do diff --git a/docs/flexbox-test-old.html b/docs/flexbox-test-old.html new file mode 100644 index 000000000..683c44842 --- /dev/null +++ b/docs/flexbox-test-old.html @@ -0,0 +1,84 @@ + + + + wtf + + + + +
+
Keffiyeh mustache pickled post-ironic, lomo vegan food truck helvetica direct trade nostrud. Assumenda odio brunch, DIY non anim delectus sunt aliqua organic VHS nihil pork belly accusamus. Bushwick vice high life tumblr mumblecore ullamco. High life pop-up lomo, pariatur exercitation odio helvetica food truck ex williamsburg stumptown hoodie ea polaroid jean shorts. Keytar sed sapiente, mumblecore fixie +1 cred occaecat accusamus. Fanny pack authentic dolor, id four loko dolore ex +1 pop-up. Thundercats cliche aliqua, fugiat irony marfa chambray banksy kogi organic selvage VHS DIY. Id mumblecore nisi, brunch narwhal nostrud vegan squid before they sold out. Sriracha laborum nesciunt, in salvia you probably haven't heard of them mustache VHS commodo squid proident williamsburg. Magna vero gentrify labore, non american apparel occaecat put a bird on it mlkshk DIY occupy eu pinterest aute cliche. Nihil delectus commodo voluptate nostrud. Tattooed tempor skateboard, sed tumblr nostrud chambray put a bird on it non salvia helvetica consectetur mcsweeney's incididunt. Est semiotics ut yr, fanny pack leggings voluptate carles. Ea odd future hoodie cred. Ex small batch wayfarers sartorial. Delectus mumblecore skateboard, kogi esse keytar vinyl sriracha before they sold out typewriter marfa odd future viral mollit. Polaroid biodiesel street art viral cupidatat art party, post-ironic minim. Irony qui reprehenderit, put a bird on it eiusmod iphone labore skateboard. Craft beer keffiyeh echo park, 3 wolf moon thundercats gentrify dolor beard VHS ullamco cillum post-ironic qui chambray. Sartorial cred ex, aliqua trust fund est consectetur put a bird on it in nisi cupidatat sapiente art party freegan. Mlkshk authentic velit laborum. Est tattooed hella pickled qui flexitarian. Mustache wes anderson food truck, cardigan selvage organic wayfarers VHS irure typewriter irony. Kale chips pitchfork four loko before they sold out. Quis blog proident jean shorts voluptate, photo booth high life post-ironic odio hella whatever. Vinyl 3 wolf moon qui, officia non artisan et helvetica cosby sweater velit street art proident quinoa reprehenderit. Odd future fap put a bird on it laboris, kale chips tempor duis velit. Voluptate mlkshk brooklyn nihil. Hoodie sustainable excepteur next level. Anim qui aliqua officia keffiyeh semiotics. Est brooklyn pop-up photo booth, jean shorts banksy mumblecore. Consequat typewriter ennui put a bird on it odio. Reprehenderit narwhal master cleanse, messenger bag sed wayfarers vinyl adipisicing ex nesciunt. Mollit carles ethnic craft beer shoreditch, incididunt veniam laboris small batch authentic dreamcatcher proident you probably haven't heard of them seitan. Id art party narwhal assumenda farm-to-table brooklyn. Seitan artisan adipisicing put a bird on it aute lomo. Next level letterpress pitchfork, master cleanse Austin small batch scenester mlkshk trust fund hella accusamus laboris iphone lo-fi. Minim consectetur fanny pack occupy, lo-fi twee cupidatat nostrud laborum sint. Master cleanse consectetur excepteur enim food truck banksy. Qui nisi truffaut helvetica excepteur. Quinoa banksy non four loko tattooed keffiyeh pickled, ex semiotics quis odd future consectetur flexitarian. Quis readymade 8-bit nisi.
+
+
Keffiyeh mustache pickled post-ironic, lomo vegan food truck helvetica direct trade nostrud. Assumenda odio brunch, DIY non anim delectus sunt aliqua organic VHS nihil pork belly accusamus. Bushwick vice high life tumblr mumblecore ullamco. High life pop-up lomo, pariatur exercitation odio helvetica food truck ex williamsburg stumptown hoodie ea polaroid jean shorts. Keytar sed sapiente, mumblecore fixie +1 cred occaecat accusamus. Fanny pack authentic dolor, id four loko dolore ex +1 pop-up. Thundercats cliche aliqua, fugiat irony marfa chambray banksy kogi organic selvage VHS DIY. Id mumblecore nisi, brunch narwhal nostrud vegan squid before they sold out. Sriracha laborum nesciunt, in salvia you probably haven't heard of them mustache VHS commodo squid proident williamsburg. Magna vero gentrify labore, non american apparel occaecat put a bird on it mlkshk DIY occupy eu pinterest aute cliche. Nihil delectus commodo voluptate nostrud. Tattooed tempor skateboard, sed tumblr nostrud chambray put a bird on it non salvia helvetica consectetur mcsweeney's incididunt. Est semiotics ut yr, fanny pack leggings voluptate carles. Ea odd future hoodie cred. Ex small batch wayfarers sartorial. Delectus mumblecore skateboard, kogi esse keytar vinyl sriracha before they sold out typewriter marfa odd future viral mollit. Polaroid biodiesel street art viral cupidatat art party, post-ironic minim. Irony qui reprehenderit, put a bird on it eiusmod iphone labore skateboard. Craft beer keffiyeh echo park, 3 wolf moon thundercats gentrify dolor beard VHS ullamco cillum post-ironic qui chambray. Sartorial cred ex, aliqua trust fund est consectetur put a bird on it in nisi cupidatat sapiente art party freegan. Mlkshk authentic velit laborum. Est tattooed hella pickled qui flexitarian. Mustache wes anderson food truck, cardigan selvage organic wayfarers VHS irure typewriter irony. Kale chips pitchfork four loko before they sold out. Quis blog proident jean shorts voluptate, photo booth high life post-ironic odio hella whatever. Vinyl 3 wolf moon qui, officia non artisan et helvetica cosby sweater velit street art proident quinoa reprehenderit. Odd future fap put a bird on it laboris, kale chips tempor duis velit. Voluptate mlkshk brooklyn nihil. Hoodie sustainable excepteur next level. Anim qui aliqua officia keffiyeh semiotics. Est brooklyn pop-up photo booth, jean shorts banksy mumblecore. Consequat typewriter ennui put a bird on it odio. Reprehenderit narwhal master cleanse, messenger bag sed wayfarers vinyl adipisicing ex nesciunt. Mollit carles ethnic craft beer shoreditch, incididunt veniam laboris small batch authentic dreamcatcher proident you probably haven't heard of them seitan. Id art party narwhal assumenda farm-to-table brooklyn. Seitan artisan adipisicing put a bird on it aute lomo. Next level letterpress pitchfork, master cleanse Austin small batch scenester mlkshk trust fund hella accusamus laboris iphone lo-fi. Minim consectetur fanny pack occupy, lo-fi twee cupidatat nostrud laborum sint. Master cleanse consectetur excepteur enim food truck banksy. Qui nisi truffaut helvetica excepteur. Quinoa banksy non four loko tattooed keffiyeh pickled, ex semiotics quis odd future consectetur flexitarian. Quis readymade 8-bit nisi.
+
+
+ I am a child of a flexbox item. My height should be exactly the height of my container. +
+
+
Keffiyeh mustache pickled post-ironic, lomo vegan food truck helvetica direct trade nostrud. Assumenda odio brunch, DIY non anim delectus sunt aliqua organic VHS nihil pork belly accusamus. Bushwick vice high life tumblr mumblecore ullamco. High life pop-up lomo, pariatur exercitation odio helvetica food truck ex williamsburg stumptown hoodie ea polaroid jean shorts. Keytar sed sapiente, mumblecore fixie +1 cred occaecat accusamus. Fanny pack authentic dolor, id four loko dolore ex +1 pop-up. Thundercats cliche aliqua, fugiat irony marfa chambray banksy kogi organic selvage VHS DIY. Id mumblecore nisi, brunch narwhal nostrud vegan squid before they sold out. Sriracha laborum nesciunt, in salvia you probably haven't heard of them mustache VHS commodo squid proident williamsburg. Magna vero gentrify labore, non american apparel occaecat put a bird on it mlkshk DIY occupy eu pinterest aute cliche. Nihil delectus commodo voluptate nostrud. Tattooed tempor skateboard, sed tumblr nostrud chambray put a bird on it non salvia helvetica consectetur mcsweeney's incididunt. Est semiotics ut yr, fanny pack leggings voluptate carles. Ea odd future hoodie cred. Ex small batch wayfarers sartorial. Delectus mumblecore skateboard, kogi esse keytar vinyl sriracha before they sold out typewriter marfa odd future viral mollit. Polaroid biodiesel street art viral cupidatat art party, post-ironic minim. Irony qui reprehenderit, put a bird on it eiusmod iphone labore skateboard. Craft beer keffiyeh echo park, 3 wolf moon thundercats gentrify dolor beard VHS ullamco cillum post-ironic qui chambray. Sartorial cred ex, aliqua trust fund est consectetur put a bird on it in nisi cupidatat sapiente art party freegan. Mlkshk authentic velit laborum. Est tattooed hella pickled qui flexitarian. Mustache wes anderson food truck, cardigan selvage organic wayfarers VHS irure typewriter irony. Kale chips pitchfork four loko before they sold out. Quis blog proident jean shorts voluptate, photo booth high life post-ironic odio hella whatever. Vinyl 3 wolf moon qui, officia non artisan et helvetica cosby sweater velit street art proident quinoa reprehenderit. Odd future fap put a bird on it laboris, kale chips tempor duis velit. Voluptate mlkshk brooklyn nihil. Hoodie sustainable excepteur next level. Anim qui aliqua officia keffiyeh semiotics. Est brooklyn pop-up photo booth, jean shorts banksy mumblecore. Consequat typewriter ennui put a bird on it odio. Reprehenderit narwhal master cleanse, messenger bag sed wayfarers vinyl adipisicing ex nesciunt. Mollit carles ethnic craft beer shoreditch, incididunt veniam laboris small batch authentic dreamcatcher proident you probably haven't heard of them seitan. Id art party narwhal assumenda farm-to-table brooklyn. Seitan artisan adipisicing put a bird on it aute lomo. Next level letterpress pitchfork, master cleanse Austin small batch scenester mlkshk trust fund hella accusamus laboris iphone lo-fi. Minim consectetur fanny pack occupy, lo-fi twee cupidatat nostrud laborum sint. Master cleanse consectetur excepteur enim food truck banksy. Qui nisi truffaut helvetica excepteur. Quinoa banksy non four loko tattooed keffiyeh pickled, ex semiotics quis odd future consectetur flexitarian. Quis readymade 8-bit nisi.
+
+
Keffiyeh mustache pickled post-ironic, lomo vegan food truck helvetica direct trade nostrud. Assumenda odio brunch, DIY non anim delectus sunt aliqua organic VHS nihil pork belly accusamus. Bushwick vice high life tumblr mumblecore ullamco. High life pop-up lomo, pariatur exercitation odio helvetica food truck ex williamsburg stumptown hoodie ea polaroid jean shorts. Keytar sed sapiente, mumblecore fixie +1 cred occaecat accusamus. Fanny pack authentic dolor, id four loko dolore ex +1 pop-up. Thundercats cliche aliqua, fugiat irony marfa chambray banksy kogi organic selvage VHS DIY. Id mumblecore nisi, brunch narwhal nostrud vegan squid before they sold out. Sriracha laborum nesciunt, in salvia you probably haven't heard of them mustache VHS commodo squid proident williamsburg. Magna vero gentrify labore, non american apparel occaecat put a bird on it mlkshk DIY occupy eu pinterest aute cliche. Nihil delectus commodo voluptate nostrud. Tattooed tempor skateboard, sed tumblr nostrud chambray put a bird on it non salvia helvetica consectetur mcsweeney's incididunt. Est semiotics ut yr, fanny pack leggings voluptate carles. Ea odd future hoodie cred. Ex small batch wayfarers sartorial. Delectus mumblecore skateboard, kogi esse keytar vinyl sriracha before they sold out typewriter marfa odd future viral mollit. Polaroid biodiesel street art viral cupidatat art party, post-ironic minim. Irony qui reprehenderit, put a bird on it eiusmod iphone labore skateboard. Craft beer keffiyeh echo park, 3 wolf moon thundercats gentrify dolor beard VHS ullamco cillum post-ironic qui chambray. Sartorial cred ex, aliqua trust fund est consectetur put a bird on it in nisi cupidatat sapiente art party freegan. Mlkshk authentic velit laborum. Est tattooed hella pickled qui flexitarian. Mustache wes anderson food truck, cardigan selvage organic wayfarers VHS irure typewriter irony. Kale chips pitchfork four loko before they sold out. Quis blog proident jean shorts voluptate, photo booth high life post-ironic odio hella whatever. Vinyl 3 wolf moon qui, officia non artisan et helvetica cosby sweater velit street art proident quinoa reprehenderit. Odd future fap put a bird on it laboris, kale chips tempor duis velit. Voluptate mlkshk brooklyn nihil. Hoodie sustainable excepteur next level. Anim qui aliqua officia keffiyeh semiotics. Est brooklyn pop-up photo booth, jean shorts banksy mumblecore. Consequat typewriter ennui put a bird on it odio. Reprehenderit narwhal master cleanse, messenger bag sed wayfarers vinyl adipisicing ex nesciunt. Mollit carles ethnic craft beer shoreditch, incididunt veniam laboris small batch authentic dreamcatcher proident you probably haven't heard of them seitan. Id art party narwhal assumenda farm-to-table brooklyn. Seitan artisan adipisicing put a bird on it aute lomo. Next level letterpress pitchfork, master cleanse Austin small batch scenester mlkshk trust fund hella accusamus laboris iphone lo-fi. Minim consectetur fanny pack occupy, lo-fi twee cupidatat nostrud laborum sint. Master cleanse consectetur excepteur enim food truck banksy. Qui nisi truffaut helvetica excepteur. Quinoa banksy non four loko tattooed keffiyeh pickled, ex semiotics quis odd future consectetur flexitarian. Quis readymade 8-bit nisi.
+
+ + + diff --git a/spec/app/buffer-spec.coffee b/spec/app/buffer-spec.coffee index 57c942835..9840584a4 100644 --- a/spec/app/buffer-spec.coffee +++ b/spec/app/buffer-spec.coffee @@ -42,14 +42,40 @@ describe 'Buffer', -> expect(buffer.getText()).toBe "" describe "path-change event", -> - afterEach -> - fs.remove("/tmp/moo.text") if fs.exists("/tmp/moo.text") + [path, newPath, bufferToChange, eventHandler] = [] - it "emits path-change event when path is changed", -> + beforeEach -> + path = fs.join(require.resolve("fixtures/"), "atom-manipulate-me") + newPath = "#{path}-i-moved" + fs.write(path, "") + bufferToChange = new Buffer(path) eventHandler = jasmine.createSpy('eventHandler') - buffer.on 'path-change', eventHandler - buffer.saveAs("/tmp/moo.text") - expect(eventHandler).toHaveBeenCalledWith(buffer) + bufferToChange.on 'path-change', eventHandler + + afterEach -> + bufferToChange.destroy() + fs.remove(path) if fs.exists(path) + fs.remove(newPath) if fs.exists(newPath) + + it "triggers a `path-change` event when path is changed", -> + bufferToChange.saveAs(newPath) + expect(eventHandler).toHaveBeenCalledWith(bufferToChange) + + it "triggers a `path-change` event when the file is moved", -> + fs.remove(newPath) if fs.exists(newPath) + fs.move(path, newPath) + + waitsFor "buffer path change", -> + eventHandler.callCount > 0 + + runs -> + expect(eventHandler).toHaveBeenCalledWith(bufferToChange) + + it "triggers a `path-change` event when the file is removed", -> + fs.remove(path) + + waitsFor "buffer path change", -> + eventHandler.callCount > 0 describe "when the buffer's file is modified (via another process)", -> path = null @@ -105,6 +131,18 @@ describe 'Buffer', -> runs -> expect(buffer.isModifiedOnDisk()).toBeTruthy() + describe "when the buffer's file is deleted (via another process)", -> + it "no longer has a path", -> + path = "/tmp/atom-file-to-delete.txt" + fs.write(path, '') + bufferToDelete = new Buffer(path) + expect(bufferToDelete.getPath()).toBe path + + fs.remove(path) + + waitsFor "file to be removed", -> + not bufferToDelete.getPath() + describe ".isModified()", -> it "returns true when user changes buffer", -> expect(buffer.isModified()).toBeFalsy() diff --git a/spec/app/file-spec.coffee b/spec/app/file-spec.coffee index f672eb169..6beb5cfcc 100644 --- a/spec/app/file-spec.coffee +++ b/spec/app/file-spec.coffee @@ -2,25 +2,24 @@ File = require 'file' fs = require 'fs' describe 'File', -> - file = null + [path, file] = [] beforeEach -> - path = fs.join(require.resolve('fixtures'), "temp.txt") + path = fs.join(require.resolve('fixtures'), "atom-file-test.txt") # Don't put in /tmp because /tmp symlinks to /private/tmp and screws up the rename test fs.remove(path) if fs.exists(path) fs.write(path, "this is old!") file = new File(path) afterEach -> file.off() - fs.remove(file.getPath()) if fs.exists(file.getPath()) + fs.remove(path) if fs.exists(path) describe "when the contents of the file change", -> it "triggers 'contents-change' event handlers", -> changeHandler = null - runs -> - changeHandler = jasmine.createSpy('changeHandler') - file.on 'contents-change', changeHandler - fs.write(file.getPath(), "this is new!") + changeHandler = jasmine.createSpy('changeHandler') + file.on 'contents-change', changeHandler + fs.write(file.getPath(), "this is new!") waitsFor "change event", -> changeHandler.callCount > 0 @@ -31,3 +30,56 @@ describe 'File', -> waitsFor "second change event", -> changeHandler.callCount > 0 + + describe "when the file is removed", -> + it "triggers 'remove' event handlers", -> + removeHandler = null + removeHandler = jasmine.createSpy('removeHandler') + file.on 'remove', removeHandler + fs.remove(file.getPath()) + + waitsFor "remove event", -> + removeHandler.callCount > 0 + + describe "when a file is moved (via the filesystem)", -> + newPath = null + + beforeEach -> + newPath = fs.join(fs.directory(path), "atom-file-was-moved-test.txt") + + afterEach -> + fs.remove(newPath) if fs.exists(newPath) + + it "it updates its path", -> + moveHandler = null + moveHandler = jasmine.createSpy('moveHandler') + file.on 'move', moveHandler + + fs.move(path, newPath) + + waitsFor "move event", -> + moveHandler.callCount > 0 + + runs -> + expect(file.getPath()).toBe newPath + + it "maintains 'contents-change' events set on previous path", -> + moveHandler = null + moveHandler = jasmine.createSpy('moveHandler') + file.on 'move', moveHandler + changeHandler = null + changeHandler = jasmine.createSpy('changeHandler') + file.on 'contents-change', changeHandler + + fs.move(path, newPath) + + waitsFor "move event", -> + moveHandler.callCount > 0 + + runs -> + expect(changeHandler).not.toHaveBeenCalled() + fs.write(file.getPath(), "this is new!") + + waitsFor "change event", -> + changeHandler.callCount > 0 + diff --git a/spec/extensions/command-panel-spec.coffee b/spec/extensions/command-panel-spec.coffee index d89cb2f95..786c0260f 100644 --- a/spec/extensions/command-panel-spec.coffee +++ b/spec/extensions/command-panel-spec.coffee @@ -29,7 +29,6 @@ describe "CommandPanel", -> newRootView.remove() - describe "when command-panel:close is triggered on the command panel", -> it "detaches the command panel", -> commandPanel.attach() diff --git a/spec/extensions/tree-view-spec.coffee b/spec/extensions/tree-view-spec.coffee index 786327d9f..464b744b0 100644 --- a/spec/extensions/tree-view-spec.coffee +++ b/spec/extensions/tree-view-spec.coffee @@ -610,14 +610,17 @@ describe "TreeView", -> describe "when the directories along the new path don't exist", -> it "creates the target directory before moving the file", -> - runs -> - newPath = fs.join(rootDirPath, 'new/directory', 'renamed-test-file.txt') - moveDialog.miniEditor.setText(newPath) + #rootView.project.destroy() # Ensure there are no open buffers (renaming a file asynchronously updates the buffer's path, this causes the afterEach block to unwatch the previous path, which no longer exists.) - moveDialog.trigger 'tree-view:confirm' + newPath = fs.join(rootDirPath, 'new/directory', 'renamed-test-file.txt') + moveDialog.miniEditor.setText(newPath) - expect(fs.exists(newPath)).toBeTruthy() - expect(fs.exists(filePath)).toBeFalsy() + moveDialog.trigger 'tree-view:confirm' + + expect(fs.exists(newPath)).toBeTruthy() + expect(fs.exists(filePath)).toBeFalsy() + + waits 50 # temporary hack (maybe). describe "when a file or directory already exists at the target path", -> it "shows an error message and does not close the dialog", -> diff --git a/src/app/buffer.coffee b/src/app/buffer.coffee index c1c71af07..31f97f681 100644 --- a/src/app/buffer.coffee +++ b/src/app/buffer.coffee @@ -60,6 +60,13 @@ class Buffer @setText(fs.read(@file.getPath())) @modified = false + @file.on "remove", => + @file = null + @trigger "path-change", this + + @file.on "move", => + @trigger "path-change", this + reload: -> @setText(fs.read(@file.getPath())) @modified = false diff --git a/src/app/directory.coffee b/src/app/directory.coffee index 7d0ade69c..03bcf54f5 100644 --- a/src/app/directory.coffee +++ b/src/app/directory.coffee @@ -29,8 +29,8 @@ class Directory @unsubscribeFromNativeChangeEvents() if @subscriptionCount() == 0 subscribeToNativeChangeEvents: -> - @watchId = $native.watchPath @path, (eventTypes) => - @trigger 'contents-change' if eventTypes.modified? + @watchId = $native.watchPath @path, (eventType) => + @trigger "contents-change" if eventType is "contents-change" unsubscribeFromNativeChangeEvents: -> $native.unwatchPath(@path, @watchId) diff --git a/src/app/file.coffee b/src/app/file.coffee index 4c3826f0e..42baf5e7b 100644 --- a/src/app/file.coffee +++ b/src/app/file.coffee @@ -12,6 +12,8 @@ class File throw "Creating file with path that is not a file: #{@path}" unless fs.isFile(@path) @updateMd5() + setPath: (@path) -> + getPath: -> @path @@ -28,9 +30,17 @@ class File @unsubscribeFromNativeChangeEvents() if @subscriptionCount() == 0 subscribeToNativeChangeEvents: -> - @watchId = $native.watchPath @path, (eventTypes) => - newMd5 = fs.md5ForPath(@getPath()) - if eventTypes.modified? and newMd5 != @md5 + @watchId = $native.watchPath @path, (eventType, path) => + if eventType is "remove" + @trigger "remove" + @off() + else if eventType is "move" + @setPath(path) + @trigger "move" + else if eventType is "contents-change" + newMd5 = fs.md5ForPath(@getPath()) + return if newMd5 == @md5 + @md5 = newMd5 @trigger 'contents-change' diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index 11ff02dff..2f9e4b9bd 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -18,7 +18,8 @@ class RootView extends View @content: -> @div id: 'root-view', tabindex: -1, => @div id: 'horizontal', outlet: 'horizontal', => - @div id: 'panes', outlet: 'panes' + @div id: 'vertical', outlet: 'vertical', => + @div id: 'panes', outlet: 'panes' @deserialize: ({ projectPath, panesViewState, extensionStates }) -> rootView = new RootView(projectPath, extensionStates: extensionStates, suppressOpen: true) diff --git a/src/extensions/command-panel/command-panel.coffee b/src/extensions/command-panel/command-panel.coffee index 9bf17f96a..7df61e175 100644 --- a/src/extensions/command-panel/command-panel.coffee +++ b/src/extensions/command-panel/command-panel.coffee @@ -85,7 +85,7 @@ class CommandPanel extends View @miniEditor.focus() attach: (text='') -> - @rootView.append(this) + @rootView.vertical.append(this) @miniEditor.focus() @miniEditor.setText(text) @miniEditor.setCursorBufferPosition([0, Infinity]) diff --git a/static/atom.css b/static/atom.css index 4da5892bc..a78cbf383 100644 --- a/static/atom.css +++ b/static/atom.css @@ -8,7 +8,6 @@ body { } #root-view { - height: 100%; height: 100%; overflow-y: auto; position: relative; @@ -16,26 +15,36 @@ body { } #root-view #horizontal { - display: -webkit-flexbox; + display: -webkit-box; + height: 100%; +} + +#root-view #vertical { + -webkit-box-flex: 1; + display: -webkit-box; + -webkit-box-orient: vertical; } #root-view #panes { - height: 100%; - width: -webkit-flex(1 0); + -webkit-box-flex: 1; position: relative; } #root-view #panes .column { position: absolute; - width: 100%; - height: 100%; + top: 0; + bottom: 0; + left: 0; + right: 0; overflow-y: hidden; } #root-view #panes .row { position: absolute; - width: 100%; - height: 100%; + top: 0; + bottom: 0; + left: 0; + right: 0; overflow-x: hidden; } @@ -43,8 +52,10 @@ body { position: absolute; display: -webkit-box; -webkit-box-orient: vertical; - width: 100%; - height: 100%; + top: 0; + bottom: 0; + left: 0; + right: 0; box-sizing: border-box; } diff --git a/static/command-panel.css b/static/command-panel.css index cfb7f835e..31801e14b 100644 --- a/static/command-panel.css +++ b/static/command-panel.css @@ -1,6 +1,4 @@ .command-panel { - position: absolute; - bottom: 0; width: 100%; background: #515151; padding: 3px; diff --git a/static/editor.css b/static/editor.css index e4816f26b..e2cc3a7e2 100644 --- a/static/editor.css +++ b/static/editor.css @@ -9,13 +9,17 @@ position: relative; } - .editor.mini { height: auto; } .editor .flexbox { - display: -webkit-flexbox; + display: -webkit-box; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; } .editor .gutter { @@ -51,8 +55,7 @@ .editor .scroll-view { overflow-x: auto; overflow-y: hidden; - width: -webkit-flex(1); - height: 100%; + -webkit-box-flex: 1; } .editor.mini .scroll-view { From 7270758a9e0597486df9d3eb93ba67d619ba773c Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 25 Jul 2012 13:26:57 -0600 Subject: [PATCH 97/97] Focus the active editor after executing a previewed operation --- spec/extensions/command-panel-spec.coffee | 8 ++++++-- src/extensions/command-panel/preview-list.coffee | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/spec/extensions/command-panel-spec.coffee b/spec/extensions/command-panel-spec.coffee index 786c0260f..9fe0e52f2 100644 --- a/spec/extensions/command-panel-spec.coffee +++ b/spec/extensions/command-panel-spec.coffee @@ -324,7 +324,8 @@ describe "CommandPanel", -> console.log previewList.find('li:first').position().top describe "when command-panel:execute is triggered on the preview list", -> - it "opens a new editor with the operation's buffer and selects the search result", -> + it "opens the operation's buffer, selects the search result, and focuses the active editor", -> + spyOn(rootView, 'focus') executeHandler = jasmine.createSpy('executeHandler') commandPanel.on 'command-panel:execute', executeHandler @@ -336,11 +337,13 @@ describe "CommandPanel", -> editSession = rootView.getActiveEditSession() expect(editSession.buffer.getPath()).toBe project.resolve(operation.getPath()) expect(editSession.getSelectedBufferRange()).toEqual operation.getBufferRange() + expect(rootView.focus).toHaveBeenCalled() expect(executeHandler).not.toHaveBeenCalled() describe "when an operation in the preview list is clicked", -> - it "opens a new editor with the operation's buffer and selects the search result", -> + it "opens the operation's buffer, selects the search result, and focuses the active editor", -> + spyOn(rootView, 'focus') operation = previewList.getOperations()[4] previewList.find('li:eq(4) span').mousedown() @@ -349,3 +352,4 @@ describe "CommandPanel", -> editSession = rootView.getActiveEditSession() expect(editSession.buffer.getPath()).toBe project.resolve(operation.getPath()) expect(editSession.getSelectedBufferRange()).toEqual operation.getBufferRange() + expect(rootView.focus).toHaveBeenCalled() diff --git a/src/extensions/command-panel/preview-list.coffee b/src/extensions/command-panel/preview-list.coffee index 31d338f3f..57828d7d3 100644 --- a/src/extensions/command-panel/preview-list.coffee +++ b/src/extensions/command-panel/preview-list.coffee @@ -59,6 +59,7 @@ class PreviewList extends View operation = @getSelectedOperation() editSession = @rootView.open(operation.getPath()) operation.execute(editSession) + @rootView.focus() false getOperations: ->

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