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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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;
diff --git a/Atom/src/native_handler.mm b/Atom/src/native_handler.mm
index 8fa2feb11..e900e37b7 100644
--- a/Atom/src/native_handler.mm
+++ b/Atom/src/native_handler.mm
@@ -4,9 +4,12 @@
#import "AtomController.h"
#import "client_handler.h"
#import "PathWatcher.h"
+#import
#import
#import
+#import
+
#define MY_EXCEPTION_TRY @try {
#define MY_EXCEPTION_HANDLE } @catch (NSException *localException) {}
@@ -15,10 +18,22 @@ 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() {";
- 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]);
@@ -419,5 +434,96 @@ 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(std::string([contents UTF8String], [contents lengthOfBytesUsingEncoding:NSUTF8StringEncoding])));
+ function->ExecuteFunction(function, args, retval, e, false);
+
+ if (e.get()) {
+ throwException(context->GetGlobal(), e, @"Error thrown in OutputHandle");
+ }
+
+ [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;
+ 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, false);
+
+ if (e.get()) {
+ throwException(context->GetGlobal(), e, @"Error thrown in TaskTerminatedHandle");
+ }
+
+ 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/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/buffer-spec.coffee b/spec/app/buffer-spec.coffee
index 1567813c1..d66fb62c2 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", ->
@@ -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", ->
@@ -82,8 +82,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)
@@ -146,7 +146,10 @@ describe 'Buffer', ->
describe ".isModified()", ->
beforeEach ->
buffer.destroy()
+ waitsFor "file to be removed", ->
+ not bufferToDelete.getPath()
+ describe ".isModified()", ->
it "returns true when user changes buffer", ->
expect(buffer.isModified()).toBeFalsy()
buffer.insert([0,0], "hi")
@@ -155,6 +158,7 @@ describe 'Buffer', ->
it "returns false after modified buffer is saved", ->
filePath = "/tmp/atom-tmp-file"
fs.write(filePath, '')
+ buffer.release()
buffer = new Buffer(filePath)
expect(buffer.isModified()).toBe false
@@ -298,7 +302,7 @@ describe 'Buffer', ->
describe ".save()", ->
beforeEach ->
- buffer.destroy()
+ buffer.release()
describe "when the buffer has a path", ->
filePath = null
@@ -349,33 +353,34 @@ describe 'Buffer', ->
expect(buffer.getText()).toBe(fileContents)
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")
@@ -597,3 +602,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/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee
index a3ecf9f2b..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
@@ -523,6 +523,47 @@ 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 "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]])
+ 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 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]]
@@ -870,8 +911,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));"
@@ -955,12 +996,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
@@ -1265,23 +1306,20 @@ 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())
+ otherEditSession = fixturesProject.buildEditSessionForPath(editSession.getPath())
otherEditSession.setSelectedBufferRange([[2, 2], [3, 3]])
otherEditSession.delete()
@@ -1324,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/spec/app/editor-spec.coffee b/spec/app/editor-spec.coffee
index 3f4316504..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()
@@ -1557,13 +1557,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])
@@ -1582,7 +1582,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()
@@ -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 15ef5e7b5..bea202ef1 100644
--- a/spec/app/project-spec.coffee
+++ b/spec/app/project-spec.coffee
@@ -11,23 +11,19 @@ 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")
- buffer = editSession.buffer
- spyOn(buffer, 'destroy').andCallThrough()
+ editSession = project.buildEditSessionForPath("a")
+ anotherEditSession = project.buildEditSessionForPath("a")
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 ".open(path)", ->
+ describe ".buildEditSessionForPath(path)", ->
[absolutePath, newBufferHandler, newEditSessionHandler] = []
beforeEach ->
absolutePath = require.resolve('fixtures/dir/a')
@@ -38,34 +34,48 @@ 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
+ 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')
@@ -106,3 +116,60 @@ 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", ->
+ it "calls the callback with all regex matches in all files in the project", ->
+ matches = []
+ waitsForPromise ->
+ project.scan /(a)+/, ({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]]
+
+ 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.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"
+
+ 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]]
diff --git a/spec/app/root-view-spec.coffee b/spec/app/root-view-spec.coffee
index 5114e6569..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)
@@ -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()
@@ -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/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/spec/extensions/autocomplete-spec.coffee b/spec/extensions/autocomplete-spec.coffee
index ca430de4c..442390f47 100644
--- a/spec/extensions/autocomplete-spec.coffee
+++ b/spec/extensions/autocomplete-spec.coffee
@@ -10,12 +10,12 @@ 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
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)", ->
@@ -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", ->
@@ -357,7 +356,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 +364,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")
@@ -382,6 +381,7 @@ describe "Autocomplete", ->
editor.remove()
editor.getBuffer().insert([0,0], "s")
expect(autocomplete.buildWordList).not.toHaveBeenCalled()
+ editor = null
describe ".attach()", ->
beforeEach ->
diff --git a/spec/extensions/command-interpreter-spec.coffee b/spec/extensions/command-interpreter-spec.coffee
index b4c7c1e46..143f64754 100644
--- a/spec/extensions/command-interpreter-spec.coffee
+++ b/spec/extensions/command-interpreter-spec.coffee
@@ -1,258 +1,329 @@
CommandInterpreter = require 'command-panel/command-interpreter'
+Project = require 'project'
Buffer = require 'buffer'
EditSession = require 'edit-session'
-Editor = require 'editor'
describe "CommandInterpreter", ->
- [interpreter, editor, buffer] = []
+ [project, interpreter, editSession, buffer, anchorCountBefore] = []
beforeEach ->
- editSession = fixturesProject.open('sample.js')
+ project = new Project(fixturesProject.resolve('dir/'))
+ interpreter = new CommandInterpreter(fixturesProject)
+ editSession = fixturesProject.buildEditSessionForPath('sample.js')
buffer = editSession.buffer
- editor = new Editor(editSession: editSession)
- interpreter = new CommandInterpreter()
afterEach ->
- editor.remove()
+ editSession?.destroy()
+ expect(buffer.getAnchors().length).toBe 0
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]]
+ 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(editor, '0')
- expect(editor.getSelections().length).toBe 1
- expect(editor.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(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]]
+ waitsForPromise -> interpreter.eval('$', editSession)
+ runs ->
+ 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]]
+ 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', ->
- editor.clearSelections()
- editor.setSelectedBufferRange([[1,1], [2,2]])
- interpreter.eval(editor, '.')
- expect(editor.getSelection().getBufferRange()).toEqual [[1,1], [2,2]]
+ editSession.clearSelections()
- editor.setSelectedBufferRange([[1,1], [2,2]])
- interpreter.eval(editor, '.,')
- expect(editor.getSelection().getBufferRange()).toEqual [[1,1], [12,2]]
+ waitsForPromise ->
+ editSession.setSelectedBufferRange([[1,1], [2,2]])
+ interpreter.eval('.', editSession)
- editor.setSelectedBufferRange([[1,1], [2,2]])
- interpreter.eval(editor, ',.')
- expect(editor.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", ->
- preSelections = editor.getSelections()
+ preSelections = editSession.getSelections()
expect(preSelections.length).toBe 3
[preRange1, preRange2, preRange3] = preSelections.map (s) -> s.getScreenRange()
- interpreter.eval(editor, '.')
+ waitsForPromise -> interpreter.eval('.', editSession)
- selections = editor.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 ->
- 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]]
+ 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', ->
- editor.setSelectedBufferRange([[4,16], [4,20]])
- interpreter.eval(editor, '/pivot')
- expect(editor.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", ->
- 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()
- 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", ->
- editor.setSelectedBufferRange([[10, 0], [10,3]])
- interpreter.eval(editor, '/pivot')
- expect(editor.getSelection().getBufferRange()).toEqual [[3,8], [3,13]]
+ waitsForPromise ->
+ editSession.setSelectedBufferRange([[10, 0], [10,3]])
+ interpreter.eval('/pivot', editSession)
- interpreter.eval(editor, '/mike tyson')
- expect(editor.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 -", ->
- editor.setSelectedBufferRange([[6, 16], [6, 22]])
- interpreter.eval(editor, '-/pivot')
- expect(editor.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(editor, '4,7')
- expect(editor.getSelections().length).toBe 1
- expect(editor.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(editor, ',7')
- expect(editor.getSelections().length).toBe 1
- expect(editor.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(editor, '4,')
- expect(editor.getSelections().length).toBe 1
- expect(editor.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(editor, ',')
- expect(editor.getSelections().length).toBe 1
- expect(editor.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(editor, '6,7 x/current/')
+ waitsForPromise -> interpreter.eval('6,7 x/current/', editSession)
- selections = editor.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(editor, '6,8 x/$/')
+ waitsForPromise -> interpreter.eval('6,8 x/$/', editSession)
- cursors = editor.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", ->
- editor.setSelectedBufferRange [[3,0], [3,62]]
- editor.addSelectionForBufferRange [[6,0], [6,65]]
+ 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)
- interpreter.eval(editor, 'x/current')
+ runs ->
+ selections = editSession.getSelections()
+ expect(selections.length).toBe 4
- selections = editor.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", ->
- editor.setSelectedBufferRange([[6, 0], [6, 44]])
- interpreter.eval(editor, 's/not-in-text/foo/')
- 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", ->
- editor.setSelectedBufferRange([[6, 0], [6, 44]])
- interpreter.eval(editor, 's/current/foo/')
- 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", ->
- editor.setSelectedBufferRange([[5, 0], [5, 20]])
- editor.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(editor, 's/current/foo/')
- 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", ->
- editor.setSelectedBufferRange([[6, 0], [6, 44]])
- interpreter.eval(editor, 's/current/foo/g')
- expect(buffer.lineForRow(6)).toBe ' foo < pivot ? left.push(foo) : right.push(current);'
+ waitsForPromise ->
+ editSession.setSelectedBufferRange([[6, 0], [6, 44]])
+ interpreter.eval('s/current/foo/g', editSession)
- describe "when prefixed with an address", ->
- it "only makes substitutions within given lines", ->
- interpreter.eval(editor, '4,6s/ /!/g')
- 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(editor, ',s/$/!!!/g')
- 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(editor, ',s/^/!!!/g')
- 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", ->
- editor.setSelectedBufferRange([[5, 0], [5, 20]])
- editor.addSelectionForBufferRange([[6, 0], [6, 44]])
-
- interpreter.eval(editor, 's/current/foo/g')
- 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", ->
- 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]]]
+ waitsForPromise ->
+ editSession.setSelectedBufferRanges([[[5, 0], [5, 20]], [[6, 0], [6, 44]]])
+ interpreter.eval(',s/current/foo/g', editSession)
- 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]])
+ runs ->
+ expect(editSession.getSelectedBufferRanges()).toEqual [[[5, 0], [5, 16]], [[6, 0], [6, 36]]]
- 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)
+ 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(project)
+
+ operations = null
+ waitsForPromise ->
+ interpreter.eval("X x/a+/").done (ops) -> operations = ops
+
+ runs ->
+ expect(operations.length).toBeGreaterThan 3
+ for operation in operations
+ editSession = project.buildEditSessionForPath(operation.getPath())
+ operation.execute(editSession)
+ expect(editSession.getSelectedText()).toMatch /a+/
+ editSession.destroy()
+ operation.destroy()
+
+ editSession = null
diff --git a/spec/extensions/command-panel-spec.coffee b/spec/extensions/command-panel-spec.coffee
index f28fd4e63..9fe0e52f2 100644
--- a/spec/extensions/command-panel-spec.coffee
+++ b/spec/extensions/command-panel-spec.coffee
@@ -1,18 +1,21 @@
RootView = require 'root-view'
CommandPanel = require 'command-panel'
+_ = require 'underscore'
describe "CommandPanel", ->
- [rootView, editor, 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')
afterEach ->
- rootView.remove()
+ rootView.deactivate()
describe "serialization", ->
it "preserves the command panel's mini editor text and visibility across reloads", ->
@@ -26,6 +29,12 @@ 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()
@@ -34,14 +43,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'
@@ -54,6 +63,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()
@@ -63,7 +148,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'
@@ -111,19 +195,61 @@ 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 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]
+
+ 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 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.hiddenInput.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.hiddenInput.trigger keydownEvent('enter')
- expect(commandPanel.execute).toHaveBeenCalled()
+ expect(buffer.lineForRow(0)).toMatch /quicktorta/
+ expect(buffer.lineForRow(1)).toMatch /var torta/
+
+ describe "when the command returns operations to be previewed", ->
+ beforeEach ->
+ rootView.attachToDom()
+ editor.remove()
+ rootView.trigger 'command-panel:toggle'
+ waitsForPromise -> commandPanel.execute('X x/a+/')
+
+ 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-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", ->
@@ -156,12 +282,74 @@ 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]]
+ describe "when the preview list is focused with search operations", ->
+ previewList = null
+
+ beforeEach ->
+ previewList = commandPanel.previewList
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()
+ 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 (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
+
+ describe "when command-panel:execute is triggered on the preview list", ->
+ 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
+
+ _.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(rootView.focus).toHaveBeenCalled()
+
+ expect(executeHandler).not.toHaveBeenCalled()
+
+ describe "when an operation in the preview list is clicked", ->
+ 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()
+
+ expect(previewList.getSelectedOperation()).toBe operation
+ 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/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/spec/extensions/tree-view-spec.coffee b/spec/extensions/tree-view-spec.coffee
index 6d120c132..ca073ed51 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()
@@ -610,8 +610,6 @@ describe "TreeView", ->
describe "when the directories along the new path don't exist", ->
it "creates the target directory before moving the file", ->
- 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.)
-
newPath = fs.join(rootDirPath, 'new/directory', 'renamed-test-file.txt')
moveDialog.miniEditor.setText(newPath)
@@ -620,6 +618,8 @@ describe "TreeView", ->
expect(fs.exists(newPath)).toBeTruthy()
expect(fs.exists(filePath)).toBeFalsy()
+ waits 50 # TODO: remove this workaround once we fix the race condition in fs events
+
describe "when a file or directory already exists at the target path", ->
it "shows an error message and does not close the dialog", ->
runs ->
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/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/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/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: ă
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..140c7bf61
--- /dev/null
+++ 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\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\".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
new file mode 100644
index 000000000..782a5bf5a
--- /dev/null
+++ b/spec/fixtures/evil-files/utfă.md
@@ -0,0 +1 @@
+I am evil because there's a UTF-8 character in my name
diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee
index 672e1e5a9..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()
@@ -88,15 +89,32 @@ 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) ->
- fn().done(moveOn)
+ promise = fn()
+ if shouldReject
+ promise.fail(moveOn)
+ promise.done ->
+ jasmine.getEnv().currentSpec.fail("Expected promise to be rejected, but it was resolved")
+ moveOn()
+ else
+ promise.done(moveOn)
+ promise.fail (error) ->
+ jasmine.getEnv().currentSpec.fail("Expected promise to be resolved, but it was rejected with #{jasmine.pp(error)}")
+ moveOn()
window.resetTimeouts = ->
window.now = 0
window.timeoutCount = 0
window.timeouts = []
+window.originalSetTimeout = window.setTimeout
window.setTimeout = (callback, ms) ->
id = ++window.timeoutCount
window.timeouts.push([id, window.now + ms, callback])
diff --git a/spec/stdlib/child-process-spec.coffee b/spec/stdlib/child-process-spec.coffee
new file mode 100644
index 000000000..7d57a21cb
--- /dev/null
+++ b/spec/stdlib/child-process-spec.coffee
@@ -0,0 +1,94 @@
+ChildProcess = require 'child-process'
+
+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"
+ ChildProcess.exec(cmd).done (stdout, stderr) ->
+ expect(stdout).toBe 'good\n'
+ expect(stderr).toBe 'bad\n'
+
+ 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)
+
+ 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"
+
+ 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)
+
+ 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 shouldReject: true, ->
+ 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/app/anchor-range.coffee b/src/app/anchor-range.coffee
index 6340945e7..54609a67f 100644
--- a/src/app/anchor-range.coffee
+++ b/src/app/anchor-range.coffee
@@ -4,11 +4,13 @@ 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)
+ @startAnchor = @buffer.addAnchorAtPosition(bufferRange.start, ignoreEqual: true)
+ @endAnchor = @buffer.addAnchorAtPosition(bufferRange.end)
getBufferRange: ->
new Range(@startAnchor.getBufferPosition(), @endAnchor.getBufferPosition())
@@ -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..2755974c0 100644
--- a/src/app/anchor.coffee
+++ b/src/app/anchor.coffee
@@ -4,12 +4,15 @@ _ = 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 = {}) ->
+ { @editSession, @ignoreEqual, @strong } = options
handleBufferChange: (e) ->
{ oldRange, newRange } = e
@@ -43,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: ->
@@ -65,11 +68,13 @@ 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)
destroy: ->
- @editSession.removeAnchor(this)
+ @buffer.removeAnchor(this)
+ @editSession?.removeAnchor(this)
@trigger 'destroy'
@off()
diff --git a/src/app/buffer-change-operation.coffee b/src/app/buffer-change-operation.coffee
index 2aa0e6098..b9f61bec8 100644
--- a/src/app/buffer-change-operation.coffee
+++ b/src/app/buffer-change-operation.coffee
@@ -38,7 +38,11 @@ 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()
+ @buffer.trigger 'update-anchors-after-change'
newRange
calculateNewRange: (oldRange, newText) ->
diff --git a/src/app/buffer.coffee b/src/app/buffer.coffee
index fa7b0c577..31f97f681 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
@@ -15,9 +17,14 @@ class Buffer
modifiedOnDisk: null
lines: null
file: null
+ anchors: null
+ anchorRanges: null
+ refcount: 0
- constructor: (path) ->
+ constructor: (path, @project) ->
@id = @constructor.idCounter++
+ @anchors = []
+ @anchorRanges = []
@lines = ['']
if path
@@ -31,22 +38,21 @@ class Buffer
@modified = false
destroy: ->
+ throw new Error("Destroying buffer twice with path '#{@getPath()}'") if @destroyed
@file?.off()
+ @destroyed = true
+ @project?.removeBuffer(this)
- reload: ->
- @setText(fs.read(@file.getPath()))
- @modified = false
- @modifiedOnDisk = false
+ retain: ->
+ @refcount++
+ this
- getPath: ->
- @file?.getPath()
-
- setPath: (path) ->
- return if path == @getPath()
-
- @file?.off()
- @file = new File(path)
+ release: ->
+ @refcount--
+ @destroy() if @refcount <= 0
+ this
+ subscribeToFile: ->
@file.on "contents-change", =>
if @isModified()
@modifiedOnDisk = true
@@ -61,6 +67,20 @@ class Buffer
@file.on "move", =>
@trigger "path-change", this
+ reload: ->
+ @setText(fs.read(@file.getPath()))
+ @modified = false
+ @modifiedOnDisk = false
+
+ getPath: ->
+ @file?.getPath()
+
+ setPath: (path) ->
+ return if path == @getPath()
+
+ @file?.off()
+ @file = new File(path)
+ @subscribeToFile()
@trigger "path-change", this
getExtension: ->
@@ -151,6 +171,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..]
@@ -194,6 +223,29 @@ class Buffer
isModified: ->
@modified
+ getAnchors: -> new Array(@anchors...)
+
+ 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/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 2632abc31..67b1a8857 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'
@@ -13,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)
@@ -41,11 +42,11 @@ class EditSession
@selections = []
@addCursorAtScreenPosition([0, 0])
+ @buffer.retain()
@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()
+ @buffer.on "update-anchors-after-change.edit-session-#{@id}", =>
@mergeCursors()
@displayBuffer.on "change.edit-session-#{@id}", (e) =>
@@ -54,10 +55,16 @@ 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)
+ anchor.destroy() for anchor in @getAnchors()
+ anchorRange.destroy() for anchorRange in @getAnchorRanges()
serialize: ->
buffer: @buffer.getPath()
@@ -88,14 +95,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()
@@ -268,8 +269,11 @@ class EditSession
getAnchors: ->
new Array(@anchors...)
- addAnchor: (options) ->
- anchor = new Anchor(this, options)
+ getAnchorRanges: ->
+ new Array(@anchorRanges...)
+
+ addAnchor: (options={}) ->
+ anchor = @buffer.addAnchor(_.extend({editSession: this}, options))
@anchors.push(anchor)
anchor
@@ -279,13 +283,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) ->
@@ -317,19 +324,27 @@ class EditSession
addSelectionForBufferRange: (bufferRange, options) ->
@addCursor().selection.setBufferRange(bufferRange, options)
+ @mergeIntersectingSelections()
setSelectedBufferRange: (bufferRange, options) ->
- @clearSelections()
- @getLastSelection().setBufferRange(bufferRange, options)
+ @setSelectedBufferRanges([bufferRange], options)
+
+ setSelectedBufferRanges: (bufferRanges, options={}) ->
+ throw new Error("Passed an empty array to setSelectedBufferRanges") unless bufferRanges.length
- setSelectedBufferRanges: (bufferRanges, options) ->
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)
if selections[i]
selections[i].setBufferRange(bufferRange, options)
else
@addSelectionForBufferRange(bufferRange, options)
- @mergeIntersectingSelections()
+ @mergeIntersectingSelections(options)
removeSelection: (selection) ->
_.remove(@selections, selection)
@@ -340,6 +355,9 @@ class EditSession
selection.destroy()
lastSelection.clear()
+ clearAllSelections: ->
+ selection.destroy() for selection in @getSelections()
+
getSelections: -> new Array(@selections...)
getSelection: (index) ->
diff --git a/src/app/editor.coffee b/src/app/editor.coffee
index c71be0e67..2727f0a56 100644
--- a/src/app/editor.coffee
+++ b/src/app/editor.coffee
@@ -337,7 +337,8 @@ class Editor extends View
@calculateDimensions()
@hiddenInput.width(@charWidth)
@setSoftWrapColumn() if @activeEditSession.getSoftWrap()
- $(window).on "resize.editor#{@id}", => @updateRenderedLines()
+ $(window).on "resize.editor#{@id}", =>
+ @updateRenderedLines()
@focus() if @isFocused
@renderWhenAttached()
@@ -393,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: ->
@@ -569,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/file.coffee b/src/app/file.coffee
index ef87f6afb..42baf5e7b 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()
setPath: (@path) ->
diff --git a/src/app/project.coffee b/src/app/project.coffee
index 40ddfc523..b2aa0f006 100644
--- a/src/app/project.coffee
+++ b/src/app/project.coffee
@@ -1,11 +1,12 @@
fs = require 'fs'
_ = require 'underscore'
$ = require 'jquery'
-
+Range = require 'range'
Buffer = require 'buffer'
EditSession = require 'edit-session'
EventEmitter = require 'event-emitter'
Directory = require 'directory'
+ChildProcess = require 'child-process'
module.exports =
class Project
@@ -15,13 +16,22 @@ class Project
autoIndent: null
softTabs: null
softWrap: null
+ ignoredPathRegexes: null
constructor: (path) ->
@setPath(path)
@editSessions = []
+ @buffers = []
@setTabText(' ')
@setAutoIndent(true)
@setSoftTabs(true)
+ @ignoredPathRegexes = [
+ /\.DS_Store$/
+ /(^|\/)\.git(\/|$)/
+ ]
+
+ destroy: ->
+ editSession.destroy() for editSession in @getEditSessions()
getPath: ->
@rootDirectory?.path
@@ -54,7 +64,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] == '/'
@@ -75,14 +88,8 @@ class Project
getSoftWrap: -> @softWrap
setSoftWrap: (@softWrap) ->
- open: (filePath, editSessionOptions={}) ->
- if filePath?
- filePath = @resolve(filePath)
- buffer = @bufferWithPath(filePath) ? @buildBuffer(filePath)
- else
- buffer = @buildBuffer()
-
- @buildEditSession(buffer, editSessionOptions)
+ buildEditSessionForPath: (filePath, editSessionOptions={}) ->
+ @buildEditSession(@bufferForPath(filePath), editSessionOptions)
buildEditSession: (buffer, editSessionOptions) ->
options = _.extend(@defaultEditSessionOptions(), editSessionOptions)
@@ -99,22 +106,11 @@ class Project
softTabs: @getSoftTabs()
softWrap: @getSoftWrap()
- destroy: ->
- for editSession in _.clone(@editSessions)
- @removeEditSession(editSession)
+ getEditSessions: ->
+ new Array(@editSessions...)
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 = []
@@ -123,7 +119,64 @@ class Project
buffers
- bufferWithPath: (path) ->
- return editSession.buffer for editSession in @editSessions when editSession.buffer.getPath() == path
+ bufferForPath: (filePath) ->
+ if filePath?
+ filePath = @resolve(filePath)
+ buffer = _.find @buffers, (buffer) -> buffer.getPath() == filePath
+ buffer or @buildBuffer(filePath)
+ else
+ @buildBuffer()
+
+ buildBuffer: (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')
+ command = "#{require.resolve('ag')} --ackmate \"#{regex.source}\" \"#{@getPath()}\""
+ bufferedData = ""
+
+ 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')
+ 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
diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee
index d066bd677..2f9e4b9bd 100644
--- a/src/app/root-view.coffee
+++ b/src/app/root-view.coffee
@@ -96,8 +96,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.buildEditSessionForPath(path)
+ editor = new Editor({editSession})
pane = new Pane(editor)
@panes.append(pane)
if changeFocus
@@ -105,23 +106,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.buildEditSessionForPath(path)
+ activeEditor.edit(editSession)
+ editSession
editorFocused: (editor) ->
@makeEditorActive(editor) if @panes.containsElement(editor)
@@ -144,7 +146,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 = []
@@ -163,6 +165,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/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'
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/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'
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 b781cff31..bf00695d0 100644
--- a/src/extensions/command-panel/command-panel.coffee
+++ b/src/extensions/command-panel/command-panel.coffee
@@ -1,7 +1,8 @@
-{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'
+PreviewList = require 'command-panel/preview-list'
Editor = require 'editor'
{SyntaxError} = require('pegjs').parser
@@ -16,6 +17,9 @@ class CommandPanel extends View
else
@instance = new CommandPanel(rootView)
+ @deactivate: ->
+ @instance.destroy()
+
@serialize: ->
text: @instance.miniEditor.getText()
visible: @instance.hasParent()
@@ -25,23 +29,29 @@ class CommandPanel extends View
commandPanel.attach(state.text) if state.visible
commandPanel
- @content: ->
+ @content: (rootView) ->
@div class: 'command-panel', =>
- @div ':', class: 'prompt', outlet: 'prompt'
- @subview 'miniEditor', new Editor(mini: true)
+ @subview 'previewList', new PreviewList(rootView)
+ @div class: 'prompt-and-editor', =>
+ @div ':', class: 'prompt', outlet: 'prompt'
+ @subview 'miniEditor', new Editor(mini: true)
commandInterpreter: null
history: null
historyIndex: 0
initialize: (@rootView)->
- @commandInterpreter = new CommandInterpreter()
+ @commandInterpreter = new CommandInterpreter(@rootView.project)
@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()
@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()
@@ -50,6 +60,11 @@ class CommandPanel extends View
@miniEditor.on 'move-up', => @navigateBackwardInHistory()
@miniEditor.on 'move-down', => @navigateForwardInHistory()
+ @previewList.hide()
+
+ destroy: ->
+ @previewList.destroy()
+
toggle: ->
if @miniEditor.isFocused
@detach()
@@ -58,30 +73,46 @@ class CommandPanel extends View
@attach() unless @hasParent()
@miniEditor.focus()
+ togglePreview: ->
+ if @previewList.is(':focus')
+ @previewList.hide()
+ @detach()
+ @rootView.focus()
+ else
+ @attach() unless @hasParent()
+ if @previewList.hasOperations()
+ @previewList.show().focus()
+ else
+ @miniEditor.focus()
+
attach: (text='') ->
@rootView.vertical.append(this)
@miniEditor.focus()
@miniEditor.setText(text)
- @prompt.css 'font', @miniEditor.css('font')
+ @miniEditor.setCursorBufferPosition([0, Infinity])
detach: ->
@rootView.focus()
+ @previewList.hide()
super
execute: (command = @miniEditor.getText()) ->
try
- @commandInterpreter.eval(@rootView.getActiveEditor(), command)
+ @commandInterpreter.eval(command, @rootView.getActiveEditSession()).done (operationsToPreview) =>
+ @history.push(command)
+ @historyIndex = @history.length
+ if operationsToPreview?.length
+ @previewList.populate(operationsToPreview)
+ @previewList.focus()
+ else
+ @detach()
catch error
- if error instanceof SyntaxError
+ if error.name is "SyntaxError"
@flashError()
return
else
throw error
- @history.push(command)
- @historyIndex = @history.length
- @detach()
-
navigateBackwardInHistory: ->
return if @historyIndex == 0
@historyIndex--
@@ -93,10 +124,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.pegjs b/src/extensions/command-panel/commands.pegjs
index 1a3b92307..7761003eb 100644
--- a/src/extensions/command-panel/commands.pegjs
+++ b/src/extensions/command-panel/commands.pegjs
@@ -1,12 +1,14 @@
{
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')
+ var SelectAllMatchesInProject = require('command-panel/commands/select-all-matches-in-project')
}
start = expressions:(expression+) {
@@ -19,21 +21,22 @@ 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
= 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]* {
@@ -43,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/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..b15c07d63 100644
--- a/src/extensions/command-panel/commands/address.coffee
+++ b/src/extensions/command-panel/commands/address.coffee
@@ -1,8 +1,17 @@
Command = require 'command-panel/commands/command'
+Operation = require 'command-panel/operation'
+$ = require 'jquery'
module.exports =
class Address extends Command
- execute: (editor, currentRange) ->
- [@getRange(editor, currentRange)]
+ compile: (project, buffer, ranges) ->
+ deferred = $.Deferred()
+ deferred.resolve ranges.map (range) =>
+ new Operation
+ project: project
+ buffer: buffer
+ bufferRange: @getRange(buffer, range)
+
+ deferred.promise()
isAddress: -> true
diff --git a/src/extensions/command-panel/commands/command.coffee b/src/extensions/command-panel/commands/command.coffee
index 5c6487384..181def85b 100644
--- a/src/extensions/command-panel/commands/command.coffee
+++ b/src/extensions/command-panel/commands/command.coffee
@@ -3,4 +3,5 @@ _ = require 'underscore'
module.exports =
class Command
isAddress: -> false
- restoreSelections: 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 90323ef19..395c4ce11 100644
--- a/src/extensions/command-panel/commands/composite-command.coffee
+++ b/src/extensions/command-panel/commands/composite-command.coffee
@@ -1,23 +1,36 @@
_ = require 'underscore'
+$ = require 'jquery'
module.exports =
class CompositeCommand
constructor: (@subcommands) ->
- execute: (editor) ->
- initialRanges = editor.getSelectedBufferRanges()
- for command in @subcommands
- newRanges = []
- currentRanges = editor.getSelectedBufferRanges()
- for currentRange in currentRanges
- newRanges.push(command.execute(editor, currentRange)...)
+ execute: (project, editSession) ->
+ currentRanges = editSession?.getSelectedBufferRanges()
+ @executeCommands(@subcommands, project, editSession, currentRanges)
- for range in newRanges
- for row in [range.start.row..range.end.row]
- editor.destroyFoldsContainingBufferRow(row)
+ executeCommands: (commands, project, editSession, ranges) ->
+ deferred = $.Deferred()
+ [currentCommand, remainingCommands...] = commands
- editor.setSelectedBufferRanges(newRanges)
- editor.setSelectedBufferRanges(initialRanges) if command.restoreSelections
+ 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
+ if currentCommand.previewOperations
+ deferred.resolve(operations)
+ 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/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-in-project.coffee b/src/extensions/command-panel/commands/select-all-matches-in-project.coffee
new file mode 100644
index 000000000..6e09abbe9
--- /dev/null
+++ b/src/extensions/command-panel/commands/select-all-matches-in-project.coffee
@@ -0,0 +1,24 @@
+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(
+ 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 0c8163e5f..fb5017f25 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'
-Range = require 'range'
+Operation = require 'command-panel/operation'
+$ = require 'jquery'
module.exports =
class SelectAllMatches extends Command
@@ -8,8 +9,15 @@ class SelectAllMatches extends Command
constructor: (pattern) ->
@regex = new RegExp(pattern, 'g')
- execute: (editor, currentRange) ->
- rangesToSelect = []
- editor.scanInRange @regex, currentRange, (match, range) ->
- rangesToSelect.push(range)
- rangesToSelect
+ compile: (project, buffer, ranges) ->
+ deferred = $.Deferred()
+ operations = []
+ for range in ranges
+ buffer.scanInRange @regex, range, (match, 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 4ba6f2ff9..403cb3731 100644
--- a/src/extensions/command-panel/commands/substitution.coffee
+++ b/src/extensions/command-panel/commands/substitution.coffee
@@ -1,16 +1,28 @@
Command = require 'command-panel/commands/command'
+Operation = require 'command-panel/operation'
+$ = require 'jquery'
module.exports =
class Substitution extends Command
regex: null
replacementText: null
- restoreSelections: true
+ preserveSelections: true
constructor: (pattern, replacementText, options) ->
@replacementText = replacementText
@regex = new RegExp(pattern, options.join(''))
- execute: (editor, currentRange) ->
- editor.scanInRange @regex, currentRange, (match, matchRange, { replace }) =>
- replace(@replacementText)
- [currentRange]
+ compile: (project, buffer, ranges) ->
+ deferred = $.Deferred()
+ operations = []
+ for range in ranges
+ buffer.scanInRange @regex, range, (match, matchRange, { replace }) =>
+ operations.push(new Operation(
+ project: project
+ buffer: buffer
+ bufferRange: matchRange
+ newText: @replacementText
+ preserveSelection: true
+ ))
+ deferred.resolve(operations)
+ deferred.promise()
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..f40b6d377
--- /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: ->
+ new Range([0, 0], [0, 0])
+
+ isRelative: -> false
diff --git a/src/extensions/command-panel/keymap.coffee b/src/extensions/command-panel/keymap.coffee
index 825394d8a..8b515ddcb 100644
--- a/src/extensions/command-panel/keymap.coffee
+++ b/src/extensions/command-panel/keymap.coffee
@@ -1,11 +1,11 @@
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'
-window.keymap.bindKeys '.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'
@@ -14,3 +14,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'
diff --git a/src/extensions/command-panel/operation.coffee b/src/extensions/command-panel/operation.coffee
new file mode 100644
index 000000000..e2efc4f54
--- /dev/null
+++ b/src/extensions/command-panel/operation.coffee
@@ -0,0 +1,30 @@
+{$$$} = require 'space-pen'
+
+module.exports =
+class Operation
+ constructor: ({@project, @buffer, bufferRange, @newText, @preserveSelection}) ->
+ @buffer.retain()
+ @anchorRange = @buffer.addAnchorRange(bufferRange)
+
+ getPath: ->
+ @project.relativize(@buffer.getPath())
+
+ getBufferRange: ->
+ @anchorRange.getBufferRange()
+
+ execute: (editSession) ->
+ @buffer.change(@getBufferRange(), @newText) if @newText
+ editSession.addSelectionForBufferRange(@getBufferRange()) unless @preserveSelection
+
+ preview: ->
+ range = @anchorRange.getBufferRange()
+ 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()
+ @anchorRange.destroy()
\ No newline at end of file
diff --git a/src/extensions/command-panel/preview-list.coffee b/src/extensions/command-panel/preview-list.coffee
new file mode 100644
index 000000000..57828d7d3
--- /dev/null
+++ b/src/extensions/command-panel/preview-list.coffee
@@ -0,0 +1,82 @@
+$ = require 'jquery'
+{$$$, View} = require 'space-pen'
+
+module.exports =
+class PreviewList extends View
+ @content: ->
+ @ol class: 'preview-list', tabindex: -1, ->
+
+ selectedOperationIndex: 0
+ operations: null
+
+ initialize: (@rootView) ->
+ @on 'move-down', => @selectNextOperation()
+ @on 'move-up', => @selectPreviousOperation()
+ @on 'command-panel:execute', => @executeSelectedOperation()
+
+ @on 'mousedown', 'li', (e) =>
+ @setSelectedOperationIndex(parseInt($(e.target).closest('li').data('index')))
+ @executeSelectedOperation()
+
+ destroy: ->
+ @destroyOperations() if @operations
+
+ hasOperations: -> @operations?
+
+ populate: (operations) ->
+ @destroyOperations() if @operations
+ @operations = operations
+ @empty()
+ @html $$$ ->
+ for operation, index in operations
+ {prefix, suffix, match} = operation.preview()
+ @li 'data-index': index, =>
+ @span operation.getPath(), outlet: "path", class: "path"
+ @span outlet: "preview", class: "preview", =>
+ @span prefix
+ @span match, class: 'match'
+ @span suffix
+
+ @setSelectedOperationIndex(0)
+ @show()
+
+ selectNextOperation: ->
+ @setSelectedOperationIndex(@selectedOperationIndex + 1)
+
+ 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
+
+ executeSelectedOperation: ->
+ operation = @getSelectedOperation()
+ editSession = @rootView.open(operation.getPath())
+ operation.execute(editSession)
+ @rootView.focus()
+ false
+
+ getOperations: ->
+ new Array(@operations...)
+
+ destroyOperations: ->
+ operation.destroy() for operation in @getOperations()
+ @operations = null
+
+ 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/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
diff --git a/src/stdlib/child-process.coffee b/src/stdlib/child-process.coffee
index a724439e0..9bddda62e 100644
--- a/src/stdlib/child-process.coffee
+++ b/src/stdlib/child-process.coffee
@@ -1,50 +1,35 @@
# 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()
- # make a task
- task = OSX.NSTask.alloc.init
+ if options.bufferLines
+ options.stdout = @bufferLines(options.stdout) if options.stdout
+ options.stderr = @bufferLines(options.stderr) if options.stderr
- # try to use their login shell
- task.setLaunchPath "/bin/bash"
+ $native.exec command, options, (exitStatus, 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
- # set stdin to /dev/null
- task.setStandardInput OSX.NSFileHandle.fileHandleWithNullDevice
+ deferred
- # -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()
+ @bufferLines: (callback) ->
+ buffered = ""
+ (data) ->
+ buffered += data
+ lastNewlineIndex = buffered.lastIndexOf('\n')
+ if lastNewlineIndex >= 0
+ callback(buffered.substring(0, lastNewlineIndex + 1))
+ buffered = buffered.substring(lastNewlineIndex + 1)
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/static/command-panel.css b/static/command-panel.css
index 3b624029b..31801e14b 100644
--- a/static/command-panel.css
+++ b/static/command-panel.css
@@ -2,6 +2,44 @@
width: 100%;
background: #515151;
padding: 3px;
+}
+
+.command-panel .preview-list {
+ max-height: 300px;
+ overflow: auto;
+ margin-bottom: 3px;
+ position: relative;
+ 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;
+ margin-right: 1ex;
+}
+
+.command-panel .preview-list .preview {
+ color: #f6f3e8;
+}
+
+.command-panel .preview-list .preview .match {
+ background-color: rgba(255,255,255,.25);
+ padding: 1px;
+}
+
+.command-panel .prompt-and-editor {
display: -webkit-box;
}
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"Defining your own types">.
+
+
+=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"Defining your own types">.
+
+=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;
diff --git a/vendor/ag b/vendor/ag
new file mode 100755
index 000000000..63e16c228
Binary files /dev/null and b/vendor/ag differ