Initial commit

This commit is contained in:
Allan Odgaard
2012-08-09 16:25:56 +02:00
commit 9894969e67
1103 changed files with 215580 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
build
build.ninja
cache
Applications/TextMate/resources/DefaultBundles.tbz

21
.gitmodules vendored Normal file
View File

@@ -0,0 +1,21 @@
[submodule "bin/CxxTest"]
path = bin/CxxTest
url = git://github.com/textmate/cxxtest.git
[submodule "vendor/MASPreferences/vendor"]
path = vendor/MASPreferences/vendor
url = git://github.com/textmate/MASPreferences.git
[submodule "rmate"]
path = rmate
url = git://github.com/textmate/rmate.git
[submodule "vendor/MGScopeBar/vendor"]
path = vendor/MGScopeBar/vendor
url = git://github.com/textmate/MGScopeBar.git
[submodule "Applications/TextMate/icons"]
path = Applications/TextMate/icons
url = git://github.com/textmate/document-icons.git
[submodule "PlugIns/dialog-1.x"]
path = PlugIns/dialog-1.x
url = git://github.com/textmate/dialog-1.x.git
[submodule "PlugIns/dialog"]
path = PlugIns/dialog
url = git://github.com/textmate/dialog.git

40
.tm_properties Normal file
View File

@@ -0,0 +1,40 @@
# Settings
projectDirectory = "$CWD"
windowTitle = "$TM_DISPLAYNAME — ${CWD/^.*\///} ($TM_SCM_BRANCH)"
excludeInFileChooser = "{$exclude,*.xib}"
# Variables
TM_ORGANIZATION_NAME = 'MacroMates'
TM_TODO_IGNORE = '/(disabled(-src)?|onig-.*|build|cache|CxxTest|(FScript|BWToolkitFramework).framework)/'
TM_CXX_FLAGS = '$TM_CXX_FLAGS -I"$CWD/Shared/include" -I"${builddir:-$HOME/build/TextMate}/include"'
TM_OBJCXX_FLAGS = '$TM_OBJCXX_FLAGS -I"$CWD/Shared/include" -I"${builddir:-$HOME/build/TextMate}/include"'
TM_NINJA_FILE = '${CWD}/build.ninja'
TM_NINJA_TARGET = 'TextMate/run'
[ "source.c++, source.objc++" ]
tabSize = 3
[ target ]
fileType = "source.tm-properties"
[ *.{h,pch} ]
fileType = "source.objc++"
[ Makefile.* ]
fileType = "source.makefile"
[ attr.untitled ]
fileType = 'source.c++'
[ "tests/*.{cc,mm}" ]
scopeAttributes = 'attr.project.ninja attr.test.cxxtest'
TM_NINJA_TARGET = '${TM_FILEPATH/^.*?([^\/]*)\/tests\/.*$/$1/}'
[ "tests/t_*.mm" ]
GUI_TESTS = '${TM_FILENAME/^t_(.*)\.mm$/$1/}'
[ "rmate/*" ]
TM_MAKE_TARGET = rmate

View File

@@ -0,0 +1 @@
TM_NINJA_TARGET = '${TM_DIRECTORY/^.*\/Applications\/([^\/]+)(\/.*)?$/$1\/run/}'

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,20 @@
{ CFBundleName = "${TARGET_NAME}";
CFBundleVersion = "1.0";
CFBundleIdentifier = "org.example.${TARGET_NAME}";
CFBundleExecutable = "${TARGET_NAME}";
CFBundleIconFile = "";
// CFBundleHelpBookName
// CFBundleHelpBookFolder
// LSMinimumSystemVersion
// NSContactsUsageDescription
CFBundleDevelopmentRegion = "English";
CFBundleInfoDictionaryVersion = "6.0";
CFBundlePackageType = "APPL";
CFBundleSignature = "????";
NSMainNibFile = "MainMenu";
NSPrincipalClass = "NSApplication";
}

View File

@@ -0,0 +1,4 @@
@interface MyView : NSView
{
}
@end

View File

@@ -0,0 +1,8 @@
#import "MyView.h"
@implementation MyView
- (void)drawRect:(NSRect)aRect
{
NSEraseRect(aRect);
}
@end

View File

@@ -0,0 +1,7 @@
#import <OakSystem/application.h>
int main (int argc, char const* argv[])
{
oak::application_t app(argc, argv);
return NSApplicationMain(argc, argv);
}

View File

@@ -0,0 +1,4 @@
SOURCES = src/*.{cc,mm}
CP_Resources = resources/*
LINK += OakFoundation OakAppKit OakSystem
FRAMEWORKS = Cocoa

View File

@@ -0,0 +1,98 @@
#ifndef OAK_CFXX_H_PA9RZ6MB
#define OAK_CFXX_H_PA9RZ6MB
namespace cf
{
template <typename T = CFTypeRef>
struct value
{
value (T value = NULL) : cf_value(value) { retain(); }
value (value const& rhs) : cf_value(rhs.cf_value) { retain(); }
~value () { release(); }
value& operator= (value const& rhs) { reset(rhs.cf_value); return *this; }
void reset (T value = NULL)
{
if(value != cf_value)
{
release();
cf_value = value;
retain();
}
}
EXPLICIT operator bool () const { return cf_value != NULL; }
operator T () const { return cf_value; }
operator T () { return cf_value; }
protected:
T cf_value;
private:
void retain () { if(cf_value) CFRetain(cf_value); }
void release () { if(cf_value) CFRelease(cf_value); }
};
struct data : value<CFDataRef>
{
data (void const* first, void const* last) { reset(CFDataCreate(kCFAllocatorDefault, (UInt8*)first, (UInt8*)last - (UInt8*)first)); }
};
struct string : value<CFStringRef>
{
string (const char* str) { reset(CFStringCreateWithBytes(kCFAllocatorDefault, (UInt8*)str, strlen(str), kCFStringEncodingUTF8, false)); }
string (std::string const& str) { reset(CFStringCreateWithBytes(kCFAllocatorDefault, (UInt8*)str.data(), str.size(), kCFStringEncodingUTF8, false)); }
};
struct dict_helper;
struct dictionary : value<CFMutableDictionaryRef>
{
dictionary () { reset(CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks)); }
dict_helper operator[] (string const& key);
dict_helper operator[] (const char* key);
};
struct dict_helper
{
cf::value<> operator= (CFTypeRef const& value)
{
CFDictionarySetValue(dict, key, value);
return value;
}
private:
friend struct dictionary;
dict_helper (dictionary const& dict, string const& key) : dict(dict), key(key) { }
dictionary dict;
string key;
};
inline dict_helper dictionary::operator[] (string const& key) { return dict_helper(*this, key); }
inline dict_helper dictionary::operator[] (const char* key) { return dict_helper(*this, cf::string(key)); }
struct array : value<CFMutableArrayRef>
{
array () { reset(CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks)); }
size_t size () const { return CFArrayGetCount(cf_value); }
bool empty () const { return size() == 0; }
array& operator<< (CFTypeRef const& value)
{
CFArrayAppendValue(cf_value, value);
return *this;
}
};
struct number : value<CFNumberRef>
{
number (int flag)
{
reset(CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &flag));
}
};
} /* cf */
#endif /* end of include guard: OAK_CFXX_H_PA9RZ6MB */

View File

@@ -0,0 +1,126 @@
#include "install.h"
#include "CFxx.h"
#include <text/format.h>
#include <io/io.h>
#include <authorization/constants.h>
static const char* kPlistFormatString =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
"<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"
"<plist version=\"1.0\">\n"
" <dict>\n"
" <key>Disabled</key>\n"
" <false/>\n"
" <key>Label</key>\n"
" <string>%s</string>\n"
" <key>OnDemand</key>\n"
" <true/>\n"
" <key>ProgramArguments</key>\n"
" <array>\n"
" <string>%s</string>\n"
" </array>\n"
" <key>ServiceIPC</key>\n"
" <true/>\n"
" <key>Sockets</key>\n"
" <dict>\n"
" <key>MasterSocket</key>\n"
" <dict>\n"
" <key>SockFamily</key>\n"
" <string>Unix</string>\n"
" <key>SockPathName</key>\n"
" <string>%s</string>\n"
" <key>SockPathMode</key>\n"
" <integer>438</integer>\n"
" </dict>\n"
" </dict>\n"
" </dict>\n"
"</plist>\n"; // job label, tool path, socket
static std::string plist_content ()
{
return text::format(kPlistFormatString, kAuthJobName, kAuthToolPath, kAuthSocketPath);
}
static void launch_control (char const* command, std::string const& argument)
{
pid_t pid = vfork();
if(pid == 0)
{
execl("/bin/launchctl", "/bin/launchctl", command, argument.c_str(), NULL);
perror("/bin/launchctl failed");
_exit(1);
}
int status = 0;
waitpid(pid, &status, 0);
}
static void remove_policy ()
{
AuthorizationRef authRef;
if(noErr == AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, kAuthorizationFlagDefaults, &authRef))
{
AuthorizationRightRemove(authRef, kAuthRightName);
AuthorizationFree(authRef, kAuthorizationFlagDefaults);
}
}
static void add_policy ()
{
remove_policy();
AuthorizationRef authRef;
if(noErr == AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, kAuthorizationFlagDefaults, &authRef))
{
cf::dictionary rightDefinition;
rightDefinition["class"] = cf::string("user");
rightDefinition["group"] = cf::string("admin");
rightDefinition["allow-root"] = CFRetain(kCFBooleanTrue);
rightDefinition["timeout"] = cf::number(900);
int errStatus = AuthorizationRightSet(authRef, kAuthRightName, rightDefinition, NULL /* description key */, NULL, NULL);
if(errStatus != noErr)
fprintf(stderr, "*** error adding policy (%s): %d\n", kAuthRightName, errStatus);
AuthorizationFree(authRef, kAuthorizationFlagDefaults);
}
}
int install_tool (std::string const& toolPath)
{
if(toolPath.empty() || toolPath[0] != '/')
{
fprintf(stderr, "need to be run with absolute path\n");
abort();
}
setuid(geteuid());
setgid(getegid());
if(!path::make_dir(path::parent(kAuthToolPath)))
return 1;
path::remove(kAuthToolPath);
if(!path::copy(toolPath, kAuthToolPath))
return 1;
chown(kAuthToolPath, 0, 0);
if(path::exists(kAuthPlistPath))
launch_control("unload", kAuthPlistPath);
if(!path::set_content(kAuthPlistPath, plist_content()))
return 1;
launch_control("load", kAuthPlistPath);
add_policy();
return 0;
}
int uninstall_tool ()
{
remove_policy();
if(path::exists(kAuthPlistPath))
{
launch_control("unload", kAuthPlistPath);
path::remove(kAuthPlistPath);
}
path::remove(kAuthToolPath);
return 0;
}

View File

@@ -0,0 +1,7 @@
#ifndef PRIVILEGED_TOOL_INSTALL_H_4IAOPZ4H
#define PRIVILEGED_TOOL_INSTALL_H_4IAOPZ4H
int install_tool (std::string const& toolPath);
int uninstall_tool ();
#endif /* end of include guard: PRIVILEGED_TOOL_INSTALL_H_4IAOPZ4H */

View File

@@ -0,0 +1,39 @@
#include "launchd.h"
#include <launch.h> // There is no <x-man-page://3/launch> page <rdar://7072543>
#include <oak/debug.h>
OAK_DEBUG_VAR(AuthServer_Launchd);
int get_socket (launch_data_t dict, char const* name)
{
launch_data_t array = launch_data_dict_lookup(dict, name);
if(array == NULL || (launch_data_get_type(array) == LAUNCH_DATA_ARRAY && launch_data_array_get_count(array) == 0))
{
D(DBF_AuthServer_Launchd, bug("no activity for %s\n", name););
return -1;
}
assert(array != NULL && launch_data_get_type(array) == LAUNCH_DATA_ARRAY);
D(DBF_AuthServer_Launchd, bug("handle %zu sockets (%s)", launch_data_array_get_count(array), name););
launch_data_t fdData = launch_data_array_get_index(array, 0);
D(DBF_AuthServer_Launchd, bug("data %p", fdData););
assert(fdData != NULL && launch_data_get_type(fdData) == LAUNCH_DATA_FD);
int fd = launch_data_get_fd(fdData);
D(DBF_AuthServer_Launchd, bug("fd %d", fd););
assert(fd >= 0);
return fd;
}
int launchd_sockets ()
{
launch_data_t req = launch_data_new_string(LAUNCH_KEY_CHECKIN);
assert(req != NULL);
launch_data_t res = launch_msg(req);
assert(res != NULL && launch_data_get_type(res) == LAUNCH_DATA_DICTIONARY); // LAUNCH_DATA_ERRNO
launch_data_t allSockets = launch_data_dict_lookup(res, LAUNCH_JOBKEY_SOCKETS);
assert(allSockets != NULL && launch_data_get_type(allSockets) == LAUNCH_DATA_DICTIONARY);
return get_socket(allSockets, "MasterSocket");
}

View File

@@ -0,0 +1,6 @@
#ifndef LAUNCHD_H_SP1GJCZ
#define LAUNCHD_H_SP1GJCZ
int launchd_sockets ();
#endif /* end of include guard: LAUNCHD_H_SP1GJCZ */

View File

@@ -0,0 +1,216 @@
#include "launchd.h"
#include "install.h"
#include <authorization/connection.h>
#include <authorization/constants.h>
#include <authorization/authorization.h>
#include <io/io.h>
#include <oak/debug.h>
OAK_DEBUG_VAR(AuthServer);
static double const AppVersion = 1.0;
static size_t const AppRevision = APP_REVISION;
extern char* optarg;
extern int optind;
static bool running = true;
static void handle_signal (int theSignal)
{
D(DBF_AuthServer, bug("%s\n", strsignal(theSignal)););
running = false;
}
static void reap_children (int theSignal)
{
D(DBF_AuthServer, bug("%s\n", strsignal(theSignal)););
pid_t pid;
int status;
while((pid = waitpid(-1, &status, WNOHANG)) > 0)
{
D(DBF_AuthServer, bug("child %d terminated\n", pid););
}
}
static void version ()
{
fprintf(stdout, "%1$s %2$.1f (" COMPILE_DATE " revision %3$zu)\n", getprogname(), AppVersion, AppRevision);
}
static void usage (FILE* io = stdout)
{
fprintf(io,
"%1$s %2$.1f (" COMPILE_DATE " revision %3$zu)\n"
"Usage: %1$s [-siuhv]\n"
"Description:\n"
" Server for authenticated file system operations.\n"
"Options:\n"
" -s, --server Run server.\n"
" -i, --install Install server.\n"
" -u, --uninstall Uninstall server.\n"
" -h, --help Show this information.\n"
" -v, --version Print version information.\n"
"\n", getprogname(), AppVersion, AppRevision
);
}
static int setup_socket ()
{
unlink(kAuthSocketPath);
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr = { 0, AF_UNIX, kAuthSocketPath };
addr.sun_len = SUN_LEN(&addr);
int rc = bind(fd, (sockaddr*)&addr, sizeof(addr));
chmod(kAuthSocketPath, S_IRWXU|S_IRWXG|S_IRWXO);
assert(rc != -1);
rc = listen(fd, 5);
assert(rc != -1);
return fd;
}
static void handle_connection (int fd)
{
D(DBF_AuthServer, bug("child %d\n", getpid()););
connection_t conn(fd);
conn << "AuthServer" << kAuthServerMajor << kAuthServerMinor;
std::string command;
conn >> command;
D(DBF_AuthServer, bug("> %s\n", command.c_str()););
if(command == "auth")
{
std::string authString;
conn >> authString;
D(DBF_AuthServer, bug("> auth: %s\n", authString.c_str()););
osx::authorization_t auth(authString);
if(!auth.check_right(kAuthRightName))
{
D(DBF_AuthServer, bug("failed authentication\n"););
return;
}
}
else
{
D(DBF_AuthServer, bug("no authentication\n"););
return;
}
std::string action;
conn >> action;
D(DBF_AuthServer, bug("> %s\n", action.c_str()););
if(action == "read")
{
std::string path;
conn >> path;
conn << path::content(path) << path::attributes(path);
}
else if(action == "write")
{
std::string path, content, error = NULL_STR;
std::map<std::string, std::string> attributes;
conn >> path >> content >> attributes;
if(!path::set_content(path, content))
error = text::format("set_content() failed: %s", strerror(errno));
else if(!path::set_attributes(path, attributes))
error = text::format("set_attributes() failed: %s", strerror(errno));
conn << error;
}
else
{
D(DBF_AuthServer, bug("unknown action: %s\n", action.c_str()););
}
}
static void close_socket (int fd)
{
close(fd);
// unlink(kAuthSocketPath); // when running via launchd we do not want to remove the socket, since launchd will not re-create it if our server process is restarted
}
int main (int argc, char const* argv[])
{
D(DBF_AuthServer, bug("\n"););
signal(SIGINT, &handle_signal);
signal(SIGTERM, &handle_signal);
signal(SIGCHLD, &reap_children);
signal(SIGPIPE, SIG_IGN);
static struct option const longopts[] = {
{ "server", no_argument, 0, 's' },
{ "install", no_argument, 0, 'i' },
{ "uninstall", no_argument, 0, 'u' },
{ "help", no_argument, 0, 'h' },
{ "version", no_argument, 0, 'v' },
{ 0, 0, 0, 0 }
};
bool server = false, install = false, uninstall = false;
unsigned int ch;
while((ch = getopt_long(argc, (char* const*)argv, "siuhv", longopts, NULL)) != -1)
{
switch(ch)
{
case 's': server = true; break;
case 'i': install = true; break;
case 'u': uninstall = true; break;
case 'h': usage(); return 0;
case 'v': version(); return 0;
default: usage(stderr); return 1;
}
}
if(geteuid() != 0)
{
fprintf(stderr, "auth_server: must run as root\n");
abort();
}
if(install)
return install_tool(argv[0]);
else if(uninstall)
return uninstall_tool();
int fd = server ? setup_socket() : launchd_sockets();
while(running)
{
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(fd, &readfds);
int rc = select(fd+1, &readfds, NULL, NULL, NULL);
if(rc == -1)
{
D(DBF_AuthServer, bug("select: %s\n", strerror(errno)););
continue;
}
if(FD_ISSET(fd, &readfds))
{
char dummy[256];
socklen_t len = sizeof(dummy);
int newFd = accept(fd, (sockaddr*)&dummy[0], &len);
// if(fork() == 0)
{
D(DBF_AuthServer, bug("new connection\n"););
handle_connection(newFd);
// _exit(0);
}
}
}
close_socket(fd);
return 0;
}

View File

@@ -0,0 +1,2 @@
SOURCES = src/*.cc
LINK += authorization

View File

@@ -0,0 +1,2 @@
[Apple]: http://apple.com/ "Apple Inc."

View File

@@ -0,0 +1,39 @@
## Oniguruma
Copyright (c) K.Kosako <sndgk393@ybb.ne.jp>
[Oniguruma][] is licensed under the BSD license.
[Oniguruma]: http://www.geocities.jp/kosako3/oniguruma/
## MASPreferences
[MASPreferences][] is licensed under the BSD license
[MASPreferences]: https://github.com/shpakovski/MASPreferences
## MGScopeBar
Copyright (c) 2008 Matt Gemmell
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1,31 @@
exclude = "{*.{o,pyc},Icon\r,CVS,_darcs,_MTN,\{arch\},blib,*~.nib}"
include = "{.tm_properties,.htaccess}"
binary = "{*.{icns,ico,jpg,jpeg,m4v,nib,o,pdf,png,psd,pyc,rtf,tif,tiff,xib},Icon\r}"
LANG = "en_US.UTF-8"
LC_CTYPE = "en_US.UTF-8"
TM_APP_PATH = "${CWD/\/Contents\/Resources$//}"
[ "/usr/include/{**/,}*" ]
tabSize = 8
[ text ]
softWrap = true
[ .git/COMMIT_EDITMSG ]
spellChecking = true
spellingLanguage = 'en'
[ source.ruby ]
softTabs = true
tabSize = 2
[ source.python ]
softTabs = true
tabSize = 4
[ "/System/Library/Frameworks/**/Headers/**/*" ]
encoding = "MACROMAN"
[ "{BUILD,README,INSTALL,LICENSE,COPYING,TODO}" ]
fileType = "text.plain"

View File

@@ -0,0 +1,286 @@
<?xml version="1.0" encoding="UTF-8"?>
<archive type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="7.10">
<data>
<int key="IBDocument.SystemTarget">1050</int>
<string key="IBDocument.SystemVersion">10K549</string>
<string key="IBDocument.InterfaceBuilderVersion">851</string>
<string key="IBDocument.AppKitVersion">1038.36</string>
<string key="IBDocument.HIToolboxVersion">461.00</string>
<object class="NSMutableDictionary" key="IBDocument.PluginVersions">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.WebKitIBPlugin</string>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>851</string>
<string>851</string>
</object>
</object>
<object class="NSMutableArray" key="IBDocument.EditedObjectIDs">
<bool key="EncodedWithXMLCoder">YES</bool>
<integer value="1"/>
</object>
<object class="NSArray" key="IBDocument.PluginDependencies">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.WebKitIBPlugin</string>
</object>
<object class="NSMutableDictionary" key="IBDocument.Metadata">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys" id="0">
<bool key="EncodedWithXMLCoder">YES</bool>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
</object>
</object>
<object class="NSMutableArray" key="IBDocument.RootObjects" id="1000">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSCustomObject" id="1001">
<string key="NSClassName">CreditsWindowController</string>
</object>
<object class="NSCustomObject" id="1003">
<string key="NSClassName">FirstResponder</string>
</object>
<object class="NSCustomObject" id="1004">
<string key="NSClassName">NSApplication</string>
</object>
<object class="NSWindowTemplate" id="1005">
<int key="NSWindowStyleMask">15</int>
<int key="NSWindowBacking">2</int>
<string key="NSWindowRect">{{100, 200}, {600, 500}}</string>
<int key="NSWTFlags">1613234176</int>
<string key="NSWindowTitle">Credits</string>
<string key="NSWindowClass">NSWindow</string>
<nil key="NSViewClass"/>
<string key="NSWindowContentMaxSize">{1.79769e+308, 1.79769e+308}</string>
<string key="NSWindowContentMinSize">{300, 150}</string>
<object class="NSView" key="NSWindowView" id="1006">
<reference key="NSNextResponder"/>
<int key="NSvFlags">274</int>
<object class="NSMutableArray" key="NSSubviews">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="WebView" id="123711406">
<reference key="NSNextResponder" ref="1006"/>
<int key="NSvFlags">274</int>
<object class="NSMutableSet" key="NSDragTypes">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="set.sortedObjects">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>Apple HTML pasteboard type</string>
<string>Apple PDF pasteboard type</string>
<string>Apple PICT pasteboard type</string>
<string>Apple URL pasteboard type</string>
<string>Apple Web Archive pasteboard type</string>
<string>NSColor pasteboard type</string>
<string>NSFilenamesPboardType</string>
<string>NSStringPboardType</string>
<string>NeXT RTFD pasteboard type</string>
<string>NeXT Rich Text Format v1.0 pasteboard type</string>
<string>NeXT TIFF v4.0 pasteboard type</string>
<string>WebURLsWithTitlesPboardType</string>
<string>public.png</string>
<string>public.url</string>
<string>public.url-name</string>
</object>
</object>
<string key="NSFrameSize">{600, 500}</string>
<reference key="NSSuperview" ref="1006"/>
<reference key="NSNextKeyView"/>
<string key="FrameName"/>
<string key="GroupName"/>
<object class="WebPreferences" key="Preferences">
<string key="Identifier"/>
<object class="NSMutableDictionary" key="Values">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>WebKitDefaultFixedFontSize</string>
<string>WebKitDefaultFontSize</string>
<string>WebKitMinimumFontSize</string>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<integer value="12"/>
<integer value="12"/>
<integer value="1"/>
</object>
</object>
</object>
<bool key="UseBackForwardList">NO</bool>
<bool key="AllowsUndo">YES</bool>
</object>
</object>
<string key="NSFrameSize">{600, 500}</string>
<reference key="NSSuperview"/>
</object>
<string key="NSScreenRect">{{0, 0}, {1280, 778}}</string>
<string key="NSMinSize">{300, 172}</string>
<string key="NSMaxSize">{1.79769e+308, 1.79769e+308}</string>
<string key="NSFrameAutosaveName">Credits</string>
</object>
</object>
<object class="IBObjectContainer" key="IBDocument.Objects">
<object class="NSMutableArray" key="connectionRecords">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">window</string>
<reference key="source" ref="1001"/>
<reference key="destination" ref="1005"/>
</object>
<int key="connectionID">4</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">delegate</string>
<reference key="source" ref="1005"/>
<reference key="destination" ref="1001"/>
</object>
<int key="connectionID">5</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">webView</string>
<reference key="source" ref="1001"/>
<reference key="destination" ref="123711406"/>
</object>
<int key="connectionID">6</int>
</object>
</object>
<object class="IBMutableOrderedSet" key="objectRecords">
<object class="NSArray" key="orderedObjects">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBObjectRecord">
<int key="objectID">0</int>
<reference key="object" ref="0"/>
<reference key="children" ref="1000"/>
<nil key="parent"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">-2</int>
<reference key="object" ref="1001"/>
<reference key="parent" ref="0"/>
<string key="objectName">File's Owner</string>
</object>
<object class="IBObjectRecord">
<int key="objectID">-1</int>
<reference key="object" ref="1003"/>
<reference key="parent" ref="0"/>
<string key="objectName">First Responder</string>
</object>
<object class="IBObjectRecord">
<int key="objectID">-3</int>
<reference key="object" ref="1004"/>
<reference key="parent" ref="0"/>
<string key="objectName">Application</string>
</object>
<object class="IBObjectRecord">
<int key="objectID">1</int>
<reference key="object" ref="1005"/>
<object class="NSMutableArray" key="children">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference ref="1006"/>
</object>
<reference key="parent" ref="0"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">2</int>
<reference key="object" ref="1006"/>
<object class="NSMutableArray" key="children">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference ref="123711406"/>
</object>
<reference key="parent" ref="1005"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">3</int>
<reference key="object" ref="123711406"/>
<reference key="parent" ref="1006"/>
</object>
</object>
</object>
<object class="NSMutableDictionary" key="flattenedProperties">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>-1.IBPluginDependency</string>
<string>-2.IBPluginDependency</string>
<string>-3.IBPluginDependency</string>
<string>1.IBEditorWindowLastContentRect</string>
<string>1.IBPluginDependency</string>
<string>1.IBWindowTemplateEditedContentRect</string>
<string>1.NSWindowTemplate.visibleAtLaunch</string>
<string>1.WindowOrigin</string>
<string>1.editorWindowContentRectSynchronizationRect</string>
<string>1.windowTemplate.hasMinSize</string>
<string>1.windowTemplate.minSize</string>
<string>2.IBPluginDependency</string>
<string>3.IBPluginDependency</string>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>{{50, 240}, {600, 500}}</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>{{50, 240}, {600, 500}}</string>
<integer value="1"/>
<string>{196, 240}</string>
<string>{{357, 418}, {480, 270}}</string>
<integer value="1"/>
<string>{300, 150}</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.WebKitIBPlugin</string>
</object>
</object>
<object class="NSMutableDictionary" key="unlocalizedProperties">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference key="dict.sortedKeys" ref="0"/>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
</object>
</object>
<nil key="activeLocalization"/>
<object class="NSMutableDictionary" key="localizations">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference key="dict.sortedKeys" ref="0"/>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
</object>
</object>
<nil key="sourceID"/>
<int key="maxID">6</int>
</object>
<object class="IBClassDescriber" key="IBDocument.Classes">
<object class="NSMutableArray" key="referencedPartialClassDescriptions">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBPartialClassDescription">
<string key="className">CreditsWindowController</string>
<string key="superclassName">NSWindowController</string>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBUserSource</string>
<string key="minorKey"/>
</object>
</object>
</object>
</object>
<int key="IBDocument.localizationMode">0</int>
<string key="IBDocument.TargetRuntimeIdentifier">IBCocoaFramework</string>
<object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDependencies">
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin.macosx</string>
<integer value="1050" key="NS.object.0"/>
</object>
<object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDevelopmentDependencies">
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin.InterfaceBuilder3</string>
<integer value="3000" key="NS.object.0"/>
</object>
<bool key="IBDocument.PluginDeclaredDependenciesTrackSystemTargetVersion">YES</bool>
<nil key="IBDocument.LastKnownRelativeProjectPath"/>
<int key="IBDocument.defaultPropertyAccessControl">3</int>
</data>
</archive>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,80 @@
title: TextMate Help
meta: AppleTitle="TextMate 2 Help", AppleIcon="TextMate Help/images/tm_small.png"
# TextMate Help
Some non-visible stuff.
## Mouse Gestures
* ⌘-click to add a new caret.
* ⌥-click close buttons to close other tabs.
* Hold ⌃ when dropping file to text view to insert its path.
Holding ⌥ to “close other” also works in file browser or when using ⌘T (and ⌘T now does multi-select).
One exception to “⌥ closes other” is when single-clicking the icon of a bundle (e.g. `.app`) in the file browser. In this case the single-click works as “Show Package Contents”.
## Completion (⎋)
The current word suffix is taken into considuration when hiting escape.
For example in this case:
enum mark_t { kErrorMarkType, kWarningMarkType };
mark_t m = ‸MarkType;
The resulting line will become:
mark_t m = kWarning‸MarkType;
## Grammar
* Scope names are format strings and can reference captures ($0, $1, $n).
* `\G` matches end of parent rules `begin` — or end of last stacked `while`.
* `begin`/`while` rule construct.
* Includes can reference grammar#repos.
## Bundles
* Shell variables are format strings and can e.g. reference `TM_SUPPORT_PATH`.
* Completion commands have all variables set from current context.
## Discontinuous Selection
Activated by typing/deleting or using a leftward or rightward movement while a column selection is active. Alternatively use “Find All”
## Find History
Just like clipboard history: ⌃⌥⌘F.
## Other
Possible to enter e.g. `main.{cc,h}` in a Save As dialog for brace expansion (saves as first expansion, background tabs are created for further expansions).
⌘T can filter on full path by including `/`, extension by starting with `.`, can go to a line by suffixing with a line specification (see elsewhere for syntax)
Using ⌘T with find clipboard containing `«file»:«line»` will use that as default text.
## Syntax / API
* [Bundle Dependencies][]
* [Format String Syntax][]
* [Glob String Syntax][]
* [JavaScript Object][]
* [Scope Selector Syntax][]
* [Selection String Syntax][]
* [Folder Specific Properties][]
* [Non-Content Scopes][]
* [Events / Filters][]
* [mate & rmate](mate_and_rmate.html)
[Bundle Dependencies]: bundle_dependencies.html
[Format String Syntax]: format_string_syntax.html
[Glob String Syntax]: glob_string_syntax.html
[JavaScript Object]: javascript_object.html
[Scope Selector Syntax]: scope_selector_syntax.html
[Selection String Syntax]: selection_string_syntax.html
[Folder Specific Properties]: properties.html
[Non-Content Scopes]: non-content_scopes.html
[Events / Filters]: events.html

View File

@@ -0,0 +1,37 @@
# Bundle Dependencies
## Description
The following can be added to a bundle item (setting it for a bundles `info.plist` is equivalent of setting it for each item in that bundle):
require = ( { name = «name»; uuid = «uuid»; }, … );
The UUID is that of the bundle or bundle item required. The name is the name under which the required item should be available (more or less), this only make sense when the UUID is that of a grammar or bundle, see details below. The name defaults to the bundles or bundle items name, except for grammars, where it is the root scope for the grammar.
If the UUID is that of a bundle item, TextMate ensures that the item and bundle containing it, when the item with the requirement is “executed”, is available.
If the UUID is that of a bundle, TextMate ensures that the bundle is available and will set `TM_«name»_BUNDLE_SUPPORT` when “executing” the item with the requirement. It will uppercase the name provided.
## Examples
To be able to use `include = 'source.c';` in the Objective-C grammar, it should have the followung requirement:
require = (
{ name = 'source.c';
uuid = '25066DC2-6B1D-11D9-9D5B-000D93589AF6';
}
)
The Subversion bundles `info.plist` could contain the following:
require = (
{ name = 'Dialog';
uuid = 'F985E884-C6F4-4FB1-B7F6-447A72ECF267';
}
)
This ensures that the bundle providing a ruby interface to the Dialog system is available and that commands in the Subversion bundle written in ruby can do:
require "#{ENV['TM_DIALOG_BUNDLE_SUPPORT']}/lib/ui"
TextMate::UI.show_menu(…)

View File

@@ -0,0 +1,10 @@
body {
font: 12px "Lucida Grande", sans-serif;
}
pre {
background-color: #E2E2E2;
border: 1px solid #919699;
padding: 10px 20px 10px 20px;
overflow: auto;
}

View File

@@ -0,0 +1,13 @@
# Events / Filters
## Events
Presently the only event fired is `event.document.did_save`.
Catching an event is done by adding an `eventSpecifier` key to a regular command and setting the value to the event which should be catched.
An example of this is the “Make Script Executable” in the included Avian bundle. This is scoped to `source.ruby` so if you create a new ruby file and save it, it should automatically get the executable bit set.
## Filters
Filters are like events but their output is used. Examples of filters are in the included Avian bundle, there are filters to decompile AppleScript, pretty print tmCommand files, and encrypt/decrypt the content.

View File

@@ -0,0 +1,38 @@
# Format String Syntax
## In Snippets
### Placeholders
$«int»
${«int»}
${«int»:«snippet»}
${«int»/«regexp»/«format»/«options»}
${«int»|«choice 1»,…,«choice n»|} # parsed but not handled
### Code
`«code»`
## In Format Strings
$0-n
\U, \L, \E, \u, \l
\n, \t
«variables»
(?«var»:«if»:«else»}
(?«var»:«if»}
## In Both
### Variables
${«var»:?«if»:«else»}
${«var»:+«if»}
${«var»:-«else»}
${«var»:«else»}
${«var»/«regexp»/«format»/«options»}
${«var»:[/upcase][/downcase][/capitalize][/asciify]}

View File

@@ -0,0 +1,25 @@
# Glob String Syntax
\«char» -- Literal «char»
? -- Match one character
* -- Match zero or more characters¹
** -- Match zero or more path components
{«a»,«b»,«c»} -- Match «a» or «b» or «c»
[«a»«b»«c»] -- Match an «a», «b» or «c» character
[«a»-«b»] -- Match one character in the range «a»-«b»
[^«a»-«b»] -- Match one character not in the range «a»-«b»
Braces can be nested and contain other glob characters. Example:
{*.{cc,mm,h},target,Makefile,.tm_properties}
Will match these files:
source.cc
source.mm
source.h
target
Makefile
.tm_properties
¹ The asterisk will not match slashes nor a leading period.

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,52 @@
# TextMate JavaScript Object API
The object has the following methods available:
system() See below for information.
log(msg) Adds a message to the system console (using NSLog).
open(path, options) Opens a file on disk as a document in the current application.
options may be either a selection range string or a (line) number.
In addition, these properties are exposed:
busy (boolean) The busy spinner in the output window will be displayed when this is true.
progress (double, 0-1) Controls the value displayed in the determinate progress indicator.
## TextMate.system()
Also see <http://developer.apple.com/documentation/AppleApplications/Conceptual/Dashboard_ProgTopics/Articles/CommandLine.html>.
### Synchronous Operation
Example:
obj = TextMate.system("/usr/bin/id -un", null);
Result is an object with following properties:
outputString: The output of the command, as placed on stdout.
errorString: The output of the command, as placed on stderr.
status: The exit status of the command.
### Asynchronous Operation
Example:
obj = TextMate.system("/usr/bin/id -un", handler);
Handler is called when the command is finished and given an object with the following properties:
outputString: The last output of the command, as placed on stdout.
errorString: The last output of the command, as placed on stderr.
status: The exit status of the command.
Result is an object with following properties/methods:
outputString: The current string written to stdout (standard output) by the command.
errorString: The current string written to stderr (standard error output) by the command.
status: The commands exit status, as defined by the command.
onreadoutput: A function called whenever the command writes to stdout. The handler must accept a single argument; when called, the argument contains the current string placed on stdout.
onreaderror: A function called whenever the command writes to stderr. The handler must accept a single argument; when called, the argument contains the current string placed on stderr.
cancel(): Cancels the execution of the command.
write(string): Writes a string to stdin (standard input).
close(): Closes stdin (EOF).

View File

@@ -0,0 +1,11 @@
# <a name="load_encoding">Encodings</a>
TextMate will do a few tests against your file:
1. Does it have an UTF-8/16/32 BE/LE BOM?
_Do not take this to mean that UTF-8 BOMs are fine!_
2. Does it have a `com.apple.TextEncoding` extended attribute?
3. Does it have an `encoding` setting via `.tm_properties`?
4. Is it valid ASCII/UTF-8?
If all of these tests fail, it will ask you to pick the proper encoding.

View File

@@ -0,0 +1,254 @@
Title: Terminal Preferences
# <a name="terminal">mate and rmate</a>
TextMate is and will always be a modern GUI application. However, developers are often forced to walk in two worlds using both GUI and command-line tools. TextMate has always bridged the gap between these environments with its trusty command-line sidekick: `mate`.
In TextMate 2, `mate` has learned some new tricks. A new partner-in-crime has also been introduced: `rmate`.
In this article will discuss:
* How to install the upgraded `mate`
* `mate`'s new features
* What `rmate` is
* The different ways to use `rmate`
## Upgrade Time
Once you have TextMate 2, you need to make sure you refresh the command-line `mate` application. This allows you to take advantage of its new features.
The installer of this command-line utility has moved in TextMate 2. You can now find it in the _TextMate_ menu under _Preferences…_ in the _Terminal_ pane.
![The Terminal Preferences pane][preferences]
To install or upgrade `mate`, just select a Location and hit Install. You may be prompted to Replace an old version, if present.
This pane can later be used to uninstall the command if you need to do that.
Users usually install `mate` into `~/bin` if they just need it for the current user and `/usr/local/bin` if they would rather make it accessible to all users (assuming they have `/usr/local/bin` in their `PATH`). If you would like to find where your TextMate 1 version is before you upgrade, feed your Terminal this command: `which mate`.
## Your New mate
Let's get to the good stuff. What's upgraded in `mate`?
First, it's a small tweak, but you will be happy to hear that `mate` now works with `sudo`. You can use this to edit restricted access files in TextMate 2 without being bothered by an authorization dialog on save:
sudo mate /etc/hosts
Next, `mate` has picked up some new command-line options. One is `-t`, which allows you to set the type of an opened file. Personally, I like this feature for README files, which I prefer to write in Markdown. Since the name of standard files like this don't always include an extension, we can just tell TextMate 2 how to handle the file as we open it:
mate -t text.html.markdown README
Another new option is `--name`. This allows you to set `TM_DISPLAYNAME` which is typically shown in the title bar on TextMate's windows. For a good use of that, let me share a line from my Bash configuration:
export GIT_EDITOR="mate --name 'Git Commit Message' -w -l 1"
The `-w` and `-l` options have been around for a while. `-w` just tells TextMate to **w**ait for the edit to finish. That's what allows you to use the command as an external editor for tools like Git that are waiting for an answer.
`-l` just tells TextMate to place the caret on the indicated **l**ine. I force the first line here because Git has a habit of reusing commit message files and TextMate would otherwise try restore the caret to it's last location. Since that last location was probably at the end of a message that's changed or no longer present, the caret would likely be left in the middle of Git's comments. That's not too helpful for a quick edit, so `-l 1` forces the caret back to the beginning.
That leads us, finally, to the new option used above. Since Git will just use some file like `.git/COMMIT_EDITMSG`, our window isn't going to have the best title by default. This use of `--name` clears that up by telling me exactly what I'm looking at.
## Selection Strings
I mentioned before that TextMate 1's version of `mate` supported `-l`. That's true, but `-l` is far more powerful in TextMate 2 and that new power has spread to other commands.
With the old version of `mate`, `-l` was super simple. It just took a number and it took you to that line. Easy enough.
In the new version, it now takes a Selection String. These are a new tool in TextMate 2 for describing selections. Here's the formal grammar for a Selection String:
selection = «range» ('&' «range»)*
range = «pos» | «normal_range» | «column_range»
pos = «line» (':' «column»)?
normal_range = «pos» '-' «pos»
column_range = «pos» 'x' «pos»
line = [1-9][0-9]*
column = [1-9][0-9]*
OK, geeky, but what does it really mean? Let's look at some examples.
If I save the above grammar to a `selection_string_syntax.txt` file, I can open it with the following command:
mate -l 3 selection_string_syntax.txt
That would drop my caret (shown as ‸) at the beginning of the third line:
‸pos = «line» (':' «column»)?
If I prefer though, I can control where it is in the line:
mate -l 3:16 selection_string_syntax.txt
That would place my caret here:
pos = ‸«line» (':' «column»)?
Of course, we can also make a selection:
mate -l 3:32-3:38 selection_string_syntax.txt
That selects (shown between ‸ marks):
pos = «line» (':' «‸column‸»)?
But wait, is this TextMate 2 or what? We have [Multiple Carets][carets] now, dang it! How do we use more than one at once? Like this:
mate -l '3&6' selection_string_syntax.txt
That places two carets:
selection = «range» ('&' «range»)*
range = «pos» | «normal_range» | «column_range»
‸pos = «line» (':' «column»)?
normal_range = «pos» '-' «pos»
column_range = «pos» 'x' «pos»
‸line = [1-9][0-9]*
column = [1-9][0-9]*
You can also make column selections:
mate -l '4:7x5:13' selection_string_syntax.txt
That gives us:
normal‸_range‸ = «pos» '-' «pos»
column‸_range‸ = «pos» 'x' «pos»
Or you can make several unlinked selections using all of the above features:
mate -l '2:18-2:21&3:1-3:4&4:18x5:21&4:30x5:33' selection_string_syntax.txt
Which selects all occurrences of `pos` for replacement:
selection = «range» ('&' «range»)*
range = «‸pos‸» | «normal_range» | «column_range»
‸pos‸ = «line» (':' «column»)?
normal_range = «‸pos‸» '-' «‸pos‸»
column_range = «‸pos‸» 'x' «‸pos‸»
line = [1-9][0-9]*
column = [1-9][0-9]*
You get the idea.
The best news about these new Selection Strings is that they aren't just for `mate`. You can also use them in the _Go to Line_ (⌘L) and _Go to File_ (⌘T) dialogs. For that latter, you just tack a colon (`:`) onto the end of the name matching string then follow it up with a Selection String.
This means that you could respond to a warning message like the following:
main.cc:32: warning: no return statement.
by selecting `main.cc:32`, copying that to the Find clipboard with ⌘E, opening TextMate 2, calling up the _Go to File_ dialog with (⌘T), and pushing ↩ to go straight there. As this shows, _Go to File_ honors the Find clipboard when it matches this format, just to make things like this easier.
## Remote mate
In the past, TextMate has suffered with editing files on a server, but that's all changed now. If you regularly find yourself SSHed into a remote box and wanting to edit a file using TextMate on your own box, your ship has come in.
TextMate 2 now ships with an `rmate` (Ruby) script that you can drop onto servers. When you trigger `rmate` on a remote box, it will connect back to your box, allow you to edit, and update the file on the server with the changes.
Let me show you how to get `rmate` installed on a server you want edit from remotely.
### Installing rmate
First, we need to copy `rmate` up to your server. You can find a link to the script in the _Preferences…_ dialog of the _TextMate_ menu, under the _Terminal_ pane. Click that link to open the script, then finish the install with these steps:
1. Open a Terminal and type `scp` followed by a space
2. Drag the `rmate` icon out of the window title bar and drop it into your Terminal to fill in the path to the script
3. Add another space and then the server you wish to install the script on followed by a colon (`:`) and the path to install the script into
Your final command should look something like:
scp /Applications/TextMate.app/Contents/Frameworks/Preferences.framework/Versions/A/Resources/rmate example.com:/usr/local/bin
If you don't have SSH setup to automatically log you into the server, you may also need to provide the proper authentication options. If your user doesn't have permission to copy directly into a the directory you want to install into, you may need to upload the script to your user's home directory, SSH into the remote, and move it into place using `sudo`. Alternately, you could place it in `~/bin` and ensure that directory is in your path.
Back on your own machine, you need to make sure TextMate 2 is ready for the incoming connections. Be sure Accept `rmate` connections is checked in the _Terminal_ pane of _Preferences…_ in the _TextMate_ menu. You can leave "Access for" on "local clients" though, because I'll show you a secure trick for bridging the two computers.
![The Terminal Preferences pane][preferences]
### The Magic of SSH Tunnels
With the install out of the way, you should be ready to use `rmate`.
To do it's job, `rmate` needs a connection back to your computer so that it can talk to TextMate 2. There are multiple ways you can accomplish this, but probably the best way is to use a reverse SSH tunnel. With the proper setup, you can forward the port `rmate` likes back to your local machine where TextMate 2 can answer the call-to-duty.
The easiest way to do this is to connect to your server using a command like:
ssh -R 52698:127.0.0.1:52698 example.com
The `-R` option sets up a reverse tunnel. The first `52698` names a port on the remote. It will be connected to `127.0.0.1:52698` or the same port on the connecting box. That port number is the default for TextMate 2 and `rmate`, so you should now be able to edit away.
To test things out, just try a command like:
rmate test_file.txt
That should contact TextMate 2 on your local box. Note that TextMate 2 does need to be running for this to work.
Type some content using any TextMate editing features you just can't live without and save the file. Then check it on your server with:
cat test_file.txt
You should see the content you typed on the server. Magic!
If you try to edit a file you don't have permission to change on the server, `rmate` will refuse the edit and warn you:
$ rmate /etc/crontab
File /etc/crontab is not writable! Use -f/--force to open anyway.
You can either use the `-f` option to force the open in read-only mode or use `sudo` to get the needed permissions. Remember that `rmate` is a Ruby script, so RVM users will probably need to use `rvmsudo` to keep the same Ruby selected:
rvmsudo rmate /etc/crontab
After you verify that things are working, feel free to update your SSH setting to automatically setup the tunnel without you needing to supply the `-R` arguments all the time.
For a single server just add an entry like the following to your `~/.ssh/config`:
Host example.com
RemoteForward 52698 127.0.0.1:52698
If you want to make those settings the default for all of your servers, use the wildcard host:
Host *
RemoteForward 52698 127.0.0.1:52698
With settings like those in place, a bare `ssh` command (without `-R`) should still establish the tunnel for you and allow you to use `rmate`.
### Port Forwarding
SSH tunneling is probably the lowest configuration option for using `rmate`. It's also the safest since the computers chat over an encrypted connection. Go that way if you can.
However, if you can't, you do have other options. One reason you might need these is if you use multiple machines to connect to the same account on one remote. They wouldn't all be able to use the same port.
Of course, you can specify different ports when you setup TextMate 2 and/or your SSH tunnel. `rmate` also supports `--host` and `--port` options, so you can use those to customize the connection.
One thing you may desire in complex connection scenarios is for `rmate` to just connect back to where the connection came from. You can have it determine the IP address the SSH connection came from using `--host auto`.
You will probably need to setup port forwarding of the desired port in your router's settings to use a connection like this.
The real trick with these connections though is that it's really the client that knows how things should best be handled. Using another feature of SSH, we can have the client set some variables to the proper details and forward those settings to the remote so `rmate` can honor them.
A server whitelists the variables it will accept from connecting clients, so you'll need to edit `/etc/ssh/sshd_config`. It probably already have these lines in it:
# Allow client to pass locale environment variables
AcceptEnv LANG LC_*
You need to add allowances for `RMATE_HOST` and `RMATE_PORT`, so change that second line to read:
AcceptEnv LANG LC_* RMATE_*
You also need a similar entry in your local `~/.ssh/config`:
Host example.com
SendEnv RMATE_*
Again, you can put that setting under the host wildcard if you prefer.
Then you can add lines like the following to your local `~/.bashrc`:
export RMATE_HOST=auto
export RMATE_PORT=12345
Those settings would be forwarded up to the server when you connect and `rmate` would use them to connect back to your computer for edits.
Either way, it's a little work to get `rmate` setup, but it sure expands TextMate 2 reach once you do. Welcome to remote editing with TextMate.
[preferences]: images/terminal_preferences.png
[carets]: multiple_carets.md

View File

@@ -0,0 +1,21 @@
# Non-content Scopes
There are a few scopes which are not related to the content.
## Dynamic Scopes
Presently I think only `dyn.selection` is available. This allow key bindings which are only active when there is a selection. One example of this could be to overload the tab key to shift right, only when text is selected.
Another example is the “Wrap in Braces” command in the included Avian bundle. This is bould to `{` and indents the selection plus puts braces on first/last line.
Other dynamic scopes planned: `dyn.caret.(begin|end).(line|document)`. This will allow keys/snippets to only trigger when caret is at the beginning/ending of the line or document.
## SCM Scopes
The SCM system used is available as `scm.attr.«name»`. A few more SCM scopes are available, for Git the current branch, and status (although that is presently not standardized).
## Other
The path is available (reversed) as `attr.rev-path.«path»`. This is so that we can bind to e.g. `attr.rev-path.erb` to target all files with an ERb extension. This is more relevent for events or injection than key bindings.
OS version is available. This allows us to have different versions of the same command for different OS versions (sometimes easier than testing for the OS version in the command when there is little code sharing between the two versions).

View File

@@ -0,0 +1,65 @@
# Properties
For many settings TextMate will look for a `.tm_properties` file in the current folder and in any parent folders (up to the users home folder).
These are simple `setting = value` listings where the value is a format string in which other variables can be referenced.
If the setting name is uppercase it will be available as an environment variable (for commands and snippets).
For example to setup the basic stuff one could have `~/.tm_properties` with these values:
# Settings
theme = "71D40D9D-AE48-11D9-920A-000D93589AF6"
fontName = "Menlo"
fontSize = 13
fileBrowserGlob = "{*,.tm_properties,.htaccess}"
# Variables
PATH = "$PATH:$HOME/bin"
TM_GIT = "/opt/local/bin/git"
It is possible to target only files matching a given glob, for example:
[ *.txt ]
softWrap = true
[ .git/COMMIT_EDITMSG ]
softWrap = true
spellChecking = true
spellingLanguage = 'en'
[ "/usr/include/{**/,}*" ]
tabSize = 8
A magic value of `attr.untitled` is used for untitled files. This allows setting the file type for these, e.g.:
[ attr.untitled ]
fileType = 'source.c++'
The normal TM variables are available when expanding the format strings (values). In addition a `CWD` variable is available which represents the folder from which the `.tm_properties` file is read. This is necessary to set the project directory, for example I have `~/Source/Avian/.tm_properties` with these settings:
projectDirectory = "$CWD"
windowTitle = "$TM_DISPLAYNAME — Avian"
fileChooserGlob = "{{src,Shared/include}/**/*.{cc,mm,h},target{,s},Makefile{,.*},.tm_properties}"
The first setting effectively sets `TM_PROJECT_DIRECTORY` to `~/Source/Avian`. In `~/Source/Avian/Applications` I have a (variable) setting like this:
TM_MAKE_TARGET = '${TM_DIRECTORY/^.*\/Applications\/([^\/]+)(\/.*)?$/$1\/run/}'
What this does is set the make target based on the current directory. So if I am editing `~/Source/Avian/Applications/mate/src/main.cc` ⌘B will make `mate/run` whereas if I am in `~/Source/Avian/Applications/Avian/src/main.cc` it will make `Avian/run`.
## Grammar
The grammar used to parse the `.tm_properties` files are as below. Whitespace (in the form of spaces or tabs) is allowed between elements.
file: ( «line» )*
line: ( «comment» | ( «section» | «assignment» )? ( «comment» )? ) ( '\n' | EOF )
section: '[' «name» ( ";" «name» )* ']'
name: ( /[^\] \t\n]/ | /\\[\] \t\n\\]/ )+
assignment: «key» '=' «value»
key: ( /[^= \t\n]/ | /\\[= \t\n\\]/ )+
value: ( «single_string» | «double_string» | «bare_string» )
single_string: "'" ( /[^']/ | /\\['\\]/ )* "'"
double_string: '"' ( /[^"]/ | /\\["\\]/ )* '"'
bare_string: ( /[^ \t\n]/ | /\\[ \t\n\\]/ )+
comment: '#' ( /[^\n]/ )*

View File

@@ -0,0 +1,14 @@
# Scope Selector Syntax
atom: «string» | '*'
scope: «atom» ('.' «atom»)*
path: '^'? «scope» ('>'? «scope»)* '$'?
group: '(' «selector» ')'
filter: ("L:"|"R:"|"B:") («group» | «path»)
expression: '-'? («filter» | «group» | «path»)
composite: «expression» ([|&-] «expression»)*
selector: «composite» (',' «composite»)*
We need to add priority (to `path` rule): `(':' «integer»)?`. This is when multiple commands handle the same event and are using same scope (which can be the case when chaining e.g. documentation commands).
We probably need `~` for negative-look ahead, e.g.: `text.html ~ meta.embedded`.

View File

@@ -0,0 +1,16 @@
# Selection String Syntax
selection = «range» ('&' «range»)*
range = «pos» | «normal_range» | «column_range»
pos = «line» (':' «column»)? ('+' «offset»)?
normal_range = «pos» '-' «pos»
column_range = «pos» 'x' «pos»
line = [1-9][0-9]*
column = [1-9][0-9]*
offset = [1-9][0-9]*
This can be used with `mate -l`, _Go to Line…_, and the ⌘T dialog (by putting `:«selection string»` after the file name) — as for the latter, if a string on the find clipboard has this format, it will be the default value in ⌘T, this is useful if you have a line in Terminal like:
main.cc:32: warning: no return statement.
You can then select `main.cc:32` press ⌘E and jump to Avian and press ⌘T followed by ↩ (one shortcoming is that ⌘T doesnt show the currently open file, so it will only work if `main.cc` is not already the active file).

View File

@@ -0,0 +1,546 @@
{
CFBundleName = "${APP_NAME}";
CFBundleShortVersionString = "${APP_VERSION}"; // shown in About window
CFBundleVersion = "${APP_REVISION}"; // shown in About window in parenthesis (but this format is not well-formed)
CFBundleIdentifier = "com.macromates.${APP_NAME}.preview";
CFBundleDevelopmentRegion = English;
CFBundleExecutable = "${TARGET_NAME}";
CFBundleHelpBookFolder = "${APP_NAME} Help";
CFBundleHelpBookName = "${APP_NAME} 2 Help";
CFBundleIconFile = "TextMate";
CFBundleInfoDictionaryVersion = "6.0";
CFBundlePackageType = APPL;
CFBundleSignature = avin; // we didnt register this code with Apple
LSMinimumSystemVersion = "${APP_MIN_OS}";
NSAppleScriptEnabled = YES;
NSMainNibFile = MainMenu;
NSPrincipalClass = NSApplication;
NSContactsUsageDescription = 'Your email address is used as identifier for crash report submission. It can be changed in Preferences.\n\nIt is also used as a default contact address if you create your own bundle. This can be changed in the bundle editor after you create a bundle.';
CFBundleURLTypes = (
{ CFBundleURLName = "TextMate URL";
CFBundleURLSchemes = ( txmt );
}
);
CFBundleDocumentTypes = (
{ CFBundleTypeName = "ADA source";
CFBundleTypeExtensions = (adb, ads);
CFBundleTypeIconFile = ADA;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Compiled AppleScript";
CFBundleTypeExtensions = (scpt);
CFBundleTypeIconFile = AppleScript;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "AppleScript source";
CFBundleTypeExtensions = (applescript);
CFBundleTypeIconFile = AppleScript;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "ActionScript source";
CFBundleTypeExtensions = (as);
CFBundleTypeIconFile = ActionScript;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "ASP document";
CFBundleTypeExtensions = (asp, asa);
CFBundleTypeIconFile = ASP;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "ASP.NET document";
CFBundleTypeExtensions = (aspx, ascx, asmx, ashx);
CFBundleTypeIconFile = ASPNET;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "BibTeX bibliography";
CFBundleTypeExtensions = (bib);
CFBundleTypeIconFile = BibTeX;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "C source";
CFBundleTypeExtensions = (c);
CFBundleTypeIconFile = C;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "C++ source";
CFBundleTypeExtensions = (cc, cp, cpp, cxx, "c++");
CFBundleTypeIconFile = "C++";
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "C# source";
CFBundleTypeExtensions = (cs);
CFBundleTypeIconFile = "C#";
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "CoffeeScript source";
CFBundleTypeExtensions = (coffee);
CFBundleTypeIconFile = CoffeeScript;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Context Free Design Grammar";
CFBundleTypeExtensions = (cfdg);
CFBundleTypeIconFile = CFDG;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Comma separated values";
CFBundleTypeExtensions = (csv);
CFBundleTypeIconFile = Text;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Tab separated values";
CFBundleTypeExtensions = (tsv);
CFBundleTypeIconFile = Text;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "CGI script";
CFBundleTypeExtensions = (cgi, fcgi);
CFBundleTypeIconFile = CGI;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Configuration file";
CFBundleTypeExtensions = (cfg, conf, config, htaccess);
CFBundleTypeIconFile = Config;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Cascading style sheet";
CFBundleTypeExtensions = (css);
CFBundleTypeIconFile = CSS;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Differences file";
CFBundleTypeExtensions = (diff);
CFBundleTypeIconFile = Diff;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Document Type Definition";
CFBundleTypeExtensions = (dtd);
CFBundleTypeIconFile = DTD;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Dylan source";
CFBundleTypeExtensions = (dylan);
CFBundleTypeIconFile = Dylan;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Erlang source";
CFBundleTypeExtensions = (erl, hrl);
CFBundleTypeIconFile = Erlang;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "F-Script source";
CFBundleTypeExtensions = (fscript);
CFBundleTypeIconFile = "F-Script";
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Fortran source";
CFBundleTypeExtensions = (f, for, fpp, f77, f90, f95);
CFBundleTypeIconFile = Fortran;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Header";
CFBundleTypeExtensions = (h, pch);
CFBundleTypeIconFile = H;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "C++ header";
CFBundleTypeExtensions = (hh, hpp, hxx, "h++");
CFBundleTypeIconFile = "H++";
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "GTD document";
CFBundleTypeExtensions = (gtd, gtdlog);
CFBundleTypeIconFile = GTD;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Haskell source";
CFBundleTypeExtensions = (hs, lhs);
CFBundleTypeIconFile = Haskell;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "HTML document";
CFBundleTypeExtensions = (htm, html, phtml, shtml);
CFBundleTypeIconFile = HTML;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Include file";
CFBundleTypeExtensions = (inc);
CFBundleTypeIconFile = Inc;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "iCalendar schedule";
CFBundleTypeExtensions = (ics);
CFBundleTypeIconFile = iCal;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "MS Windows initialization file";
CFBundleTypeExtensions = (ini);
CFBundleTypeIconFile = INI;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Io source";
CFBundleTypeExtensions = (io);
CFBundleTypeIconFile = Io;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Java source";
CFBundleTypeExtensions = (java);
CFBundleTypeIconFile = Java;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "BeanShell script";
CFBundleTypeExtensions = (bsh);
CFBundleTypeIconFile = Java;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Java properties file";
CFBundleTypeExtensions = (properties);
CFBundleTypeIconFile = Java;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "JavaScript source";
CFBundleTypeExtensions = (js, htc);
CFBundleTypeIconFile = JavaScript;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Java Server Page";
CFBundleTypeExtensions = (jsp);
CFBundleTypeIconFile = JSP;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "JSON file";
CFBundleTypeExtensions = (json);
CFBundleTypeIconFile = JSON;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "LDAP Data Interchange Format";
CFBundleTypeExtensions = (ldif);
CFBundleTypeIconFile = Blank;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Lisp source";
CFBundleTypeExtensions = (lisp, cl, l, lsp, mud, el);
CFBundleTypeIconFile = Lisp;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Log file";
CFBundleTypeExtensions = (log);
CFBundleTypeIconFile = Log;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Logo source";
CFBundleTypeExtensions = (logo);
CFBundleTypeIconFile = Logo;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Lua source";
CFBundleTypeExtensions = (lua);
CFBundleTypeIconFile = Lua;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Markdown document";
CFBundleTypeExtensions = (markdown, mdown, markdn, md);
CFBundleTypeIconFile = Markdown;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Mediawiki document";
CFBundleTypeExtensions = (wiki, wikipedia, mediawiki);
CFBundleTypeIconFile = Wiki;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "MIPS assembler source";
CFBundleTypeExtensions = (s, mips, spim, asm);
CFBundleTypeIconFile = ASM;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Modula-3 source";
CFBundleTypeExtensions = (m3, cm3);
CFBundleTypeIconFile = Modula;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "MoinMoin document";
CFBundleTypeExtensions = (moinmoin);
CFBundleTypeIconFile = Wiki;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Objective-C source";
CFBundleTypeExtensions = (m);
CFBundleTypeIconFile = "Obj-C";
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Objective-C++ source";
CFBundleTypeExtensions = (mm);
CFBundleTypeIconFile = "Obj-C++";
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "OCaml source";
CFBundleTypeExtensions = (ml, mli, mll, mly);
CFBundleTypeIconFile = OCaml;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Pascal source";
CFBundleTypeExtensions = (pas, p);
CFBundleTypeIconFile = Pascal;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Patch file";
CFBundleTypeExtensions = (patch);
CFBundleTypeIconFile = Patch;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Perl source";
CFBundleTypeExtensions = (pl, pod, perl);
CFBundleTypeIconFile = Perl;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Perl module";
CFBundleTypeExtensions = (pm);
CFBundleTypeIconFile = PERL;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "PHP source";
CFBundleTypeExtensions = (php, php3, php4, php5);
CFBundleTypeIconFile = PHP;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "PostScript source";
CFBundleTypeExtensions = (ps, eps);
CFBundleTypeIconFile = PostScript;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Property list";
CFBundleTypeExtensions = (dict, plist, scriptSuite, scriptTerminology);
CFBundleTypeIconFile = Plist;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Python source";
CFBundleTypeExtensions = (py, rpy, cpy, python);
CFBundleTypeIconFile = Python;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "R source";
CFBundleTypeExtensions = (r, s);
CFBundleTypeIconFile = R;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Ragel source";
CFBundleTypeExtensions = (rl, ragel);
CFBundleTypeIconFile = Ragel;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Remind document";
CFBundleTypeExtensions = (rem, remind);
CFBundleTypeIconFile = Blank;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "reStructuredText document";
CFBundleTypeExtensions = (rst, rest);
CFBundleTypeIconFile = reST;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "HTML with embedded Ruby";
CFBundleTypeExtensions = (rhtml);
CFBundleTypeIconFile = RHTML;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "SQL with embedded Ruby";
CFBundleTypeExtensions = (erbsql);
CFBundleTypeIconFile = SQL;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Ruby source";
CFBundleTypeExtensions = (rb, rbx, rjs, rxml);
CFBundleTypeIconFile = Ruby;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Scheme source";
CFBundleTypeExtensions = (scm, sch);
CFBundleTypeIconFile = Scheme;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Setext document";
CFBundleTypeExtensions = (ext);
CFBundleTypeIconFile = Blank;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Shell script";
CFBundleTypeExtensions = (sh, ss, bashrc, "bash_profile", "bash_login", profile, "bash_logout");
CFBundleTypeIconFile = Shell;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Slate source";
CFBundleTypeExtensions = (slate);
CFBundleTypeIconFile = Slate;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "SQL source";
CFBundleTypeExtensions = (sql);
CFBundleTypeIconFile = SQL;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Standard ML source";
CFBundleTypeExtensions = (sml);
CFBundleTypeIconFile = SML;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Strings document";
CFBundleTypeExtensions = (strings);
CFBundleTypeIconFile = Strings;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Scalable vector graphics";
CFBundleTypeExtensions = (svg);
CFBundleTypeIconFile = SVG;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "SWIG source";
CFBundleTypeExtensions = (i, swg);
CFBundleTypeIconFile = SWIG;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Tcl source";
CFBundleTypeExtensions = (tcl);
CFBundleTypeIconFile = Tcl;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "TeX document";
CFBundleTypeExtensions = (tex, sty, cls);
CFBundleTypeIconFile = TeX;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Plain text document";
CFBundleTypeExtensions = (text, txt, utf8);
CFBundleTypeIconFile = Text;
CFBundleTypeMIMETypes = ("text/plain");
CFBundleTypeOSTypes = (TEXT, sEXT, ttro);
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Textile document";
CFBundleTypeExtensions = (textile);
CFBundleTypeIconFile = Textile;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "XHTML document";
CFBundleTypeExtensions = (xhtml);
CFBundleTypeIconFile = XHTML;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "XML document";
CFBundleTypeExtensions = (xml, rss, tld, pt, cpt, dtml);
CFBundleTypeIconFile = XML;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "XSL stylesheet";
CFBundleTypeExtensions = (xsl, xslt);
CFBundleTypeIconFile = XSL;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Electronic business card";
CFBundleTypeExtensions = (vcf, vcard);
CFBundleTypeIconFile = Blank;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Visual Basic source";
CFBundleTypeExtensions = (vb);
CFBundleTypeIconFile = VisualBasic;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "YAML document";
CFBundleTypeExtensions = (yaml, yml);
CFBundleTypeIconFile = YAML;
CFBundleTypeRole = Editor;
},
/* bundle items */
{ CFBundleTypeName = "TextMate snippet";
CFBundleTypeExtensions = (tmSnippet);
CFBundleTypeIconFile = 'TextMate Snippet';
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "TextMate macro";
CFBundleTypeExtensions = (tmMacro);
CFBundleTypeIconFile = 'TextMate Macro';
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "TextMate language grammar";
CFBundleTypeExtensions = (tmLanguage);
CFBundleTypeIconFile = 'TextMate Grammar';
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "TextMate preferences";
CFBundleTypeExtensions = (tmPreferences);
CFBundleTypeIconFile = 'TextMate Settings';
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "TextMate command";
CFBundleTypeExtensions = (tmCommand);
CFBundleTypeIconFile = 'TextMate Command';
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "TextMate drag command";
CFBundleTypeExtensions = (tmDragCommand);
CFBundleTypeIconFile = 'TextMate Bundle Item';
CFBundleTypeRole = Editor;
},
/* other textmate types */
{ CFBundleTypeName = "TextMate release notes";
CFBundleTypeExtensions = (tmReleaseNotes);
CFBundleTypeIconFile = Text;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "TextMate theme";
CFBundleTypeExtensions = (tmTheme, tmtheme);
CFBundleTypeIconFile = 'TextMate Theme';
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "TextMate bundle";
CFBundleTypeExtensions = (tmBundle, tmbundle);
CFBundleTypeIconFile = 'TextMate Bundle';
CFBundleTypeRole = Editor;
LSTypeIsPackage = 1;
},
{ CFBundleTypeName = "TextMate plug-in";
CFBundleTypeExtensions = (tmPlugIn, tmPlugin, tmplugin);
CFBundleTypeIconFile = 'TextMate PlugIn';
CFBundleTypeRole = Editor;
LSTypeIsPackage = 1;
},
/* generic types */
{ CFBundleTypeName = "Text document"; /* generic plain text types */
CFBundleTypeExtensions = (nfo);
CFBundleTypeIconFile = Text;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Source"; /* generic source code types */
CFBundleTypeExtensions = (
g, vss, d, e, gri, inf, mel, build, re,
textmate, fxscript, lgt
);
CFBundleTypeIconFile = Blank;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Document"; /* generic document types */
CFBundleTypeExtensions = (
cfm, cfml, dbm, dbml, dist, dot, ics, ifb, dwt, g,
in, l, m4, mp, mtml, orig, pde,
rej, servlet, s5, tmp, tpl, tt,
xql, yy, "*"
);
CFBundleTypeIconFile = Blank;
CFBundleTypeRole = Editor;
},
{ CFBundleTypeName = "Document";
CFBundleTypeIconFile = GenericDocumentIcon;
CFBundleTypeOSTypes = ("****");
CFBundleTypeRole = Editor;
LSItemContentTypes = ( public.data );
},
);
}

View File

@@ -0,0 +1,59 @@
{
"~\UF700" = "moveToBeginningOfColumn:";
"~$\UF700" = "moveToBeginningOfColumnAndModifySelection:";
"~\UF701" = "moveToEndOfColumn:";
"~$\UF701" = "moveToEndOfColumnAndModifySelection:";
"^\UF700" = "moveToBeginningOfBlock:";
"^\UF701" = "moveToEndOfBlock:";
"^$\UF700" = "moveToBeginningOfBlockAndModifySelection:";
"^$\UF701" = "moveToEndOfBlockAndModifySelection:";
"^~\UF700" = "moveToBeginningOfBlock:";
"^~\UF701" = "moveToEndOfBlock:";
"^~$\UF700" = "moveToBeginningOfBlockAndModifySelection:";
"^~$\UF701" = "moveToEndOfBlockAndModifySelection:";
"^\UF702" = "moveSubWordLeft:";
"^\UF703" = "moveSubWordRight:";
"^$\UF702" = "moveSubWordLeftAndModifySelection:";
"^$\UF703" = "moveSubWordRightAndModifySelection:";
"^\U007F" = "deleteSubWordLeft:";
"^\UF728" = "deleteSubWordRight:";
"^~\UF702" = "moveSubWordLeft:";
"^~\UF703" = "moveSubWordRight:";
"^~$\UF702" = "moveSubWordLeftAndModifySelection:";
"^~$\UF703" = "moveSubWordRightAndModifySelection:";
"^~\U007F" = "deleteSubWordLeft:";
"^~\UF728" = "deleteSubWordRight:";
"$\033" = "previousCompletion:";
"@\UF728" = "deleteToEndOfLine:";
"~\UF705" = "showContextMenu:";
"^s" = "incrementalSearch:";
"^w" = "selectWord:";
"@L" = "selectHardLine:";
"@P" = "selectParagraph:";
"^~b" = "selectCurrentScope:";
"^T" = "transposeWords:";
"^g" = "changeCaseOfLetter:";
"^~g" = "changeCaseOfWord:";
"^u" = "uppercaseWord:";
"^U" = "lowercaseWord:";
"^~u" = "capitalizeWord:";
"~$\t" = "shiftLeft:";
"~\t" = "shiftRight:";
"~@[" = "indent:";
"^q" = "reformatText:";
"^j" = "reformatTextAndJustify:";
"^~q" = "unwrapText:";
"^r" = "executeSelectionInsertingOutput:";
"^~r" = "executeSelectionReplacingSelection:";
}

View File

@@ -0,0 +1,223 @@
# Release Notes
## 2012-07-09
* Signed for Gatekeeper.
* Using ⌃K at the end of a line now deletes the newline.
* Implement `moveToBeginning/EndOfParagraphAndModifySelection:` selectors. The default key bindings have ⌃⇧E extend selection to end of paragraph (so was previously a no-op in TM).
* The `.tm_properties` parser no longer require spaces around the equal character nor is there a need to quote section names when they contain special characters, etc. The full grammar can be found in the help book.
* Disable font ligatures. Using the Mensch font on Lion (but not Snow Leopard) would previously render fi using the fi ligature.
* Fix drawing issues for Retina Mac Books (horizontal lines would appear while scrolling).
* The Select Bundle Item window now use the same key equivalent control as the bundle editor.
* Improve keyboard usage with key equivalent control. You can tab to/from the control, when the control has focus you can use forward/backward delete to remove the current key equivalent or space to record a new one. Clicking the control using the mouse will go directly to record mode.
* When closing the preferences window pending edits of the variables will be committed.
* If a bundle item contained an empty string for key equivalent, the bundle editor would crash when showing that item.
* When recording a key equivalent in the bundle editor, global hotkeys are disabled. This means you can bind to keys that may not actually work on your system (because another application or system service is swallowing the event).
* Commands which use ssh should now work even if the private key has a passphrase (e.g. the Subversion commands).
* Improve clicking various UI elements (e.g. the close button in tabs) for inactive windows.
* Add clear button to key equivalent recorder.
* Fix file browser crash when duplicating files which had a suffix one character longer than the base name.
* Support services.
* Request administration rights when required by the software update process.
* Fix missing repeat events (on Lion) for characters with no potential diacritics.
* Fixed crash resulting from clicking and holding mouse exactly on a caret, starting a drag operation and dropping the (empty) selection.
* Show dialog for (some) keychain failures. The license key is stored in and retrieved from the keychain, so errors will cause TextMate to not run. I suspect there is an issue where TextMate “loses” its signed state and is denied access to the keychain, as a few users have reported sudden problems which were resolved simply by moving TextMate on disk.
* When switching tabs, selection was drawn as if in a background window.
## 2012-02-27
* Restore UI sounds for 10.7.
* Bundle editor now use a key equivalent recorder (instead of text field).
* Fixed issue with involuntary folder expansion when using ⌥⌘N with focus in file browser (missed by previous “fix”).
* Dropped support for Mac OS X 10.5.
## 2012-02-18
* Improved SCM workaround for Mercurial issue — previous workaround could leave SCM status outdated, this should no longer be the case.
* Fix problem with items that differed only in capitalization. These were collapsed into a single item in several UI menus/dialogs, e.g. Open Favorites.
* Fix bad encoding when copying Find in Folder / Find All results.
* Using Go to Counterpart no longer include files matched by the effective `binary` or `exclude` glob from `.tm_properties`.
* Changed `binary` setting in `.tm_properties` from a boolean to a glob. This allows to set a global binary glob instead of needing to set the boolean for specific settings (which is more complex to manage via a GUI).
* Introduce `attr.file.unknown-type` and `attr.file.unknown-encoding`. You can use this in `.tm_properties` files to set `fileType` and `encoding` to provide default values only for files where TextMate was unable to find the proper type/encoding (which normally results in a dialog asking the user).
* Cached `.tm_properties` files are now observed via `kevent` so changes invalidate the cache immediately (previously it could take up to 30 seconds before the updated file was read). On file systems without `kevent` support you need to relaunch TextMate to flush the cache.
* The sections in `.tm_properties` files are now considered real scope selectors. As the syntax for globs and scope selectors is the same, the latter must start with `attr`, `source`, or `text` to indicate it is a scope selector. If your desired scope selector does not, you can always prefix it (with the always present `attr`) e.g.: `[ "attr - attr.scm" ]` to have settings for files not under version control. This should just be temporary while exact requirements are being fleshed out.
* Introduce `TM_SCM_BRANCH` variable (mainly useful for setting `windowTitle` in `.tm_properties`).
* You can augment the rules for scope attributes by saving a property list to `~/Library/Application Support/TextMate/ScopeAttributes.plist`. Your rules will be added to the default list which is:
{
rules = (
{ attribute = 'attr.scm.svn'; glob = '.svn'; group = 'scm'; },
{ attribute = 'attr.scm.hg'; glob = '.hg'; group = 'scm'; },
{ attribute = 'attr.scm.git'; glob = '.git'; group = 'scm'; },
{ attribute = 'attr.scm.p4'; glob = '.p4config'; group = 'scm'; },
{ attribute = 'attr.project.make'; glob = 'Makefile'; group = 'build'; },
{ attribute = 'attr.project.rake'; glob = 'Rakefile'; group = 'build'; },
{ attribute = 'attr.project.xcode'; glob = '*.xcodeproj'; group = 'build'; },
);
}
The `group` is to allow mutually exclusive attributes. E.g. if a folder contains both a `Rakefile` and a `Makefile` entry, files in that folder only get the `attr.project.make` attribute set since the two tests are in same group (and the `Makefile` test is listed first).
* Added _View → Show Wrap Column_ setting and corresponding `showWrapColumn` `.tm_properties` setting.
* Introduced `fontLeadingDelta` and `fontAscentDelta` defaults keys. These must be set as floats or integers (not strings), e.g.: `defaults write com.macromates.TextMate.preview fontLeadingDelta -float 0`. The default value for both keys is `1`.
* Use of extended attributes can now be disabled. The defaults key is `volumeSettings` and the value is an associative array with path prefix and another associative array with settings for that path (presently only `extendedAttributes` is supported, but SCM badges, display names, and more is likely to appear). So if for example we wish to disable extended attributes for files under `/net/` we can do: `defaults write com.macromates.TextMate.preview volumeSettings '{ "/net/" = { extendedAttributes = 0; }; }'`.
* Some improvements to file chooser (⌘T):
- Current document is now included in list, and if typed exactly, will rank at top.
- Filter string is now down-cased with spaces removed before matching to cater to the habits of some users.
<!-- - Typing `:` or `@` without a filter string will go to line/symbol in current file (rather than currently selected file). -->
- Raw paths are shown instead of display names, which should be faster (especially on network file systems).
- Subtle ranking improvements.
* The font size unit parser (for themes) is no longer locale sensitive (so period is always `.`).
* The collapse/expand all button for the Find in Folder results did not properly refresh the outline view on Lion.
* Implement option page up/down (for moving caret).
* Set `initialFileBrowserURL` defaults key (to a URL) to change the initial location of the file browser (for new windows). E.g.: `defaults write com.macromates.TextMate.preview initialFileBrowserURL "file://$HOME/Source/"`.
* Default location for `mate` is now `/usr/local/bin` (if it exists) as this is included in the `PATH` for most users.
* Fix failure to install `mate` when the install path existed as a dead symbolic link.
* Fix issues with involuntary (recursive) expansion of file browser.
* Support transparent backgrounds in themes.
* Exclude binary matches for folder searching.
* Its now possible to change case in the file browser (previously a “file exists” error was shown for case-insensitive systems).
* Fix issue with the `txmt:` URL scheme (though omitting the `file` parameter is still not supported).
## 2011-12-20
* Rename _Beta Builds_ to _Nightly Builds_ (Preferences → Software Update). This will be semi-daily builds that might have minor regressions (like e.g. the out-of-date SCM badges mentioned below), as I plan to do these frequently (but at most once per day). With Christmas coming up in four days (and me being in charge this year) dont expect too many updates in the coming days though; Im still working my way through reported issues.
* Dont add documents to recent menu when opened via file browser.
* Fix the previously introduced `callback.application.did-(de)activate` semantic classes.
* Asking for SCM status now use selected folder over file browser root.
* Provide document name in Save As dialog.
* Fix issue with proxy auto-configuration scripts.
* The `htmlOutputPlacement` defaults value can now be set to `right` to get the split to the right of the document. The width is presently not persisted across relaunches (and defaults to 100px).
* Throttle SCM status polls. This is a provisional fix for mercurial issues which can cause SCM badges to be out of date.
## 2011-12-18
* Introduced `callback.application.did-activate` and `callback.application.did-deactivate` as two new semantic classes. A command with this class will be executed when the application gain/lose focus. The scope selector is matched against the “current scope” of the visible document (in each window). This allows creating a command with “Save” set to “Modified Documents” and thereby recreating TextMate 1.xs ability to save modified documents on lost focus.
The approach in 2.0 also allows to run some code, for example reloading the currently open browsers, and it can be scoped e.g. to `text.html` to only have the “save on lost focus” enabled when editing HTML files — alternatively one can introduce a custom scope like `attr.save-on-deactivate` and set that for specific projects via a `scopeAttributes` setting in `.tm_properties`.
* Tab bar can be made always visible: `defaults write com.macromates.TextMate.preview disableTabBarCollapsing -bool YES`.
* Fix issue with indented soft wrap having the prefix string wider than the wrap column.
* Fix issue with selection sent to commands needing entire document (e.g. ⌘R for Ruby, Python, etc.).
* Fix issue with ⌃⇥ not always working to move focus to file browser.
* Remove folding patterns from grammar template.
## 2011-12-16
* HTML output can open in its own window: `defaults write com.macromates.TextMate.preview htmlOutputPlacement window`.
* Anti-alias can be disabled: `defaults write com.macromates.TextMate.preview disableAntiAlias -bool YES`.
* File browser can be placed on the right side: `defaults write com.macromates.TextMate.preview fileBrowserPlacement right`.
* With multiple carets, ⌘-clicking one of them removes it (i.e. its a toggle).
* Carriage returns (`<CR>`) on the general clipboard are converted to regular newlines when pasting.
* Rename help book to “TextMate 2” which fixes issues where _Help → TextMate Help_ showed TextMate 1.5s help book.
* TextMate 1.x and 2.0 can now both run at the same time.
* Setting `TM_HG` and `TM_GIT` in Preferences is no longer eclipsed by the default properties (which set them to point at `/opt/local/bin`).
* Fix potential crash when deleting bundles on disk (`rm -rf`).
- - -
## 2011-12-13
Notable changes since to TextMate 1.5.10:
* **Multiple carets / discontinuous selection**
The easiest way to get multiple carets is by doing a left or right movement command (like arrow left/right) with a column selection. You can also ⌘-click to place new carets or use _Edit → Find All_ (⌥⌘F).
* **Snippets**
New syntax for pop-up menus: `${1|first,second,…,last|}`. In a Git commit message try `fixup⇥` to get a pop-up of your last 10 commit messages (for a `fixup!`-commit) or between `@interface``@end` in Objective-C you can try `prop⇥` to get a `@property`-declaration with pop-up for its access type.
Snippets can be nested, a frequent request but it does cause a few false firings of snippets which we havent yet found a good solution for.
* **Movement / Selection**
Text is classified into units which you can change with the `characterClass` (scope) setting. For example a C string with `%1$ld\n` has only two units (`%1$ld` and `\n`) for the purpose of selection (_Select → Word_ or double click), word movement (⌥⇠/⌥⇢), and other functions that rely on character types.
TextMate 1.x has _Select → Block_ (⇧⌘B) where a block is found using scope-defined paired characters such as braces, brackets, etc. In 2.0 you can move to begin/end of such block with ⌃⇡/⌃⇣ and the paired characters can be strings (planned to be patterns).
Whenever you select a unit (like anything in the _Edit → Select_ submenu) the selection is “unanchored”, meaning it can be extended to either the left or right side. A minor thing but I find it surprisingly useful, e.g. to select `foo(bar‸)` we first select the parenthesis with ⇧⌘B and then extend left by one unit with ⌥⇧⇠, previously you couldnt be sure the last step would extend or simply move the selection end point.
* **Themes**
There now is the ability to change font and font size per scope. So youll (by default) see that headings in markup are shown with a larger (non-fixed width) font.
You can also change soft wrap on a per-line basis, so soft wrap is e.g. disabled (by default) for diffs embedded in Git commit messages, raw (code) blocks embedded in markup, and enabled for line comments in source.
Soft wrap can be indented. This is also based on scoped settings so list items in markup are indented differently than line comments in source.
* **Foldings**
Foldings have been taken out of grammars and are now per-line (via scoped settings). In addition there are two new patterns to allow folding indented blocks.
* **Indent**
TextMate is more aggressive about doing indent corrections. This works great when the patterns are well-calibrated but can be disabled with the `disableIndentCorrections` scope-setting. This setting also change the behavior of re-indented paste to a heuristic that works much better with Python (for which indent corrections are disabled by default).
* **Folder specific settings**
Variables and some settings can be set on a folder / file-type basis.
* **Project drawer**
The project drawer has been replaced with a file browser sidebar. This file browser has most Finder functions (labels, duplicate, etc.) and does SCM-badging, presently only for Subversion, Git, and Mercurial. In addition it has support for running commands (although somewhat proof-of-concept ATM) and can also do custom data sources, the API is not yet public but an SCM Status data source is included (the smart folder icon) which show SCM status for the current project. This data source works great together with the various SCM bundles.
* **Buffer completion**
This has been improved to work with the existing word suffix (in addition to prefix), use the new character class system, etc.
* **Shell invocation**
_Preferences → Terminal_ allows you to install `mate` which has a few new options and work when called as `sudo mate`. In addition youll find `rmate` which is a ruby implementation that you can use on a server over ssh.
* **Search in project / folder**
Revamped the old “Find in Folder” and merged it with the regular find dialog. Its now asynchronous, a lot faster, and more flexible.
* **Format strings**
Format strings are ubiquitous in TextMate 2. Even scopes in grammars are format strings (allowing you to capture document content for use in scopes) and format strings are more powerful than the 1.x variant.
* **Scopes**
Scopes now contain info beyond document context. For example the `attr.scm` scopes give info about the current files SCM status, allowing using same key equivalent for different SCM systems.
The `dyn` scopes give info such as “is there a selection” (allowing to change key bindings only when there is or isnt a selection), an example of this is overloading `{` to “wrap selection” (nicely), but leave the key alone when there is no selection. Another use-case could be to change tab to shift the text right (indent) only when there is a selection (common user request that I dont plan to make native behavior).
You can also add your own stuff to the scope via the file and folder specific settings which allows e.g. adding `attr.test` to your unit tests and overload ⌘R for that scope or have certain snippets only fire in that scope (e.g. we have a `main⇥` snippet for C which inserts the venerable main function — makes sense to overload this for (C) unit tests, which generally would be regular C files).
* **Scope selectors**
The caret is actually between two scopes, the left and right characters scope. In 2.0 we finally allow you to explicitly target either side (or both), making the firing of certain overloads more correct. There are also a few other new things related to scope selectors, but a lot of it is still only implemented in the parser, so will delay the thorough explanation of this.
* **Commands**
Commands can require other bundles which allow them to reference support files in the required bundle. They can also declare which shell commands they need which means better error handling and also having TextMate try to find the required commands. Failing commands result in more graceful error dialogs. The output options for a command has been split into location and format. Commands can run without a document.
* **Grammars**
Grammars can be injected into existing scopes, for example there are now grammars for marking URLs, `TODO`, and similar which are injected into comments and other appropriate places.
Various other things related to parsing which require its own post.
* **Semantic classes**
Bundle items can now be assigned a semantic class. This allows us to query for e.g. `action.build || action.run` (for current scope) to find an appropriate build command (with a run command as fallback). The advantage of this is manyfold: ability to do tool bars, palettes, and unified key bindings, for example we have a single _proxy item_ bound to ⌘Y which does a query for `action.scm` finding all the SCM actions relevant for the current scope (remember scope now include SCM info, making it pick the proper SCM system).
Proxy items are a new construct but still at the proof-of-concept level, so more about this later.
TextMate itself will also do queries at various times, for example when opening a file it queries for import commands, where we include several to decompile AppleScript, pretty-print binary property lists, and similar. It also uses this system on save, so by default we now make new scripts with a shebang executable after save.
Like proxy items, this system is in its infancy.
* **Managed bundles**
When you open a file type for which you have no bundle, youll be asked to install one (if TextMate knows of one). Bundles are automatically kept up to date and you can see installed bundles (and install more) in _Preferences → Bundles_.
* **Session restore**
TextMate restores the full session which includes unsaved changes incase of abnormal exit. Hold shift (⇧) during launch to bypass.
* **Foreign Input Modes**
Display of CJK and “advanced” input modes is now be supported (although only limited testing has been done).

View File

@@ -0,0 +1,23 @@
{
AppleEventCode = AviN;
Classes = {
NSApplication = {
AppleEventCode = capp;
Superclass = "NSCoreSuite.NSApplication";
};
};
Commands = {
GetURL = {
AppleEventClassCode = GURL;
AppleEventCode = GURL;
CommandClass = GetURLScriptCommand;
Type = "";
UnnamedArgument = {
Type = NSString;
Optional = NO;
};
};
};
Name = TextMate;
UsedFeatures = (UnnamedArguments);
}

View File

@@ -0,0 +1,18 @@
{
Classes = {
NSApplication = {
Description = "TextMate's top-level object.";
Name = application;
PluralName = applications;
};
};
Commands = {
GetURL = {
Description = "Open an URL";
Name = "get url";
UnnamedArgument = { Description = "url to open, should start with txmt://"; };
};
};
Description = "AppleScript commands and classes specific to TextMate.";
Name = "TextMate suite";
}

View File

@@ -0,0 +1,88 @@
#import "AppController.h"
#import <DocumentWindow/DocumentController.h>
#import <bundles/bundles.h>
#import <command/parser.h>
#import <command/runner.h>
#import <document/collection.h>
#import <editor/editor.h>
#import <ns/ns.h>
#import <settings/settings.h>
#import <OakAppKit/NSAlert Additions.h>
#import <OakAppKit/OakToolTip.h>
#import <OakFoundation/NSString Additions.h>
#import <OakSystem/application.h>
#import <plist/uuid.h>
#import <HTMLOutputWindow/HTMLOutputWindow.h>
#import <oak/CocoaSTL.h>
OAK_DEBUG_VAR(AppController_Commands);
static CGPoint MenuPosition ()
{
NSPoint pos = [NSEvent mouseLocation];
pos.y -= 16;
NSRect mainScreen = [[NSScreen mainScreen] frame];
for(NSScreen* candidate in [NSScreen screens])
{
if(NSMinX([candidate frame]) == 0 && NSMinY([candidate frame]) == 0)
mainScreen = [candidate frame];
}
CGFloat top = round(NSMaxY(mainScreen) - pos.y);
CGFloat left = round(pos.x - NSMinX(mainScreen));
return CGPointMake(top, left);
}
@implementation AppController (Commands)
- (void)performBundleItemWithUUIDString:(NSString*)uuidString
{
if(bundles::item_ptr item = bundles::lookup(to_s(uuidString)))
{
DocumentController* delegate = (DocumentController*)[[NSApp mainWindow] delegate];
if([delegate respondsToSelector:@selector(performBundleItem:)])
return [delegate performBundleItem:item];
switch(item->kind())
{
case bundles::kItemTypeSnippet:
{
// TODO set language according to snippets scope selector
// TODO mark document as “not modified”
document::document_ptr doc = document::create();
doc->open();
ng::editor_ptr editor = ng::editor_for_document(doc);
editor->snippet_dispatch(item->plist(), editor->variables(item->environment()));
document::show(doc);
doc->close();
}
break;
case bundles::kItemTypeCommand:
{
document::run(parse_command(item), ng::buffer_t(), ng::ranges_t(), document::document_ptr());
}
break;
case bundles::kItemTypeGrammar:
{
document::show(document::from_content("", item->value_for_field(bundles::kFieldGrammarScope)));
}
break;
}
}
}
- (BOOL)canHandleMenuKeyEquivalent:(NSEvent*)anEvent
{
if([[[NSApp keyWindow] delegate] isKindOfClass:[DocumentController class]])
return NO;
return !bundles::query(bundles::kFieldKeyEquivalent, to_s(anEvent), "").empty();
}
- (void)handleMenuKeyEquivalent:(id)sender
{
if(bundles::item_ptr item = bundles::show_menu_for_items(bundles::query(bundles::kFieldKeyEquivalent, to_s([NSApp currentEvent]), ""), MenuPosition()))
[self performBundleItemWithUUIDString:[NSString stringWithCxxString:item->uuid()]];
}
@end

View File

@@ -0,0 +1,193 @@
#import "AppController.h"
#import <DocumentWindow/DocumentController.h>
#import "ODBEditorSuite.h"
#import <Preferences/Keys.h>
#import <OakAppKit/NSSavePanel Additions.h>
#import <OakFoundation/NSArray Additions.h>
#import <OakFoundation/NSString Additions.h>
#import <OakFoundation/OakFoundation.h>
#import <ns/ns.h>
#import <oak/CocoaSTL.h>
#import <document/collection.h>
#import <document/session.h>
OAK_DEBUG_VAR(AppController_Documents);
static NSString* const OakGlobalSessionInfo = @"OakGlobalSessionInfo";
@implementation AppController (Documents)
- (void)newDocument:(id)sender
{
D(DBF_AppController_Documents, bug("\n"););
[[[DocumentController alloc] init] showWindow:nil];
}
- (void)newFileBrowser:(id)sender
{
D(DBF_AppController_Documents, bug("\n"););
DocumentController* controller = [[DocumentController alloc] init];
[controller window];
controller.fileBrowserHidden = NO;
[controller showWindow:self];
}
- (void)openDocument:(id)sender
{
D(DBF_AppController_Documents, bug("\n"););
NSOpenPanel* openPanel = [NSOpenPanel openPanel];
openPanel.allowsMultipleSelection = YES;
openPanel.canChooseDirectories = YES;
openPanel.canChooseFiles = YES;
openPanel.treatsFilePackagesAsDirectories = YES;
openPanel.title = [NSString stringWithFormat:@"%@: Open", [[[NSBundle mainBundle] localizedInfoDictionary] valueForKey: @"CFBundleName"] ?: [[NSProcessInfo processInfo] processName]];
[openPanel setShowsHiddenFilesCheckBox:YES];
if([openPanel runModalForTypes:nil] == NSOKButton)
OakOpenDocuments([openPanel filenames]);
}
- (BOOL)application:(NSApplication*)theApplication openFile:(NSString*)aPath
{
D(DBF_AppController_Documents, bug("%s\n", [aPath UTF8String]););
if(!DidHandleODBEditorEvent([[[NSAppleEventManager sharedAppleEventManager] currentAppleEvent] aeDesc]))
OakOpenDocuments(@[ aPath ]);
return YES;
}
- (void)application:(NSApplication*)sender openFiles:(NSArray*)filenames
{
D(DBF_AppController_Documents, bug("%s\n", [[filenames description] UTF8String]););
if(!DidHandleODBEditorEvent([[[NSAppleEventManager sharedAppleEventManager] currentAppleEvent] aeDesc]))
OakOpenDocuments(filenames);
[sender replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
}
- (BOOL)applicationOpenUntitledFile:(NSApplication*)theApplication
{
D(DBF_AppController_Documents, bug("\n"););
[[[DocumentController alloc] init] showWindow:nil];
return YES;
}
- (void)handleTxMtURL:(NSURL*)aURL
{
D(DBF_AppController_Documents, bug("%s\n", [[aURL absoluteString] UTF8String]););
if([[aURL host] isEqualToString:@"open"])
{
std::map<std::string, std::string> parameters;
NSArray* components = [[aURL query] componentsSeparatedByString:@"&"];
for(NSString* part in components)
{
NSArray* keyValue = [part componentsSeparatedByString:@"="];
if([keyValue count] == 2)
{
std::string key = to_s([[keyValue firstObject] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]);
NSURL* fileURL = key == "url" ? [NSURL URLWithString:[[keyValue lastObject] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]] : nil;
parameters[key] = to_s([fileURL isFileURL] ? [fileURL path] : [[keyValue lastObject] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]);
}
}
std::map<std::string, std::string>::const_iterator const& url = parameters.find("url");
std::map<std::string, std::string>::const_iterator const& line = parameters.find("line");
std::map<std::string, std::string>::const_iterator const& column = parameters.find("column");
std::map<std::string, std::string>::const_iterator const& project = parameters.find("project");
if(url != parameters.end())
{
std::string const& path = url->second;
if(path::is_directory(path))
{
document::show_browser(path);
}
else if(path::exists(path))
{
text::range_t range = text::range_t::undefined;
size_t col = column != parameters.end() ? atoi(column->second.c_str()) : 1;
if(line != parameters.end())
range = text::pos_t(atoi(line->second.c_str())-1, col-1);
document::document_ptr doc = document::create(path);
doc->set_recent_tracking(false);
document::show(doc, project != parameters.end() ? oak::uuid_t(project->second) : document::kCollectionCurrent, range);
}
else
{
NSRunAlertPanel(@"File Does not Exist", @"The item “%@” does not exist.", @"Continue", nil, nil, [NSString stringWithCxxString:path]);
}
}
else
{
NSRunAlertPanel(@"Missing Parameter", @"You need to provide the URL parameter.", @"Continue", nil, nil);
}
}
else
{
NSRunAlertPanel(@"Unknown URL Scheme", @"This version of TextMate does not support “%@” in its URL scheme.", @"Continue", nil, nil, [aURL host]);
}
}
- (BOOL)applicationShouldHandleReopen:(NSApplication*)theApplication hasVisibleWindows:(BOOL)flag
{
D(DBF_AppController_Documents, bug("%s\n", BSTR(flag)););
BOOL disableUntitledAtReactivationPrefs = [[NSUserDefaults standardUserDefaults] boolForKey:kUserDefaultsDisableNewDocumentAtReactivationKey];
return !disableUntitledAtReactivationPrefs;
}
// =====================
// = Load/Save Session =
// =====================
- (void)windowNotificationActual:(id)sender
{
document::schedule_session_backup();
}
- (void)windowNotification:(NSNotification*)aNotification
{
D(DBF_AppController_Documents, bug("%s\n", [[aNotification description] UTF8String]););
[self performSelector:@selector(windowNotificationActual:) withObject:nil afterDelay:0]; // A deadlock happens if we receive a notification while a sheet is closing and we save session (since session saving schedules a timer with the run loop, and the run loop is in a special state when a sheet is up, or something like that --Allan)
}
- (BOOL)loadSession:(id)sender
{
BOOL res = document::load_session();
static NSString* const WindowNotifications[] = { NSWindowDidBecomeKeyNotification, NSWindowDidDeminiaturizeNotification, NSWindowDidExposeNotification, NSWindowDidMiniaturizeNotification, NSWindowDidMoveNotification, NSWindowDidResizeNotification, NSWindowWillCloseNotification };
iterate(notification, WindowNotifications)
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(windowNotification:) name:*notification object:nil];
return res;
}
// ===========================
// = Application Termination =
// ===========================
- (void)closeAllWindows:(id)sender
{
D(DBF_AppController_Documents, bug("\n"););
NSAutoreleasePool* pool = [NSAutoreleasePool new];
for(NSWindow* window in [NSApp windows])
{
if(window.isVisible)
[window close];
}
[pool release];
}
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication*)sender
{
D(DBF_AppController_Documents, bug("%s\n", [NSApp windows].description.UTF8String););
for(NSWindow* window in [NSApp orderedWindows])
{
DocumentController* delegate = (DocumentController*)[window delegate];
if([delegate isKindOfClass:[DocumentController class]])
return [delegate applicationShouldTerminate:sender];
}
document::save_session(false);
return NSTerminateNow;
}
@end

View File

@@ -0,0 +1,215 @@
#import "AppController.h"
#import <oak/CocoaSTL.h>
#import <oak/oak.h>
#import <text/ctype.h>
#import <bundles/bundles.h>
#import <command/parser.h>
#import <cf/cf.h>
#import <ns/ns.h>
#import <OakAppKit/NSMenu Additions.h>
#import <OakAppKit/NSMenuItem Additions.h>
#import <OakAppKit/OakToolTip.h>
#import <OakFoundation/OakFoundation.h>
#import <OakFoundation/NSString Additions.h>
#import <oak/debug.h>
OAK_DEBUG_VAR(AppController_Menus);
@interface BundleMenuDelegate : NSObject <NSMenuDelegate>
{
OBJC_WATCH_LEAKS(BundleMenuDelegate);
bundles::item_ptr umbrellaItem;
}
@end
@implementation BundleMenuDelegate
- (id)initWithBundleItem:(bundles::item_ptr const&)aBundleItem
{
if(self = [super init])
umbrellaItem = aBundleItem;
return self;
}
- (BOOL)menuHasKeyEquivalent:(NSMenu*)aMenu forEvent:(NSEvent*)theEvent target:(id*)aTarget action:(SEL*)anAction
{
return NO;
}
- (void)menuNeedsUpdate:(NSMenu*)aMenu
{
D(DBF_AppController_Menus, bug("\n"););
[aMenu removeAllItems];
citerate(item, umbrellaItem->menu())
{
switch((*item)->kind())
{
case bundles::kItemTypeMenu:
{
NSMenuItem* menuItem = [aMenu addItemWithTitle:[NSString stringWithCxxString:(*item)->name()] action:NULL keyEquivalent:@""];
menuItem.submenu = [[NSMenu new] autorelease];
menuItem.submenu.autoenablesItems = NO;
BundleMenuDelegate* delegate = [[BundleMenuDelegate alloc] initWithBundleItem:*item];
menuItem.submenu.delegate = delegate;
}
break;
case bundles::kItemTypeMenuItemSeparator:
[aMenu addItem:[NSMenuItem separatorItem]];
break;
default:
{
NSMenuItem* menuItem = [aMenu addItemWithTitle:[NSString stringWithCxxString:(*item)->name()] action:@selector(doBundleItem:) keyEquivalent:@""];
[menuItem setKeyEquivalentCxxString:(*item)->value_for_field(bundles::kFieldKeyEquivalent)];
[menuItem setTabTriggerCxxString:(*item)->value_for_field(bundles::kFieldTabTrigger)];
[menuItem setRepresentedObject:[NSString stringWithCxxString:(*item)->uuid()]];
}
break;
}
}
}
- (void)menuWillOpen:(NSMenu*)aMenu
{
[aMenu enableTabTriggers];
}
- (void)menuDidClose:(NSMenu*)aMenu
{
// We are not allowed to modify aMenu here so we do it “afterDelay” — I really wish we didnt have to do this at all…
[self performSelector:@selector(zapMenu:) withObject:aMenu afterDelay:0.0];
}
- (void)zapMenu:(NSMenu*)aMenu
{
// After a menu has been up, the system will cache all its key equivalents. Even if we set all the key equivalents to the empty string, the system will still remember. The only workaround seems to be to delete all the entries in the menu.
[aMenu removeAllItems];
}
@end
@interface NSObject (BundleMenuDelegate)
- (BOOL)canHandleMenuKeyEquivalent:(NSEvent*)anEvent;
- (void)handleMenuKeyEquivalent:(id)sender;
@end
@implementation AppController (BundlesMenu)
- (void)doBundleItem:(id)anArgument
{
[NSApp sendAction:@selector(performBundleItemWithUUIDString:) to:nil from:[anArgument representedObject]];
}
- (BOOL)menuHasKeyEquivalent:(NSMenu*)aMenu forEvent:(NSEvent*)theEvent target:(id*)aTarget action:(SEL*)anAction
{
D(DBF_AppController_Menus, bug("%s (%s)\n", ns::glyphs_for_event_string(to_s(theEvent)).c_str(), to_s(theEvent).c_str()););
if(aMenu != bundlesMenu)
return NO;
*anAction = @selector(handleMenuKeyEquivalent:);
*aTarget = self;
if(id target = [NSApp targetForAction:@selector(canHandleMenuKeyEquivalent:)])
{
*aTarget = target;
return [target canHandleMenuKeyEquivalent:theEvent];
}
return NO;
}
- (void)bundlesMenuNeedsUpdate:(NSMenu*)aMenu
{
D(DBF_AppController_Menus, bug("\n"););
for(int i = aMenu.numberOfItems; i--; )
{
if([[aMenu itemAtIndex:i] isSeparatorItem])
break;
[aMenu removeItemAtIndex:i];
}
std::multimap<std::string, bundles::item_ptr, text::less_t> ordered;
citerate(item, bundles::query(bundles::kFieldAny, NULL_STR, scope::wildcard, bundles::kItemTypeBundle))
ordered.insert(std::make_pair((*item)->name(), *item));
iterate(pair, ordered)
{
if(pair->second->menu().empty())
continue;
NSMenuItem* menuItem = [aMenu addItemWithTitle:[NSString stringWithCxxString:pair->first] action:NULL keyEquivalent:@""];
menuItem.submenu = [NSMenu new];
menuItem.submenu.autoenablesItems = NO;
BundleMenuDelegate* delegate = [[BundleMenuDelegate alloc] initWithBundleItem:pair->second];
menuItem.submenu.delegate = delegate;
}
if(ordered.empty())
[aMenu addItemWithTitle:@"No Bundles Loaded" action:@selector(nop:) keyEquivalent:@""];
}
- (void)themesMenuNeedsUpdate:(NSMenu*)aMenu
{
D(DBF_AppController_Menus, bug("\n"););
[aMenu removeAllItems];
std::multimap<std::string, bundles::item_ptr, text::less_t> ordered;
citerate(item, bundles::query(bundles::kFieldAny, NULL_STR, scope::wildcard, bundles::kItemTypeTheme))
ordered.insert(std::make_pair((*item)->name(), *item));
iterate(pair, ordered)
{
NSMenuItem* menuItem = [aMenu addItemWithTitle:[NSString stringWithCxxString:pair->first] action:@selector(takeThemeUUIDFrom:) keyEquivalent:@""];
[menuItem setKeyEquivalentCxxString:pair->second->value_for_field(bundles::kFieldKeyEquivalent)];
[menuItem setRepresentedObject:[NSString stringWithCxxString:pair->second->uuid()]];
}
if(ordered.empty())
[aMenu addItemWithTitle:@"No Themes Loaded" action:@selector(nop:) keyEquivalent:@""];
}
- (void)spellingMenuNeedsUpdate:(NSMenu*)aMenu
{
D(DBF_AppController_Menus, bug("\n"););
for(int i = aMenu.numberOfItems; i--; )
{
NSMenuItem* item = [aMenu itemAtIndex:i];
if([item action] == @selector(takeSpellingLanguageFrom:))
{
[[item retain] autorelease];
[aMenu removeItemAtIndex:i];
}
}
std::multimap<std::string, NSString*, text::less_t> ordered;
NSSpellChecker* spellChecker = [NSSpellChecker sharedSpellChecker];
for(NSString* lang in [spellChecker availableLanguages])
{
D(DBF_AppController_Menus, bug("%s\n", [lang UTF8String]););
CFStringRef str = CFLocaleCopyDisplayNameForPropertyValue(CFLocaleGetSystem(), kCFLocaleIdentifier, (CFStringRef)lang);
D(DBF_AppController_Menus, bug("→ %s\n", cf::to_s(str ?: (CFStringRef)lang).c_str()););
ordered.insert(std::make_pair(cf::to_s(str ?: (CFStringRef)lang), lang));
if(str)
CFRelease(str);
}
iterate(it, ordered)
{
D(DBF_AppController_Menus, bug("Add Item: %s\n", it->first.c_str()););
NSMenuItem* menuItem = [aMenu addItemWithTitle:[NSString stringWithCxxString:it->first] action:@selector(takeSpellingLanguageFrom:) keyEquivalent:@""];
D(DBF_AppController_Menus, bug("Represented Object: %s\n", [it->second UTF8String]););
menuItem.representedObject = it->second;
}
}
- (void)menuNeedsUpdate:(NSMenu*)aMenu
{
if(aMenu == bundlesMenu)
[self bundlesMenuNeedsUpdate:aMenu];
else if(aMenu == themesMenu)
[self themesMenuNeedsUpdate:aMenu];
else if(aMenu == spellingMenu)
[self spellingMenuNeedsUpdate:aMenu];
}
@end

View File

@@ -0,0 +1,54 @@
extern NSString* const OakSessionDidChangeNotification;
@class OakFilterWindowController;
namespace find_tags
{
enum
{
in_document = 1,
in_selection,
in_project,
in_folder,
};
}
@interface AppController : NSObject <NSMenuDelegate>
{
IBOutlet NSMenu* bundlesMenu;
IBOutlet NSMenu* themesMenu;
IBOutlet NSMenu* spellingMenu;
IBOutlet NSPanel* goToLinePanel;
IBOutlet NSTextField* goToLineTextField;
struct
{
std::string filter_string;
BOOL key_equivalent;
BOOL all_scopes;
int search_type;
} bundleItemSearch;
OakFilterWindowController* filterWindowController;
}
- (IBAction)orderFrontFindPanel:(id)sender;
- (IBAction)orderFrontGoToLinePanel:(id)sender;
- (IBAction)performGoToLine:(id)sender;
- (IBAction)showBundleItemChooser:(id)sender;
- (IBAction)showPreferences:(id)sender;
- (IBAction)showBundleEditor:(id)sender;
- (void)setup;
@end
@interface AppController (Documents)
- (void)newDocument:(id)sender;
- (BOOL)loadSession:(id)sender;
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication*)sender;
@end
void OakOpenDocuments (NSArray* paths);

View File

@@ -0,0 +1,234 @@
#import "AppController.h"
#import "Favorites.h"
#import "CreditsWindowController.h"
#import <oak/CocoaSTL.h>
#import <oak/oak.h>
#import <oak/debug.h>
#import <Find/Find.h>
#import <io/path.h>
#import <OakFoundation/OakFoundation.h>
#import <OakFoundation/NSString Additions.h>
#import <BundleEditor/BundleEditor.h>
#import <OakAppKit/OakAppKit.h>
#import <OakAppKit/OakPasteboard.h>
#import <OakFilterList/OakFilterList.h>
#import <OakFilterList/BundleItemChooser.h>
#import <OakTextView/OakDocumentView.h>
#import <Preferences/Preferences.h>
#import <text/types.h>
#import <document/collection.h>
#import <ns/ns.h>
OAK_DEBUG_VAR(AppController);
void OakOpenDocuments (NSArray* paths)
{
std::vector<document::document_ptr> documents;
for(NSString* path in paths)
{
if(path::is_directory(to_s(path)))
{
document::show_browser(to_s(path));
}
else
{
documents.push_back(document::create(to_s(path)));
}
}
document::show(documents);
}
@interface AppController ()
@property (nonatomic, retain) OakFilterWindowController* filterWindowController;
@end
@implementation AppController
@synthesize filterWindowController;
- (void)setup
{
bundlesMenu.delegate = self;
themesMenu.delegate = self;
spellingMenu.delegate = self;
[NSApp setDelegate:self];
}
- (IBAction)orderFrontFindPanel:(id)sender
{
D(DBF_AppController, bug("\n"););
Find* find = [Find sharedInstance];
int mode = [sender respondsToSelector:@selector(tag)] ? [sender tag] : find_tags::in_document;
if(mode == find_tags::in_folder)
return [find showFolderSelectionPanel:self];
switch(mode)
{
case find_tags::in_document:
find.searchScope = find::in::document;
break;
case find_tags::in_selection:
find.searchScope = find::in::selection;
break;
case find_tags::in_project:
find.searchFolder = find.projectFolder;
find.searchScope = find::in::folder;
break;
}
[find showFindPanel:self];
}
- (IBAction)orderFrontGoToLinePanel:(id)sender;
{
D(DBF_AppController, bug("\n"););
[goToLinePanel makeKeyAndOrderFront:self];
}
- (IBAction)performGoToLine:(id)sender
{
D(DBF_AppController, bug("\n"););
[goToLinePanel orderOut:self];
[NSApp sendAction:@selector(setSelectionString:) to:nil from:[goToLineTextField stringValue]];
}
- (IBAction)showPreferences:(id)sender
{
D(DBF_AppController, bug("\n"););
[[Preferences sharedInstance] showWindow:self];
}
- (IBAction)showBundleEditor:(id)sender
{
D(DBF_AppController, bug("\n"););
[[BundleEditor sharedInstance] showWindow:self];
}
- (IBAction)openFavorites:(id)sender
{
OakFilterWindowController* controller = [OakFilterWindowController filterWindow];
controller.dataSource = [FavoritesDataSource favoritesDataSource];
controller.action = @selector(didSelectFavorite:);
controller.allowsMultipleSelection = YES;
[controller showWindow:self];
}
- (void)didSelectFavorite:(id)sender
{
NSMutableArray* paths = [NSMutableArray array];
for(id item in [sender selectedItems])
[paths addObject:[item objectForKey:@"path"]];
OakOpenDocuments(paths);
}
- (IBAction)showCredits:(id)sender
{
D(DBF_AppController, bug("\n"););
[CreditsWindowController showPath:[[NSBundle mainBundle] pathForResource:@"Credits" ofType:@"html"]];
}
// =======================
// = Bundle Item Chooser =
// =======================
- (void)setFilterWindowController:(OakFilterWindowController*)controller
{
if(controller != filterWindowController)
{
if(filterWindowController)
{
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSWindowWillCloseNotification object:filterWindowController.window];
filterWindowController.target = nil;
[filterWindowController close];
[filterWindowController release];
}
if(filterWindowController = [controller retain])
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(filterWindowWillClose:) name:NSWindowWillCloseNotification object:filterWindowController.window];
}
}
- (void)filterWindowWillClose:(NSNotification*)notification
{
bundleItemSearch.filter_string = [[[filterWindowController dataSource] filterString] UTF8String];
bundleItemSearch.key_equivalent = [[filterWindowController dataSource] keyEquivalentSearch];
bundleItemSearch.all_scopes = [[filterWindowController dataSource] searchAllScopes];
bundleItemSearch.search_type = [[filterWindowController dataSource] searchType];
self.filterWindowController = nil;
}
- (IBAction)showBundleItemChooser:(id)sender
{
self.filterWindowController = [OakFilterWindowController filterWindow];
OakTextView* textView = [NSApp targetForAction:@selector(scope)];
BundleItemChooser* dataSource = [BundleItemChooser bundleItemChooserForScope:textView ? [textView scope] : scope::wildcard];
dataSource.searchType = search::type(bundleItemSearch.search_type);
dataSource.keyEquivalentSearch = bundleItemSearch.key_equivalent;
dataSource.searchAllScopes = bundleItemSearch.all_scopes;
dataSource.filterString = [NSString stringWithCxxString:bundleItemSearch.filter_string];
filterWindowController.dataSource = dataSource;
filterWindowController.action = @selector(bundleItemChooserDidSelectItems:);
filterWindowController.accessoryAction = @selector(editBundleItem:);
[filterWindowController showWindow:self];
}
- (void)bundleItemChooserDidSelectItems:(id)sender
{
for(NSDictionary* item in [sender selectedItems])
{
if(OakIsAlternateKeyOrMouseEvent())
[[BundleEditor sharedInstance] revealBundleItem:bundles::lookup(to_s((NSString*)[item objectForKey:@"uuid"]))];
else [NSApp sendAction:@selector(performBundleItemWithUUIDString:) to:nil from:[item objectForKey:@"uuid"]];
}
}
// ===========================
// = Find options menu items =
// ===========================
- (IBAction)toggleFindOption:(id)sender
{
[[Find sharedInstance] takeFindOptionToToggleFrom:sender];
}
- (BOOL)validateMenuItem:(NSMenuItem*)item
{
BOOL enabled = YES;
if([item action] == @selector(toggleFindOption:))
{
BOOL active = NO;
if(OakPasteboardEntry* entry = [[OakPasteboard pasteboardWithName:NSFindPboard] current])
{
switch([item tag])
{
case find::ignore_case: active = [[NSUserDefaults standardUserDefaults] boolForKey:kUserDefaultsFindIgnoreCase]; break;
case find::regular_expression: active = [entry regularExpression]; break;
case find::full_words: active = [entry fullWordMatch]; enabled = ![entry regularExpression]; break;
case find::ignore_whitespace: active = [entry ignoreWhitespace]; enabled = ![entry regularExpression]; break;
case find::wrap_around: active = [[NSUserDefaults standardUserDefaults] boolForKey:kUserDefaultsFindWrapAround]; break;
}
[item setState:(active ? NSOnState : NSOffState)];
}
else
{
enabled = NO;
}
}
else if([item action] == @selector(orderFrontGoToLinePanel:))
{
enabled = [NSApp targetForAction:@selector(setSelectionString:)] != nil;
}
return enabled;
}
- (void)editBundleItem:(id)sender
{
ASSERT([sender respondsToSelector:@selector(selectedItems)]);
ASSERT([[sender selectedItems] count] == 1);
self.filterWindowController = nil;
if(NSString* uuid = [[[sender selectedItems] lastObject] objectForKey:@"uuid"])
[[BundleEditor sharedInstance] revealBundleItem:bundles::lookup(to_s(uuid))];
}
@end

View File

@@ -0,0 +1,13 @@
@class AppController;
@interface AppStartupController : NSObject
{
IBOutlet AppController* appController;
NSAppleEventDescriptor* openEvent;
NSArray* openDocumentsArray;
BOOL disableSessionRestore;
}
@property (nonatomic, retain) NSAppleEventDescriptor* openEvent;
@property (nonatomic, retain) NSArray* openDocumentsArray;
@end

View File

@@ -0,0 +1,162 @@
#import "AppStartup.h"
#import "AppController.h"
#import <DocumentWindow/DocumentController.h>
#import "ODBEditorSuite.h"
#import "TMPlugInController.h"
#import "RMateServer.h"
#import <Preferences/Keys.h>
#import <OakFoundation/NSString Additions.h>
#import <OakAppKit/NSEvent Additions.h>
#import <OakFoundation/OakFoundation.h>
#import <CrashReporter/CrashReporter.h>
#import <SoftwareUpdate/SoftwareUpdate.h>
#import <BundlesManager/BundlesManager.h>
#import <io/path.h>
#import <bundles/bundles.h>
#import <ns/ns.h>
#import <network/tbz.h>
#import <oak/server.h>
#import <oak/debug.h>
OAK_DEBUG_VAR(AppStartup);
@implementation AppStartupController
@synthesize openEvent, openDocumentsArray;
- (void)userDefaultsDidChange:(id)sender
{
BOOL disableRmate = [[NSUserDefaults standardUserDefaults] boolForKey:kUserDefaultsDisableRMateServerKey];
NSString* rmateInterface = [[NSUserDefaults standardUserDefaults] stringForKey:kUserDefaultsRMateServerListenKey];
int rmatePort = [[NSUserDefaults standardUserDefaults] integerForKey:kUserDefaultsRMateServerPortKey];
setup_rmate_server(!disableRmate, [rmateInterface isEqualToString:kRMateServerListenRemote] ? INADDR_ANY : INADDR_LOOPBACK, rmatePort);
}
- (void)applicationWillFinishLaunching:(NSNotification*)aNotification
{
D(DBF_AppStartup, bug("\n"););
[[NSUserDefaults standardUserDefaults] registerDefaults:[NSDictionary dictionaryWithObjectsAndKeys:
NO_obj, @"ApplePressAndHoldEnabled",
@25, @"NSRecentDocumentsLimit",
nil]];
RegisterDefaults();
[[NSUserDefaults standardUserDefaults] setObject:NO_obj forKey:@"NSQuitAlwaysKeepsWindows"];
disableSessionRestore = ([NSEvent slModifierFlags] & NSShiftKeyMask) == NSShiftKeyMask;
std::string dest = path::join(path::home(), "Library/Application Support/TextMate/Managed");
if(!path::exists(dest))
{
if(NSString* archive = [[NSBundle mainBundle] pathForResource:@"DefaultBundles" ofType:@"tbz"])
{
int input, output;
std::string error;
path::make_dir(dest);
pid_t pid = network::launch_tbz(dest, input, output, error);
if(pid != -1)
{
int fd = open([archive fileSystemRepresentation], O_RDONLY);
if(fd != -1)
{
char buf[4096];
ssize_t len;
while((len = read(fd, buf, sizeof(buf))) > 0)
{
if(write(input, buf, len) != len)
{
fprintf(stderr, "*** error wrting bytes to tar\n");
break;
}
}
close(fd);
}
if(!network::finish_tbz(pid, input, output, error))
fprintf(stderr, "%s\n", error.c_str());
}
else
{
fprintf(stderr, "%s\n", error.c_str());
}
}
}
bundles::build_index(path::join(path::home(), "Library/Application Support/TextMate/Cache"));
}
- (BOOL)application:(NSApplication*)theApplication openFile:(NSString*)aPath
{
D(DBF_AppStartup, bug("%s\n", [aPath UTF8String]););
self.openEvent = [[NSAppleEventManager sharedAppleEventManager] currentAppleEvent];
self.openDocumentsArray = @[ aPath ];
return YES;
}
- (void)application:(NSApplication*)sender openFiles:(NSArray*)filenames
{
D(DBF_AppStartup, bug("%s\n", [[filenames description] UTF8String]););
self.openEvent = [[NSAppleEventManager sharedAppleEventManager] currentAppleEvent];
self.openDocumentsArray = filenames;
[sender replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
}
- (BOOL)applicationShouldOpenUntitledFile:(NSApplication*)anApplication
{
D(DBF_AppStartup, bug("\n"););
return NO;
}
- (void)applicationDidFinishLaunching:(NSNotification*)aNotification
{
D(DBF_AppStartup, bug("\n"););
OakSubmitNewCrashReportsInBackground(REST_API @"/crashes");
[[TMPlugInController sharedInstance] loadAllPlugIns:nil];
[self userDefaultsDidChange:nil]; // setup mate/rmate server
[BundlesManager sharedInstance]; // trigger periodic polling of remote bundle index
BOOL disableSessionRestorePrefs = [[NSUserDefaults standardUserDefaults] boolForKey:kUserDefaultsDisableSessionRestoreKey];
BOOL disableUntitledAtStartupPrefs = [[NSUserDefaults standardUserDefaults] boolForKey:kUserDefaultsDisableNewDocumentAtStartupKey];
BOOL didRestoreSession = !disableSessionRestorePrefs && !disableSessionRestore && [appController loadSession:self];
BOOL didOpenDocuments = DidHandleODBEditorEvent([self.openEvent aeDesc]) || ([self.openDocumentsArray count] && (OakOpenDocuments(self.openDocumentsArray), YES));
if(!disableUntitledAtStartupPrefs && !didRestoreSession && !didOpenDocuments && getenv("OAK_DISABLE_UNTITLED_FILE") == NULL)
[[[DocumentController alloc] init] showWindow:nil];
SoftwareUpdate* swUpdate = [SoftwareUpdate sharedInstance];
[swUpdate setSignee:key_chain_t::key_t("org.textmate.duff", "Allan Odgaard", "-----BEGIN PUBLIC KEY-----\nMIIBtjCCASsGByqGSM44BAEwggEeAoGBAPIE9PpXPK3y2eBDJ0dnR/D8xR1TiT9m\n8DnPXYqkxwlqmjSShmJEmxYycnbliv2JpojYF4ikBUPJPuerlZfOvUBC99ERAgz7\nN1HYHfzFIxVo1oTKWurFJ1OOOsfg8AQDBDHnKpS1VnwVoDuvO05gK8jjQs9E5LcH\ne/opThzSrI7/AhUAy02E9H7EOwRyRNLofdtPxpa10o0CgYBKDfcBscidAoH4pkHR\nIOEGTCYl3G2Pd1yrblCp0nCCUEBCnvmrWVSXUTVa2/AyOZUTN9uZSC/Kq9XYgqwj\nhgzqa8h/a8yD+ao4q8WovwGeb6Iso3WlPl8waz6EAPR/nlUTnJ4jzr9t6iSH9owS\nvAmWrgeboia0CI2AH++liCDvigOBhAACgYAFWO66xFvmF2tVIB+4E7CwhrSi2uIk\ndeBrpmNcZZ+AVFy1RXJelNe/cZ1aXBYskn/57xigklpkfHR6DGqpEbm6KC/47Jfy\ny5GEx+F/eBWEePi90XnLinytjmXRmS2FNqX6D15XNG1xJfjociA8bzC7s4gfeTUd\nlpQkBq2z71yitA==\n-----END PUBLIC KEY-----\n")];
[swUpdate setChannels:[NSDictionary dictionaryWithObjectsAndKeys:
[NSURL URLWithString:REST_API @"/releases/release"], kSoftwareUpdateChannelRelease,
[NSURL URLWithString:REST_API @"/releases/beta"], kSoftwareUpdateChannelBeta,
[NSURL URLWithString:REST_API @"/releases/nightly"], kSoftwareUpdateChannelNightly,
nil]];
self.openEvent = nil;
self.openDocumentsArray = nil;
unsetenv("OAK_DISABLE_UNTITLED_FILE");
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(userDefaultsDidChange:) name:NSUserDefaultsDidChangeNotification object:[NSUserDefaults standardUserDefaults]];
[appController setup];
}
- (BOOL)applicationShouldHandleReopen:(NSApplication*)theApplication hasVisibleWindows:(BOOL)flag
{
D(DBF_AppStartup, bug("%s\n", BSTR(flag)););
return NO;
}
- (IBAction)newDocument:(id)sender
{
// avoid NSDocumentControllers implementation
}
- (BOOL)validateMenuItem:(NSMenuItem*)menuItem
{
if([menuItem action] == @selector(newDocument:))
return NO;
return YES;
}
@end

View File

@@ -0,0 +1,11 @@
#include <oak/debug.h>
@interface CreditsWindowController : NSWindowController
{
OBJC_WATCH_LEAKS(CreditsWindowController);
IBOutlet WebView* webView;
NSURL* creditsURL;
}
+ (void)showPath:(NSString*)aPath;
@end

View File

@@ -0,0 +1,43 @@
#import "CreditsWindowController.h"
@interface CreditsWindowController ()
@property (nonatomic, retain) NSURL* creditsURL;
@end
@implementation CreditsWindowController
@synthesize creditsURL;
- (id)initWithURL:(NSURL*)aURL
{
if(self = [super initWithWindowNibName:@"CreditsWindow"])
{
self.creditsURL = aURL;
}
return self;
}
+ (void)showPath:(NSString*)aPath
{
[[[self alloc] initWithURL:[NSURL fileURLWithPath:aPath]] showWindow:nil];
}
- (void)windowDidLoad
{
[[webView mainFrame] loadRequest:[NSURLRequest requestWithURL:creditsURL cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:60]];
[webView setPolicyDelegate:self];
}
- (void)webView:(WebView*)sender decidePolicyForNavigationAction:(NSDictionary*)actionInformation request:(NSURLRequest*)request frame:(WebFrame*)frame decisionListener:(id <WebPolicyDecisionListener>)listener
{
if([[NSWorkspace sharedWorkspace] openURL:request.URL])
[listener ignore];
else if([NSURLConnection canHandleRequest:request])
[listener use];
}
- (void)windowWillClose:(NSNotification*)aNotification
{
[webView setPolicyDelegate:nil];
[self release];
}
@end

View File

@@ -0,0 +1,16 @@
#import <OakFilterList/OakFilterList.h>
#import <text/ctype.h>
@class FavoritesViewController;
@interface FavoritesDataSource : NSObject <FilterListDataSource>
{
OBJC_WATCH_LEAKS(FavoritesDataSource);
std::string favoritesPath;
std::multimap<std::string, std::string, text::less_t> favorites;
std::string filterString;
FavoritesViewController* viewController;
}
+ (FavoritesDataSource*)favoritesDataSource;
@property (nonatomic, readonly) NSString* filterString;
@end

View File

@@ -0,0 +1,157 @@
#import "Favorites.h"
#import <OakFoundation/NSString Additions.h>
#import <OakSystem/application.h>
#import <text/ranker.h>
#import <oak/CocoaSTL.h>
#import <io/entries.h>
#import <text/case.h>
#import <io/path.h>
#import <ns/ns.h>
// ===================
// = View Controller =
// ===================
@interface FavoritesViewController : NSViewController
{
NSSearchField* searchField;
FavoritesDataSource* favoritesDataSource;
}
@end
@implementation FavoritesViewController
- (id)initWithFavoritesDataSource:(FavoritesDataSource*)aDataSource
{
if(self = [super init])
{
favoritesDataSource = aDataSource;
searchField = [[[NSSearchField alloc] initWithFrame:NSMakeRect(10, 10, 180, 22)] autorelease];
searchField.autoresizingMask = NSViewWidthSizable|NSViewMinYMargin;
searchField.target = favoritesDataSource;
searchField.action = @selector(search:);
[searchField.cell setScrollable:YES];
self.view = [[[NSView alloc] initWithFrame:NSMakeRect(0, 0, 200, NSMaxY(searchField.frame) + 8)] autorelease];
self.view.autoresizingMask = NSViewWidthSizable|NSViewMinYMargin;
[self.view addSubview:searchField];
}
return self;
}
- (void)dealloc
{
searchField.target = nil;
searchField.action = NULL;
[super dealloc];
}
- (void)setSearchFieldDelegate:(id)aDelegate
{
searchField.delegate = aDelegate;
}
@end
// ===============
// = Data Source =
// ===============
@implementation FavoritesDataSource
- (NSViewController*)viewController
{
if(!viewController)
viewController = [[FavoritesViewController alloc] initWithFavoritesDataSource:self];
return viewController;
}
- (id)initWithCxxPath:(std::string const&)aPath
{
if(self = [super init])
{
favoritesPath = aPath;
filterString = "";
citerate(entry, path::entries(favoritesPath))
{
if((*entry)->d_type == DT_LNK)
favorites.insert(std::make_pair((*entry)->d_name, path::resolve(path::join(favoritesPath, (*entry)->d_name))));
}
}
return self;
}
+ (FavoritesDataSource*)favoritesDataSource
{
return [[[self alloc] initWithCxxPath:oak::application_t::support("Favorites")] autorelease];
}
- (NSString*)title
{
return @"Open Favorite";
}
- (NSString*)filterString
{
return [NSString stringWithCxxString:filterString];
}
- (IBAction)search:(id)sender
{
ASSERT([sender respondsToSelector:@selector(stringValue)]);
NSString* objCStr = [[sender stringValue] lowercaseString] ?: @"";
std::string const newFilterString = to_s(objCStr);
if(newFilterString != filterString)
{
filterString = newFilterString;
[[NSNotificationCenter defaultCenter] postNotificationName:FLDataSourceItemsDidChangeNotification object:self];
}
}
- (NSArray*)items
{
std::multimap< double, std::pair<std::string, std::string> > ranked;
iterate(pair, favorites)
{
if(filterString == "")
{
ranked.insert(std::make_pair(ranked.size(), *pair));
}
else
{
double rank = oak::rank(filterString, pair->first);
if(rank > 0)
ranked.insert(std::make_pair(-rank, *pair));
}
}
NSMutableArray* items = [NSMutableArray array];
iterate(pair, ranked)
{
[items addObject:
[NSDictionary dictionaryWithObjectsAndKeys:
[NSString stringWithCxxString:pair->second.first], @"title",
[NSString stringWithCxxString:pair->second.second], @"path",
nil]
];
}
return items;
}
- (NSAttributedString*)displayStringForItem:(id)anItem
{
std::string str = to_s((NSString*)[anItem objectForKey:@"title"]);
return [[[NSAttributedString alloc] initWithString:[NSString stringWithCxxString:str]] autorelease];
}
- (NSAttributedString*)infoStringForItem:(id)anItem
{
std::string str = path::with_tilde(to_s((NSString*)[anItem objectForKey:@"path"]));
return [[[NSAttributedString alloc] initWithString:[NSString stringWithCxxString:str]] autorelease];
}
- (void)dealloc
{
[viewController release];
[super dealloc];
}
@end

View File

@@ -0,0 +1,18 @@
#import <document/collection.h>
#import <oak/CocoaSTL.h>
#import <OakFoundation/NSArray Additions.h>
#import <io/path.h>
@interface GetURLScriptCommand : NSScriptCommand
@end
@implementation GetURLScriptCommand
- (id)performDefaultImplementation
{
NSString* urlString = [self directParameter];
if([urlString hasPrefix:@"txmt:"] && ![urlString hasPrefix:@"txmt://"])
urlString = [@"txmt://" stringByAppendingString:[urlString substringFromIndex:5]];
[NSApp sendAction:@selector(handleTxMtURL:) to:nil from:[NSURL URLWithString:urlString]];
return nil;
}
@end

View File

@@ -0,0 +1,184 @@
#include "ODBEditorSuite.h"
#include <document/collection.h>
#include <oak/debug.h>
#include <text/hexdump.h>
#include <oak/oak.h>
OAK_DEBUG_VAR(ODBEditorSuite);
struct ae_record_t;
typedef std::tr1::shared_ptr<ae_record_t> ae_record_ptr;
struct ae_record_t
{
WATCH_LEAKS(ae_record_t);
ae_record_t (AEDesc const& value) : value(value) { }
ae_record_t (AEDesc const* value) { AEDuplicateDesc(value, &this->value); }
~ae_record_t () { AEDisposeDesc(&value); }
DescType type () const { return value.descriptorType; }
std::string data () const { std::string str(AEGetDescDataSize(&value), ' '); AEGetDescData(&value, &str[0], str.size()); return str; }
size_t array_size () const { long res; AECountItems(&value, &res); return res; }
std::string path () const
{
UInt8 buf[PATH_MAX];
std::string const& fsref = data();
if(noErr == FSRefMakePath((FSRef const*)&fsref[0], buf, sizeof(buf)))
return std::string((char*)buf);
return NULL_STR;
}
ae_record_ptr record_at_index (size_t i, DescType type = typeWildCard)
{
AEDesc item;
if(noErr == AEGetNthDesc(&value, i+1, type, NULL, &item))
return ae_record_ptr(new ae_record_t(item));
return ae_record_ptr();
}
ae_record_ptr record_for_key (AEKeyword key, DescType type = typeWildCard)
{
AEDesc item;
if(noErr == AEGetParamDesc(&value, key, type, &item))
return ae_record_ptr(new ae_record_t(item));
return ae_record_ptr();
}
private:
AEDesc value;
};
namespace odb // wrap in namespace to avoid clashing with other callbacks named the same
{
struct save_close_callback_t : document::document_t::callback_t
{
WATCH_LEAKS(save_close_callback_t);
save_close_callback_t (std::string const& path, std::string const& token, ae_record_ptr const& sender) : path(path), token(token), sender(sender)
{
}
void handle_document_event (document::document_ptr document, event_t event)
{
if(event == did_change_open_status && !document->is_open())
send_event(kAEClosedFile);
else if(event == did_save)
send_event(kAEModifiedFile);
if(event == did_change_open_status && !document->is_open())
{
document->remove_callback(this);
delete this;
}
}
private:
void send_event (AEEventID eventId) const
{
D(DBF_ODBEditorSuite, int c = htonl(eventId); bug("%.4s\n", (char*)&c););
FSRef fsRef;
if(noErr == FSPathMakeRef((UInt8 const*)path.c_str(), &fsRef, NULL))
{
AEAddressDesc target;
std::string const& senderData = sender->data();
D(DBF_ODBEditorSuite, int t = htonl(sender->type()); bug("send to: %.*s (%.4s)\n", (int)senderData.size(), senderData.data(), (char*)&t););
AECreateDesc(sender->type() == typeType ? typeApplSignature : sender->type(), senderData.data(), senderData.size(), &target);
AppleEvent event;
AECreateAppleEvent(kODBEditorSuite, eventId, &target, kAutoGenerateReturnID, kAnyTransactionID, &event);
AEPutParamPtr(&event, keyDirectObject, typeFSRef, &fsRef, sizeof(fsRef));
if(token != NULL_STR)
AEPutParamPtr(&event, keySenderToken, typeWildCard, token.data(), token.size());
AppleEvent reply;
OSStatus err DB_VAR = AESendMessage(&event, &reply, kAENoReply, kAEDefaultTimeout);
D(DBF_ODBEditorSuite, if(err != noErr) bug("*** AESendMessage(): error %d\n", (int)err););
AEDisposeDesc(&event);
AEDisposeDesc(&target);
}
}
std::string path;
std::string token;
ae_record_ptr sender;
};
}
bool DidHandleODBEditorEvent (AppleEvent const* event)
{
if(!event)
return false;
D(DBF_ODBEditorSuite, int c = htonl(event->descriptorType); bug("descriptor: %.4s\n", (char*)&c););
// open content: file:///Developer/Documentation/DocSets/com.apple.ADC_Reference_Library.CoreReference.docset/Contents/Resources/Documents/documentation/AppleScript/Conceptual/AppleEvents/responding_aepg/chapter_6_section_4.html
DescType attr;
if(noErr != AEGetAttributePtr(event, keyEventClassAttr, typeType, NULL, &attr, sizeof(attr), NULL) || attr != kCoreEventClass)
return false;
if(noErr != AEGetAttributePtr(event, keyEventIDAttr, typeType, NULL, &attr, sizeof(attr), NULL) || attr != kAEOpenDocuments)
return false;
D(DBF_ODBEditorSuite, bug("Got odoc event\n"););
ae_record_t ae(event);
ae_record_ptr files = ae.record_for_key(keyDirectObject, typeAEList);
ae_record_ptr displayNames = ae.record_for_key(keyFileCustomPath, typeAEList);
ae_record_ptr positions = ae.record_for_key(keyAEPosition, typeAEList);
ae_record_ptr sender = ae.record_for_key(keyServerID, typeWildCard);
ae_record_ptr tokens = ae.record_for_key(keyFileSenderToken, typeAEList);
ae_record_ptr searchTexts = ae.record_for_key(keyAESearchText, typeAEList);
if(ae_record_ptr const& extra = ae.record_for_key(keyAEPropData, typeAERecord))
{
if(!displayNames)
displayNames = extra->record_for_key(keyFileCustomPath, typeAEList);
if(!sender)
sender = extra->record_for_key(keyServerID, typeWildCard);
if(!tokens)
tokens = extra->record_for_key(keyFileSenderToken, typeAEList);
}
if(sender || positions || displayNames)
{
std::vector<document::document_ptr> documents;
for(size_t i = 0; i < files->array_size(); ++i)
{
ae_record_ptr file = files->record_at_index(i, typeFSRef);
documents.push_back(document::create(file->path()));
documents.back()->set_recent_tracking(false);
if(displayNames && i < displayNames->array_size())
documents.back()->set_custom_name(displayNames->record_at_index(i)->data());
if(positions && i < positions->array_size())
{
std::string const& v = positions->record_at_index(i)->data();
if(v.size() == sizeof(PBX_SelectionRange))
{
PBX_SelectionRange& sel = *(PBX_SelectionRange*)&v[0];
if(sel.lineNum >= 0)
documents.back()->set_selection(text::pos_t(sel.lineNum, 0));
}
}
if(sender)
{
std::string token = (tokens && i < tokens->array_size()) ? tokens->record_at_index(i)->data() : NULL_STR;
documents.back()->add_callback(new odb::save_close_callback_t(file->path(), token, sender));
D(DBF_ODBEditorSuite, int c = htonl(sender->type()); bug("server: %.*s (%.4s), token: %s\n", (int)sender->data().size(), sender->data().data(), (char*)&c, token != NULL_STR ? token.c_str() : "(none)"););
}
}
document::show(documents);
return true;
}
return false;
}

View File

@@ -0,0 +1,34 @@
#ifndef ODB_EDITOR_SUITE_H_AT0LJGEF
#define ODB_EDITOR_SUITE_H_AT0LJGEF
bool DidHandleODBEditorEvent (AppleEvent const* event);
// optional paramters to 'aevt'/'odoc'
#define keyServerID 'FSnd'
#define keyFileSenderToken 'FTok'
#define keyFileCustomPath 'Burl'
// suite code for ODB editor suite events
#define kODBEditorSuite 'R*ch'
// ODB editor suite events, sent by the editor to the server.
#define kAEModifiedFile 'FMod'
#define keyNewLocation 'New?'
#define kAEClosedFile 'FCls'
// optional paramter to kAEModifiedFile/kAEClosedFile
#define keySenderToken 'Tokn'
#pragma options align=mac68k
struct PBX_SelectionRange
{
short unused1; // 0 (not used)
short lineNum; // line to select (<0 to specify range)
long startRange; // start of selection range (if line < 0)
long endRange; // end of selection range (if line < 0)
long unused2; // 0 (not used)
long theDate; // modification date/time
};
#pragma options align=reset
#endif /* end of include guard: ODB_EDITOR_SUITE_H_AT0LJGEF */

View File

@@ -0,0 +1,590 @@
#include <oak/oak.h>
#include <text/parse.h>
#include <text/hexdump.h>
#include <document/collection.h>
#include <oak/debug.h>
#include <authorization/authorization.h>
#include <io/io.h>
OAK_DEBUG_VAR(RMateServer);
#define SOCKET_PATH "/tmp/avian.sock"
/*
open
path: [«path»|-]
real-path: «path»
token: «string»
display-name: «string»
selection: «line»[:«column»][-«line»[:«column»]]
file-type: «scope»
project-uuid: «uuid»
add-to-recents: «boolean»
re-activate: «boolean»
authorization: «blob»
wait: «boolean»
data-on-save: «boolean»
data-on-close: «boolean»
data: «integer»
data: «integer»
*/
// ============================
// = Socket run loop callback =
// ============================
struct socket_callback_t
{
WATCH_LEAKS(socket_callback_t);
template <typename F>
socket_callback_t (F f, socket_t const& fd)
{
D(DBF_RMateServer, bug("%p, %d\n", this, (int)fd););
helper.reset(new helper_t<F>(f, fd, this));
CFSocketContext const context = { 0, helper.get(), NULL, NULL, NULL };
socket = CFSocketCreateWithNative(kCFAllocatorDefault, fd, kCFSocketReadCallBack, callback, &context);
CFSocketSetSocketFlags(socket, CFSocketGetSocketFlags(socket) & ~kCFSocketCloseOnInvalidate);
run_loop_source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, socket, 0);
CFRunLoopAddSource(CFRunLoopGetCurrent(), run_loop_source, kCFRunLoopDefaultMode);
}
~socket_callback_t ()
{
D(DBF_RMateServer, bug("%p\n", this););
ASSERT(CFRunLoopContainsSource(CFRunLoopGetCurrent(), run_loop_source, kCFRunLoopDefaultMode));
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), run_loop_source, kCFRunLoopDefaultMode);
CFSocketInvalidate(socket);
CFRelease(run_loop_source);
CFRelease(socket);
}
static void callback (CFSocketRef s, CFSocketCallBackType callbackType, CFDataRef address, void const* data, void* info)
{
(*(helper_base_t*)info)();
}
private:
struct helper_base_t
{
WATCH_LEAKS(helper_base_t);
virtual ~helper_base_t () { }
virtual void operator() () = 0;
};
template <typename F>
struct helper_t : helper_base_t
{
helper_t (F f, socket_t const& socket, socket_callback_t* parent) : f(f), socket(socket), parent(parent) { }
void operator() () { if(!f(socket)) delete parent; }
private:
F f;
socket_t socket;
socket_callback_t* parent;
};
typedef std::tr1::shared_ptr<helper_base_t> helper_ptr;
helper_ptr helper;
CFSocketRef socket;
CFRunLoopSourceRef run_loop_source;
};
typedef std::tr1::shared_ptr<socket_callback_t> socket_callback_ptr;
// ======================
// = Return system info =
// ======================
static std::string sys_info (int field)
{
char buf[1024];
size_t bufSize = sizeof(buf);
int request[] = { CTL_KERN, field };
if(sysctl(request, sizeofA(request), buf, &bufSize, NULL, 0) != -1)
return std::string(buf, buf + bufSize - 1);
return "?";
}
static bool rmate_connection_handler_t (socket_t const& socket);
namespace
{
struct mate_server_t
{
mate_server_t ()
{
D(DBF_RMateServer, bug("%s\n", SOCKET_PATH););
unlink(SOCKET_PATH);
socket_t fd(socket(AF_UNIX, SOCK_STREAM, 0));
fcntl(fd, F_SETFD, 1);
struct sockaddr_un addr = { 0, AF_UNIX, SOCKET_PATH };
addr.sun_len = SUN_LEN(&addr);
bind(fd, (sockaddr*)&addr, sizeof(addr));
listen(fd, 5);
_callback.reset(new socket_callback_t(&rmate_connection_handler_t, fd));
}
~mate_server_t ()
{
D(DBF_RMateServer, bug("%s\n", SOCKET_PATH););
unlink(SOCKET_PATH);
}
private:
socket_callback_ptr _callback;
};
struct rmate_server_t
{
rmate_server_t (uint32_t ip, uint16_t port) : _ip(ip), _port(port)
{
D(DBF_RMateServer, bug("%08ux %ud\n", _ip, _port););
static int const on = 1;
socket_t fd(socket(AF_INET, SOCK_STREAM, 0));
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
fcntl(fd, F_SETFD, 1);
struct sockaddr_in iaddr = { sizeof(sockaddr_in), AF_INET, htons(_port), { htonl(_ip) } };
if(-1 == bind(fd, (sockaddr*)&iaddr, sizeof(iaddr)))
fprintf(stderr, "bind(): %s\n", strerror(errno));
if(-1 == listen(fd, 5))
fprintf(stderr, "listen(): %s\n", strerror(errno));
_callback.reset(new socket_callback_t(&rmate_connection_handler_t, fd));
}
~rmate_server_t ()
{
D(DBF_RMateServer, bug("%08ux %ud\n", _ip, _port););
}
uint32_t ip () const { return _ip; }
uint16_t port () const { return _port; }
private:
uint32_t _ip;
uint16_t _port;
socket_callback_ptr _callback;
};
}
void setup_rmate_server (bool enabled, uint32_t ip, uint16_t port)
{
static mate_server_t mate_server;
static std::tr1::shared_ptr<rmate_server_t> rmate_server;
if(!enabled || !rmate_server || ip != rmate_server->ip() || port != rmate_server->port())
{
rmate_server.reset();
if(enabled)
rmate_server.reset(new rmate_server_t(ip, port));
}
}
// ==================================
struct temp_file_t
{
temp_file_t ()
{
path = path::temp("rmate_buffer");
D(DBF_RMateServer, bug("create temp file: %s\n", path.c_str()););
}
operator char const* () const { return path.c_str(); }
~temp_file_t () { unlink(path.c_str()); }
private:
std::string path;
};
typedef std::tr1::shared_ptr<temp_file_t> temp_file_ptr;
struct record_t
{
WATCH_LEAKS(record_t);
record_t (std::string const& command) : command(command) { }
~record_t () { }
std::string command;
std::map<std::string, std::string> arguments;
temp_file_ptr file;
void accept_data (char const* first, char const* last)
{
if(!file)
{
file.reset(new temp_file_t);
arguments["data"] = std::string(*file);
}
if(FILE* fp = fopen(*file, "a"))
{
fwrite(first, 1, last - first, fp);
fclose(fp);
}
}
};
namespace // wrap in anonymous namespace to avoid clashing with other callbacks named the same
{
struct base_t : document::document_t::callback_t
{
WATCH_LEAKS(base_t);
virtual void save_document (document::document_ptr document) { }
virtual void close_document (document::document_ptr document) { }
void handle_document_event (document::document_ptr document, event_t event)
{
if(event == did_change_open_status && !document->is_open())
{
close_document(document);
D(DBF_RMateServer, bug("%p\n", this););
document->remove_callback(this);
delete this;
}
else if(event == did_save)
{
save_document(document);
}
}
};
struct retain_temp_file_callback_t : base_t
{
retain_temp_file_callback_t (temp_file_ptr file) : file(file) { }
temp_file_ptr file;
};
struct save_close_callback_t : base_t
{
WATCH_LEAKS(save_close_callback_t);
save_close_callback_t (std::string const& path, socket_t const& socket, bool data_on_save, bool data_on_close, std::string const& token) : path(path), socket(socket), data_on_save(data_on_save), data_on_close(data_on_close), token(token)
{
D(DBF_RMateServer, bug("%p\n", this););
}
void save_document (document::document_ptr document)
{
D(DBF_RMateServer, bug("%s\n", document->path().c_str()););
bool res = true;
res = res && write(socket, "save\r\n", 6) == 6;
res = res && write_token();
if(data_on_save)
res = res && write_data();
res = res && write(socket, "\r\n", 2) == 2;
if(!res)
fprintf(stderr, "*** rmate: callback failed to save %s\n", document->display_name().c_str());
}
void close_document (document::document_ptr document)
{
D(DBF_RMateServer, bug("%s\n", document->path().c_str()););
bool res = true;
res = res && write(socket, "close\r\n", 7) == 7;
res = res && write_token();
if(data_on_close)
res = res && write_data();
res = res && write(socket, "\r\n", 2) == 2;
if(!res)
fprintf(stderr, "*** rmate: callback failed while closing %s\n", document->display_name().c_str());
}
private:
bool write_token () const
{
if(token != NULL_STR)
{
std::string str = "token: " + token + "\r\n";
return write(socket, str.data(), str.size()) == str.size();
}
return true;
}
bool write_data () const
{
bool res = false;
if(FILE* fp = fopen(path.c_str(), "r"))
{
res = true;
char buf[1024];
while(size_t len = fread(buf, 1, sizeof(buf), fp))
{
std::string str = text::format("data: %zu\r\n", len);
res = res && write(socket, str.data(), str.size()) == str.size();
res = res && write(socket, buf, len) == len;
}
fclose(fp);
}
return res;
}
std::string path;
socket_t socket;
bool data_on_save;
bool data_on_close;
std::string token;
};
struct reactivate_callback_t : base_t
{
WATCH_LEAKS(reactivate_callback_t);
struct helper_t { helper_t () : open_documents(0) { } size_t open_documents; WATCH_LEAKS(reactivate_callback_t); };
typedef std::tr1::shared_ptr<helper_t> helper_ptr;
reactivate_callback_t () : helper(helper_ptr(new helper_t))
{
D(DBF_RMateServer, bug("%p\n", this););
GetFrontProcess(&psn);
}
void watch_document (document::document_ptr document)
{
++helper->open_documents;
document->add_callback(new reactivate_callback_t(*this));
}
void close_document (document::document_ptr document)
{
D(DBF_RMateServer, bug("%zu → %zu\n", helper->open_documents, helper->open_documents - 1););
if(--helper->open_documents == 0)
SetFrontProcess(&psn);
}
private:
helper_ptr helper;
struct ProcessSerialNumber psn;
};
}
// ==================
// = Handle Request =
// ==================
struct socket_observer_t
{
WATCH_LEAKS(socket_observer_t);
socket_observer_t () : state(command), bytesLeft(0) { }
std::vector<record_t> records;
enum { command, arguments, data, done } state;
std::string line;
ssize_t bytesLeft;
bool operator() (socket_t const& socket)
{
char buf[1024];
ssize_t len = read(socket, buf, sizeof(buf));
D(DBF_RMateServer, bug("%p, %d — %zd bytes\n", this, (int)socket, len););
if(len == 0)
return false;
if(len != -1)
{
receive_data(buf, len);
parse();
if(state == done)
{
D(DBF_RMateServer, bug("done\n"););
if(records.empty() || records.begin()->command == "open") // we treat no command as open to bring our application to front
open_documents(socket);
else
fprintf(stderr, "*** error unsupported command: %s\n", records.begin()->command.c_str());
return false;
}
}
return true;
}
void receive_data (char const* buf, ssize_t len)
{
if(state == data)
{
ssize_t dataLen = std::min(len, bytesLeft);
D(DBF_RMateServer, bug("Got data, %zd bytes\n", dataLen););
records.back().accept_data(buf, buf + dataLen);
bytesLeft -= dataLen;
state = bytesLeft == 0 ? arguments : data;
line.insert(line.end(), buf + dataLen, buf + len);
}
else
{
line.insert(line.end(), buf, buf + len);
}
}
void parse ()
{
while(line.find('\n') != std::string::npos)
{
std::string::size_type eol = line.find('\n');
std::string str = line.substr(0, eol);
if(!str.empty() && str[str.size()-1] == '\r')
str.resize(str.size()-1);
line.erase(line.begin(), line.begin() + eol + 1);
if(str.empty())
{
D(DBF_RMateServer, bug("Got end of record\n"););
state = command;
}
else if(state == command)
{
if(str == ".")
{
state = done;
}
else
{
records.push_back(record_t(str));
state = arguments;
}
D(DBF_RMateServer, bug("Got command %s\n", str.c_str()););
}
else if(state == arguments)
{
std::string::size_type n = str.find(':');
if(n != std::string::npos)
{
std::string const key = str.substr(0, n);
std::string const value = str.substr(n+2);
if(key == "data")
{
bytesLeft = strtol(value.c_str(), NULL, 10);
size_t dataLen = std::min((ssize_t)line.size(), bytesLeft);
D(DBF_RMateServer, bug("Got data of size %zd (%zu in this packet)\n", bytesLeft, dataLen););
records.back().accept_data(line.data(), line.data() + dataLen);
line.erase(line.begin(), line.begin() + dataLen);
bytesLeft -= dataLen;
state = bytesLeft == 0 ? arguments : data;
}
else
{
D(DBF_RMateServer, bug("Got argument: %s = %s\n", key.c_str(), value.c_str()););
if(!value.empty())
records.back().arguments.insert(std::make_pair(key, value));
}
}
}
}
}
void open_documents (socket_t const& socket)
{
reactivate_callback_t reactivate_callback;
std::vector<document::document_ptr> documents;
iterate(record, records)
{
std::map<std::string, std::string>& args = record->arguments;
bool wait = args["wait"] == "yes";
bool writeBackOnSave = args["data-on-save"] == "yes";
bool writeBackOnClose = args["data-on-close"] == "yes";
std::string token = args.find("token") != args.end() ? args["token"] : NULL_STR;
bool reActivate = args["re-activate"] == "yes";
std::string fileType = args.find("file-type") == args.end() ? NULL_STR : args["file-type"];
document::document_ptr doc;
if(args.find("path") != args.end())
{
if(path::is_directory(args["path"]))
{
document::show_browser(args["path"]);
continue;
}
doc = document::create(args["path"]);
}
else if(args.find("data") != args.end())
{
if(writeBackOnSave || writeBackOnClose)
{
doc = document::create(args["data"]);
doc->set_recent_tracking(false);
}
else
{
doc = document::from_content(path::content(args["data"]), fileType);
}
}
else
{
doc = document::create();
}
if(fileType != NULL_STR)
doc->set_file_type(fileType);
if(args.find("real-path") != args.end())
{
D(DBF_RMateServer, bug("set documents virtual path: %s\n", args["real-path"].c_str()););
doc->set_virtual_path(args["real-path"]);
}
if(!args["display-name"].empty())
doc->set_custom_name(args["display-name"]);
if(!args["selection"].empty())
doc->set_selection(args["selection"]);
if(wait || writeBackOnSave || writeBackOnClose)
doc->add_callback(new save_close_callback_t(doc->path(), socket, writeBackOnSave, writeBackOnClose, token));
if(args.find("data") != args.end() && (writeBackOnSave || writeBackOnClose))
doc->add_callback(new retain_temp_file_callback_t(record->file));
if(reActivate)
reactivate_callback.watch_document(doc);
// std::string folder; // when there is no path we still may provide a default folder
// enum fallback_t { must_share_path, should_share_path, frontmost, create_new } project_fallback;
// bool add_to_recents;
// bool bring_to_front;
if(args.find("authorization") != args.end())
doc->set_authorization(args["authorization"]);
if(oak::uuid_t::is_valid(args["project-uuid"]))
document::show(doc, args["project-uuid"]);
else documents.push_back(doc);
}
if(documents.empty())
SetFrontProcess(&(ProcessSerialNumber){ 0, kCurrentProcess });
else document::show(documents);
}
};
static bool rmate_connection_handler_t (socket_t const& socket)
{
socklen_t dummyLen = std::max(sizeof(sockaddr_un), sizeof(sockaddr_in));
char dummy[dummyLen];
int newFd = accept(socket, (sockaddr*)&dummy[0], &dummyLen);
std::string welcome = "220 " + sys_info(KERN_HOSTNAME) + " RMATE TextMate (" + sys_info(KERN_OSTYPE) + " " + sys_info(KERN_OSRELEASE) + ")\n";
ssize_t len = write(newFd, welcome.data(), welcome.size());
if(len == -1)
fprintf(stderr, "error writing: %s\n", strerror(errno));
new socket_callback_t(socket_observer_t(), newFd);
return true;
}

View File

@@ -0,0 +1,6 @@
#ifndef RMATESERVER_H_UPCJJJ8U
#define RMATESERVER_H_UPCJJJ8U
void setup_rmate_server (bool enabled, uint32_t ip, uint16_t port);
#endif /* end of include guard: RMATESERVER_H_UPCJJJ8U */

View File

@@ -0,0 +1,12 @@
@interface TMPlugInController : NSObject
{
NSMutableArray* loadedPlugIns;
NSMutableSet* plugInBundleIdentifiers;
BOOL didLoadAllPlugIns;
}
+ (TMPlugInController*)sharedInstance;
- (void)loadAllPlugIns:(id)sender;
- (void)loadPlugIn:(NSString*)aPath;
// - (void)installPlugIn:(NSString*)aPath;
- (float)version;
@end

View File

@@ -0,0 +1,246 @@
#import "TMPlugInController.h"
#import <oak/CocoaSTL.h>
#import <oak/debug.h>
OAK_DEBUG_VAR(PlugInController);
static TMPlugInController* SharedInstance;
@protocol TMWindow
- (BOOL)addStatusBarCell:(NSCell*)aCell;
- (BOOL)removeStatusBarCell:(NSCell*)aCell;
@end
@interface NSObject (TMFileOperationProtocol)
- (void)willOpenURL:(NSURL*)anURL inWindow:(id <TMWindow>)aWindow;
- (void)didOpenURL:(NSURL*)anURL inWindow:(id <TMWindow>)aWindow;
- (void)willSaveToURL:(NSURL*)anURL inWindow:(id <TMWindow>)aWindow;
- (void)didSaveToURL:(NSURL*)anURL inWindow:(id <TMWindow>)aWindow;
- (void)willCloseURL:(NSURL*)anURL inWindow:(id <TMWindow>)aWindow;
- (void)didCloseURL:(NSURL*)anURL inWindow:(id <TMWindow>)aWindow;
@end
@protocol TMPlugInController
- (float)version;
- (void)registerFileOperationObserver:(id)anObserver;
- (void)unregisterFileOperationObserver:(id)anObserver;
@end
@interface NSObject (TMPlugInClass)
- (id)initWithPlugInController:(TMPlugInController*)aController;
@end
@interface TMPlugIn : NSObject
{
NSBundle* plugInBundle;
id instance;
}
+ (TMPlugIn*)plugInWithPath:(NSString*)aPath;
@end
@implementation TMPlugIn
- (TMPlugIn*)initWithPath:(NSString*)aPath
{
D(DBF_PlugInController, bug("%s\n", [aPath UTF8String]););
if(plugInBundle = [NSBundle bundleWithPath:aPath])
{
if(self = [super init])
[plugInBundle retain];
}
else
{
NSLog(@"%s couldn't load plugIn %@", SELNAME(_cmd), aPath);
[self dealloc];
self = nil;
}
return self;
}
+ (TMPlugIn*)plugInWithPath:(NSString*)aPath
{
return [[[self alloc] initWithPath:aPath] autorelease];
}
- (void)dealloc
{
D(DBF_PlugInController, bug("\n"););
[plugInBundle release];
[super dealloc];
}
- (NSString*)name
{
return [plugInBundle objectForInfoDictionaryKey:@"CFBundleName"];
}
- (NSString*)bundleIdentifier
{
return [plugInBundle objectForInfoDictionaryKey:@"CFBundleIdentifier"];
}
- (int)undocumentedRelianceVersion
{
return [[plugInBundle objectForInfoDictionaryKey:@"ReliesOnClassesFromVersion"] intValue];
}
- (id)instance
{
if(!instance)
{
[plugInBundle load];
id obj = [[plugInBundle principalClass] alloc];
if(!obj)
NSLog(@"%s %@ plug-in has no principal class", SELNAME(_cmd), [self name]);
else if([obj respondsToSelector:@selector(initWithPlugInController:)])
instance = [obj initWithPlugInController:[TMPlugInController sharedInstance]];
else
NSLog(@"%s %@ plug-in doesn't have proper initializer", SELNAME(_cmd), [self name]);
}
D(DBF_PlugInController, bug("%s\n", [[instance description] UTF8String]););
return instance;
}
@end
@implementation TMPlugInController
+ (TMPlugInController*)sharedInstance
{
return SharedInstance ?: [[TMPlugInController new] autorelease];
}
- (id)init
{
if(SharedInstance)
{
[self release];
}
else if(self = SharedInstance = [[super init] retain])
{
D(DBF_PlugInController, bug("\n"););
loadedPlugIns = [NSMutableArray new];
plugInBundleIdentifiers = [NSMutableSet new];
}
return SharedInstance;
}
- (float)version
{
return 2.0;
}
- (int)lastMajorInternalChange
{
return 1700;
}
- (void)loadPlugIn:(NSString*)aPath
{
if(TMPlugIn* plugIn = [TMPlugIn plugInWithPath:aPath])
{
if(![[plugIn bundleIdentifier] hasPrefix:@"com.macromates"])
{
NSLog(@"Skip loading plug-in: %@ (%@)", [plugIn bundleIdentifier], aPath);
return;
}
if(![plugIn undocumentedRelianceVersion] || [plugIn undocumentedRelianceVersion] >= [self lastMajorInternalChange])
{
NSString* bundleIdentifier = [plugIn bundleIdentifier];
if(bundleIdentifier && ![plugInBundleIdentifiers containsObject:bundleIdentifier])
{
[loadedPlugIns addObject:plugIn];
[plugInBundleIdentifiers addObject:bundleIdentifier];
[plugIn instance];
}
}
else
{
NSLog(@"%s %@ plug-in was not loaded as it relies on version %d", SELNAME(_cmd), [plugIn name], [plugIn undocumentedRelianceVersion]);
}
}
else
{
NSLog(@"%s failed to load %@", SELNAME(_cmd), aPath);
}
}
#if 0
- (NSString*)installPath
{
NSArray* libraryPaths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
return [NSString pathWithComponents:@[ [libraryPaths firstObject], [[NSProcessInfo processInfo] processName], @"PlugIns" ]];
}
- (void)installPlugIn:(NSString*)aPath
{
NSString* installAs = [[self installPath] stringByAppendingPathComponent:[aPath lastPathComponent]];
if([aPath isEqualToString:installAs])
return;
id plugInName = [[NSBundle bundleWithPath:aPath] objectForInfoDictionaryKey:@"CFBundleName"] ?: [[installAs lastPathComponent] stringByDeletingPathExtension];
if([installAs existsAsPath])
{
id newVersion = [[NSBundle bundleWithPath:aPath] objectForInfoDictionaryKey:@"CFBundleShortVersionString"] ?: [[NSBundle bundleWithPath:aPath] objectForInfoDictionaryKey:@"CFBundleVersion"];
id oldVersion = [[NSBundle bundleWithPath:installAs] objectForInfoDictionaryKey:@"CFBundleShortVersionString"] ?: [[NSBundle bundleWithPath:installAs] objectForInfoDictionaryKey:@"CFBundleVersion"];
int choice = NSRunAlertPanel(@"Plug-in Already Installed", @"Version %@ of “%@” is already installed.\nDo you want to replace it with version %@?\n\nUpgrading a plug-in will require TextMate to be relaunched.", @"Replace", @"Cancel", nil, oldVersion ?: @"???", plugInName, newVersion ?: @"???");
if(choice == NSAlertDefaultReturn) // "Replace"
{
if(![installAs moveFileToTrash])
{
NSRunAlertPanel(@"Install Failed", @"Couldn't remove old plug-in (“%@”)", @"Continue", nil, nil, [installAs stringByAbbreviatingWithTildeInPath]);
installAs = nil;
}
}
else if(choice == NSAlertAlternateReturn) // "Cancel"
{
installAs = nil;
}
}
if(installAs)
{
if([[self installPath] canCreateAsDirectory])
{
NSFileManager* fm = [NSFileManager defaultManager];
BOOL res = [fm isDeletableFileAtPath:aPath] ? [fm movePath:aPath toPath:installAs handler:nil] : [fm copyPath:aPath toPath:installAs handler:nil];
if(res && didLoadAllPlugIns)
{
int choice = NSRunAlertPanel(@"Plug-in Installed", @"To activate “%@” you will need to relaunch TextMate.", @"Relaunch", @"Cancel", nil, plugInName);
if(choice == NSAlertDefaultReturn) // "Relaunch"
[OakSelfUpdate restart];
}
else if(!res)
{
NSRunAlertPanel(@"Install Failed", @"The plug-in has not been installed.", @"Continue", nil, nil);
}
}
else
{
NSRunAlertPanel(@"Install Failed", @"It was not possible to create the plug-in folder (“%@”)", @"Continue", nil, nil, [[self installPath] stringByAbbreviatingWithTildeInPath]);
}
}
}
#endif
- (void)loadAllPlugIns:(id)sender
{
NSMutableArray* array = [NSMutableArray array];
NSArray* appSupportPaths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSAllDomainsMask, YES);
NSString* subPath = [NSString pathWithComponents:@[ @"TextMate", @"PlugIns" ]];
for(NSString* path in appSupportPaths)
[array addObject:[path stringByAppendingPathComponent:subPath]];
[array addObject:[[NSBundle mainBundle] builtInPlugInsPath]];
for(NSString* path in array)
{
D(DBF_PlugInController, bug("scan %s\n", [path UTF8String]););
for(NSString* plugInName in [[NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:nil])
{
if([[[plugInName pathExtension] lowercaseString] isEqualToString:@"tmplugin"])
[self loadPlugIn:[path stringByAppendingPathComponent:plugInName]];
}
}
didLoadAllPlugIns = YES;
}
@end

View File

@@ -0,0 +1,80 @@
#import <cf/callback.h>
#import <oak/compat.h>
#import <oak/debug.h>
#import <OakSystem/application.h>
#import <document/collection.h>
#import <io/path.h>
static void sig_int_handler (void* arg)
{
fprintf(stderr, "%s received SIGINT: Regular shutdown.\n", getprogname());
[NSApp terminate:nil];
}
static void sig_term_handler (void* arg)
{
fprintf(stderr, "%s received SIGTERM: Quick shutdown.\n", getprogname());
document::save_session(true);
[[NSNotificationCenter defaultCenter] postNotificationName:NSApplicationWillTerminateNotification object:NSApp];
[NSApp stop:nil];
[NSApp postEvent:[NSEvent otherEventWithType:NSApplicationDefined location:NSZeroPoint modifierFlags:0 timestamp:0 windowNumber:0 context:NULL subtype:0 data1:0 data2:0] atStart:NO];
[[NSUserDefaults standardUserDefaults] synchronize];
}
static cf::callback_ptr SigIntSource = cf::create_callback<void*>(&sig_int_handler, NULL);
static cf::callback_ptr SigTermSource = cf::create_callback<void*>(&sig_term_handler, NULL);
void* signal_handler_thread (void* userdata)
{
oak::set_thread_name("main::signal_handler");
sigset_t sigs;
sigemptyset(&sigs);
sigaddset(&sigs, SIGINT);
sigaddset(&sigs, SIGTERM);
int receivedSignal;
while(sigwait(&sigs, &receivedSignal) != -1)
{
switch(receivedSignal)
{
case SIGINT: SigIntSource->signal(); break;
case SIGTERM: SigTermSource->signal(); break;
}
}
perror("sigwait()");
return NULL;
}
int main (int argc, char const* argv[])
{
curl_global_init(CURL_GLOBAL_ALL);
oak::application_t::set_support(path::join(path::home(), "Library/Application Support/TextMate"));
oak::application_t app(argc, argv);
signal(SIGINT, SIG_DFL);
signal(SIGTERM, SIG_DFL);
signal(SIGPIPE, SIG_IGN);
sigset_t sigs;
sigemptyset(&sigs);
sigaddset(&sigs, SIGINT);
sigaddset(&sigs, SIGTERM);
if(pthread_sigmask(SIG_BLOCK, &sigs, NULL) == -1)
perror("pthread_sigmask()");
pthread_t thread;
if(pthread_create(&thread, NULL, &signal_handler_thread, NULL) == 0)
pthread_detach(thread);
else perror("pthread_create()");
NSAutoreleasePool* pool = [NSAutoreleasePool new];
for(NSString* variable in [[[NSProcessInfo processInfo] environment] allKeys])
{
if([variable hasPrefix:@"TM_"])
unsetenv([variable UTF8String]);
}
[pool release];
return NSApplicationMain(argc, argv);
}

View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>beforeRunningCommand</key>
<string>nop</string>
<key>command</key>
<string>#!/usr/bin/env ruby -wKU
require "#{ENV['TM_SUPPORT_PATH']}/lib/escape"
require "shellwords"
abort "Select one or more items in file browser" unless ENV.has_key? 'TM_SELECTED_FILES'
def same_nth_element?(n, array)
return false if array.any? { |arr| arr.size &lt;= n }
array.map { |arr| arr[n] }.uniq.size == 1
end
def common_ancestor(paths)
paths = paths.map { |path| path.split('/') }
i = 0; i += 1 while same_nth_element?(i, paths)
paths.first[0...i].join('/')
end
def find_unique_path(head, tail)
i = 1
path = head + tail
while File.exists? path
path = format("%s %d%s", head, i += 1, tail)
end
path
end
paths = Shellwords.shellwords(ENV['TM_SELECTED_FILES'])
dirs = paths.map { |path| File.dirname(path) }
dest_dir = common_ancestor(dirs)
dest_base = paths.size == 1 ? File.basename(paths.first) : 'Archive'
dest_path = find_unique_path("#{dest_dir}/#{dest_base}", ".tbz")
relative_paths = paths.map { |path| path.sub(/#{Regexp.escape dest_dir}\//, '') }
%x{tar -cjf #{e_sh dest_path} -C #{e_sh dest_dir + '/'} #{relative_paths.map { |path| e_sh path }.join(' ')}}
</string>
<key>input</key>
<string>none</string>
<key>inputFormat</key>
<string>text</string>
<key>name</key>
<string>Compress Selected Items</string>
<key>outputCaret</key>
<string>afterOutput</string>
<key>outputFormat</key>
<string>text</string>
<key>outputLocation</key>
<string>toolTip</string>
<key>semanticClass</key>
<string>callback.file-browser.action-menu</string>
<key>uuid</key>
<string>5B28B062-9E93-40C9-8F83-A7643634C22D</string>
<key>version</key>
<integer>2</integer>
</dict>
</plist>

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>beforeRunningCommand</key>
<string>nop</string>
<key>command</key>
<string>#!/usr/bin/ruby -KU
require ENV['TM_SUPPORT_PATH'] + '/lib/textmate.rb'
require ENV['TM_SUPPORT_PATH'] + '/lib/escape.rb'
require ENV['TM_SUPPORT_PATH'] + '/lib/ui.rb'
passwd = TextMate::UI.request_string :title =&gt; "Password", :prompt =&gt; "Enter password to decrypt file:"
abort "No password." if passwd.nil?
STDOUT &lt;&lt; %x{ /usr/bin/openssl enc -d -aes128 -pass pass:#{e_sh passwd} }
</string>
<key>contentMatch</key>
<string>\ASalted__</string>
<key>hideFromUser</key>
<string>1</string>
<key>input</key>
<string>document</string>
<key>inputFormat</key>
<string>text</string>
<key>name</key>
<string>Decrypt (Auto-detect)</string>
<key>outputCaret</key>
<string>interpolateByLine</string>
<key>outputFormat</key>
<string>text</string>
<key>outputLocation</key>
<string>replaceDocument</string>
<key>semanticClass</key>
<string>callback.document.binary-import</string>
<key>uuid</key>
<string>BF448EEA-B23C-49EE-A2A6-E672AA736D70</string>
<key>version</key>
<integer>2</integer>
</dict>
</plist>

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>beforeRunningCommand</key>
<string>nop</string>
<key>command</key>
<string>#!/usr/bin/ruby -KU
require ENV['TM_SUPPORT_PATH'] + '/lib/textmate.rb'
require ENV['TM_SUPPORT_PATH'] + '/lib/escape.rb'
require ENV['TM_SUPPORT_PATH'] + '/lib/ui.rb'
passwd = TextMate::UI.request_string :title =&gt; "Password", :prompt =&gt; "Enter password to encrypt file:"
abort "No password." if passwd.nil?
STDOUT &lt;&lt; %x{ /usr/bin/openssl enc -e -aes128 -pass pass:#{e_sh passwd} }
</string>
<key>hideFromUser</key>
<string>1</string>
<key>input</key>
<string>document</string>
<key>inputFormat</key>
<string>text</string>
<key>name</key>
<string>Encrypt on Save</string>
<key>outputCaret</key>
<string>interpolateByLine</string>
<key>outputFormat</key>
<string>text</string>
<key>outputLocation</key>
<string>replaceDocument</string>
<key>scope</key>
<string>attr.rev-path.crypt</string>
<key>semanticClass</key>
<string>callback.document.binary-export</string>
<key>uuid</key>
<string>1B531CC1-3085-4E53-83FC-41F347B6684A</string>
<key>version</key>
<integer>2</integer>
</dict>
</plist>

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>beforeRunningCommand</key>
<string>nop</string>
<key>command</key>
<string>#!/usr/bin/env ruby -wKU
require "#{ENV['TM_SUPPORT_PATH']}/lib/osx/plist"
text = STDIN.read
if text =~ /\A(#!.*\n)#\n((?:#.+$\n)*)#\n/
shebang, head, body = $1, $2, $'
plist = { 'command' =&gt; shebang + body }
head.scan(/#\s+([^:]*): (.*)$/) { plist[$1] = $2 }
STDOUT &lt;&lt; plist.to_plist
else
STDOUT &lt;&lt; text
end
</string>
<key>hideFromUser</key>
<string>1</string>
<key>input</key>
<string>document</string>
<key>inputFormat</key>
<string>text</string>
<key>name</key>
<string>Export TM Commands</string>
<key>outputCaret</key>
<string>interpolateByLine</string>
<key>outputFormat</key>
<string>text</string>
<key>outputLocation</key>
<string>replaceDocument</string>
<key>scope</key>
<string>attr.rev-path.tmCommand</string>
<key>semanticClass</key>
<string>callback.document.export</string>
<key>uuid</key>
<string>E2C42B70-5823-49A7-A259-A1622EBD191C</string>
<key>version</key>
<integer>2</integer>
</dict>
</plist>

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>beforeRunningCommand</key>
<string>nop</string>
<key>command</key>
<string>#!/bin/bash
/usr/bin/bunzip2
</string>
<key>contentMatch</key>
<string>\ABZh[1-9]</string>
<key>hideFromUser</key>
<string>1</string>
<key>input</key>
<string>document</string>
<key>inputFormat</key>
<string>text</string>
<key>name</key>
<string>Import BZip2</string>
<key>outputCaret</key>
<string>interpolateByLine</string>
<key>outputFormat</key>
<string>text</string>
<key>outputLocation</key>
<string>replaceDocument</string>
<key>semanticClass</key>
<string>callback.document.binary-import</string>
<key>uuid</key>
<string>5CAD0D24-F9E4-490E-8780-556D1BFBAA3D</string>
<key>version</key>
<integer>2</integer>
</dict>
</plist>

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>beforeRunningCommand</key>
<string>nop</string>
<key>command</key>
<string>#!/bin/bash
/usr/bin/gunzip
</string>
<key>contentMatch</key>
<string>\A\x{1F}\x{8B}\x{08}</string>
<key>hideFromUser</key>
<string>1</string>
<key>input</key>
<string>document</string>
<key>inputFormat</key>
<string>text</string>
<key>name</key>
<string>Import GZip</string>
<key>outputCaret</key>
<string>interpolateByLine</string>
<key>outputFormat</key>
<string>text</string>
<key>outputLocation</key>
<string>replaceDocument</string>
<key>semanticClass</key>
<string>callback.document.binary-import</string>
<key>uuid</key>
<string>632110BD-89A3-44BB-8437-C6A744A6271A</string>
<key>version</key>
<integer>2</integer>
</dict>
</plist>

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>beforeRunningCommand</key>
<string>nop</string>
<key>command</key>
<string>#!/bin/bash
echo "PNG Image: ${TM_DISPLAYNAME}"
sips -g pixelWidth -g pixelHeight "$TM_FILEPATH"|egrep 'pixel(Width|Height)'
</string>
<key>contentMatch</key>
<string>\A\x{89}PNG\r\n\x{1A}\n</string>
<key>hideFromUser</key>
<string>1</string>
<key>input</key>
<string>document</string>
<key>inputFormat</key>
<string>text</string>
<key>name</key>
<string>Import PNG Image</string>
<key>outputCaret</key>
<string>interpolateByLine</string>
<key>outputFormat</key>
<string>text</string>
<key>outputLocation</key>
<string>replaceDocument</string>
<key>semanticClass</key>
<string>callback.document.binary-import</string>
<key>uuid</key>
<string>E5E8E329-EFA2-4EC2-A2E3-EF21275372CB</string>
<key>version</key>
<integer>2</integer>
</dict>
</plist>

View File

@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>beforeRunningCommand</key>
<string>nop</string>
<key>command</key>
<string>#!/usr/bin/env ruby -wKU
require "#{ENV['TM_SUPPORT_PATH']}/lib/osx/plist"
data = STDIN.read
begin
plist = OSX::PropertyList.load(data)
if plist['command'] =~ /(\A#!.*\n)/
shebang, body = $&amp;, $'
puts shebang
puts "#"
plist.keys.reject { |e| e == 'command' }.sort.each { |key| puts "# %25.25s: #{plist[key]}" % key }
puts "#"
puts body
else
puts "#!/bin/bash"
puts "#"
plist.keys.reject { |e| e == 'command' }.sort.each { |key| puts "# %25.25s: #{plist[key]}" % key }
puts "#"
puts "source \"$TM_SUPPORT_PATH/lib/bash_init.sh\" # might not be necessary"
puts plist['command']
end
rescue Exception =&gt; e
if data.empty?
STDOUT &lt;&lt; &lt;&lt;-END_OF_TEMPLATE
#!/usr/bin/env ruby -wKU
#
# name:
# input: selection
# output: showAsTooltip
# scope:
# eventSpecifier:
# keyEquivalent:
# tabTrigger:
# uuid: #{%x{uuidgen}.chomp}
#
END_OF_TEMPLATE
else
STDOUT &lt;&lt; data
end
end
</string>
<key>contentMatch</key>
<string>.*</string>
<key>hideFromUser</key>
<string>1</string>
<key>input</key>
<string>document</string>
<key>inputFormat</key>
<string>text</string>
<key>name</key>
<string>Import TM Commands</string>
<key>outputCaret</key>
<string>interpolateByLine</string>
<key>outputFormat</key>
<string>text</string>
<key>outputLocation</key>
<string>replaceDocument</string>
<key>scope</key>
<string>attr.rev-path.tmCommand</string>
<key>semanticClass</key>
<string>callback.document.binary-import</string>
<key>uuid</key>
<string>306DEA02-3019-4968-A56B-E9E86D490B91</string>
<key>version</key>
<integer>2</integer>
</dict>
</plist>

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>beforeRunningCommand</key>
<string>nop</string>
<key>command</key>
<string>#!/usr/bin/env ruby -wKU
require "#{ENV['TM_SUPPORT_PATH']}/lib/escape"
dir = ENV['TM_DIRECTORY']
until File.exist?("#{dir}/target")
parent = File.expand_path("..", dir)
abort "No target file found." if dir == parent || parent.empty?
dir = parent
end
%x{ #{e_sh "#{ENV['TM_SUPPORT_PATH']}/bin/mate"} #{e_sh "#{dir}/target"} }
</string>
<key>input</key>
<string>none</string>
<key>keyEquivalent</key>
<string>~@</string>
<key>name</key>
<string>Open Target File</string>
<key>output</key>
<string>showAsTooltip</string>
<key>scope</key>
<string>source.c, source.c++, source.objc, source.objc++</string>
<key>uuid</key>
<string>DD542BA4-D16F-43E1-A4B4-BC66CFB26BC3</string>
</dict>
</plist>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>beforeRunningCommand</key>
<string>nop</string>
<key>command</key>
<string>#!/bin/sh
echo 'This is a Subversion repository, use ⌘Y.'
</string>
<key>hideFromUser</key>
<string>1</string>
<key>input</key>
<string>none</string>
<key>keyEquivalent</key>
<string>^G</string>
<key>name</key>
<string>SCM Warning (Git)</string>
<key>output</key>
<string>showAsTooltip</string>
<key>scope</key>
<string>attr.scm.svn</string>
<key>uuid</key>
<string>E9240402-203E-46FB-A99B-AF5C732B98FD</string>
</dict>
</plist>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>beforeRunningCommand</key>
<string>nop</string>
<key>command</key>
<string>#!/bin/sh
echo 'This is a Git repository, use ⌘Y.'
</string>
<key>hideFromUser</key>
<string>1</string>
<key>input</key>
<string>none</string>
<key>keyEquivalent</key>
<string>^A</string>
<key>name</key>
<string>SCM Warning (Subversion)</string>
<key>output</key>
<string>showAsTooltip</string>
<key>scope</key>
<string>attr.scm.git</string>
<key>uuid</key>
<string>2DD9AAFF-72F4-4BF0-AE56-C4792B0EB5C5</string>
</dict>
</plist>

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>beforeRunningCommand</key>
<string>nop</string>
<key>command</key>
<string>#!/usr/bin/env ruby -wKU
require "#{ENV['TM_SUPPORT_PATH']}/lib/escape"
require "shellwords"
abort "Select one or more items in file browser" unless ENV.has_key? 'TM_SELECTED_FILES'
paths = Shellwords.shellwords(ENV['TM_SELECTED_FILES'])
tags = paths.map { |path| "&lt;img src='file://#{e_url path}'&gt;\n" }
title = paths.size &gt; 1 ? "Selected Images" : File.basename(paths.first)
puts "&lt;html&gt;\n&lt;head&gt;&lt;title&gt;#{title}&lt;/title&gt;&lt;/head&gt;\n&lt;body&gt;&lt;center&gt;\n#{tags.join("&lt;br&gt;\n")}&lt;/center&gt;&lt;/body&gt;\n&lt;/html&gt;"
</string>
<key>hideFromUser</key>
<string>1</string>
<key>input</key>
<string>none</string>
<key>inputFormat</key>
<string>text</string>
<key>name</key>
<string>Show Images</string>
<key>outputCaret</key>
<string>afterOutput</string>
<key>outputFormat</key>
<string>html</string>
<key>outputLocation</key>
<string>newWindow</string>
<key>semanticClass</key>
<string>callback.file-browser.action-menu</string>
<key>uuid</key>
<string>8858B455-AD7C-44E9-BDDE-C5B110E13936</string>
<key>version</key>
<integer>2</integer>
</dict>
</plist>

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>name</key>
<string>Avian</string>
<key>ordering</key>
<array>
<string>AD495D39-E4CE-4790-8263-92851356C2F2</string>
<string>1BD332E5-D97D-4D79-992C-758F003096AA</string>
<string>A694C541-51F1-4E7C-9286-D89B3BBC6B0C</string>
<string>DE84747E-90A6-4731-92A4-A9C6823D35DE</string>
<string>AC5F664E-86BA-4D81-B6CE-1B12F69FA490</string>
<string>73C7344A-CA0F-4BED-8438-0313767E79D7</string>
<string>B9B9EEC2-26C1-4475-9642-3EA551999169</string>
<string>D77CB8B6-C75D-4A59-9181-45D27BC82C45</string>
<string>5C7D926C-E9A4-424F-876A-D1D8DE8CE02F</string>
<string>7AEE4573-8C47-4923-A414-B5EA4BC91A82</string>
<string>1B531CC1-3085-4E53-83FC-41F347B6684A</string>
<string>E2C42B70-5823-49A7-A259-A1622EBD191C</string>
<string>306DEA02-3019-4968-A56B-E9E86D490B91</string>
<string>0D7C6CA2-5A3A-4FBE-9CE6-9C23BA87CCA0</string>
<string>AB0B0546-2007-4E5E-AE48-E3136821C560</string>
<string>DD542BA4-D16F-43E1-A4B4-BC66CFB26BC3</string>
<string>E9240402-203E-46FB-A99B-AF5C732B98FD</string>
<string>2DD9AAFF-72F4-4BF0-AE56-C4792B0EB5C5</string>
<string>D37FEB31-0A60-45DF-A07A-2FC830FBF93C</string>
</array>
<key>uuid</key>
<string>C5F9DCF7-13D1-446C-BCA4-BCAD44E573B8</string>
</dict>
</plist>

View File

@@ -0,0 +1,11 @@
SOURCES = src/*.{cc,mm}
CP_Resources = resources/* icons/*.icns @PrivilegedTool
CP_SharedSupport = support/*
CP_PlugIns = @Dialog @Dialog2
FLAGS += -DREST_API='@"$rest_api"'
LINK += bundles cf command document editor io network ns plist settings text
LINK += BundleEditor BundlesManager CrashReporter DocumentWindow Find HTMLOutputWindow OakAppKit OakFilterList OakFoundation OakSystem OakTextView Preferences SoftwareUpdate
FRAMEWORKS = Cocoa
HTML_HEADER = templates/header.html
HTML_FOOTER = templates/footer.html
MARKDOWN_FOOTER = references.md

View File

@@ -0,0 +1,2 @@
</body>
</html>

View File

@@ -0,0 +1,21 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<%- meta_data.each do |key, value| -%>
<meta name="<%= key %>" content="<%= value %>">
<%- end -%>
<link href="css/stylesheet.css" rel="stylesheet" type="text/css">
<%- css_files.each do |css| -%>
<link rel="stylesheet" type="text/css" href="<%= css %>" charset="utf-8">
<%- end -%>
<%- js_files.each do |js| -%>
<script type="text/javascript" src="<%= js %>" charset="utf-8"></script>
<%- end -%>
<title><%= page_title %></title>
</head>
<body>

View File

@@ -0,0 +1 @@
BL_RUN_ARGUMENTS = "--version"

306
Applications/bl/src/bl.cc Normal file
View File

@@ -0,0 +1,306 @@
#include <updater/updater.h>
#include <regexp/format_string.h>
#include <regexp/glob.h>
#include <oak/oak.h>
#include <text/case.h>
#include <text/ctype.h>
#include <text/decode.h>
#include <text/format.h>
#include <io/io.h>
#include <OakSystem/application.h>
static double const AppVersion = 1.2;
static size_t const AppRevision = APP_REVISION;
// example: bl install Apache AppleScript Blogging Bundle\ Development C CSS Diff Git HTML Hyperlink\ Helper JavaScript Mail Make Markdown Math Objective-C PHP Perl Property\ List Ragel Remind Ruby SQL Shell\ Script Source Subversion TODO Text TextMate XML Xcode
static int get_width ()
{
if(!isatty(STDIN_FILENO))
return INT_MAX;
struct winsize ws;
if(ioctl(0, TIOCGWINSZ, &ws) == -1)
{
fprintf(stderr, "TIOCGWINSZ: %s\n", strerror(errno));
exit(1);
}
return ws.ws_col;
}
static std::string textify (std::string str)
{
str = format_string::replace(str, "\\A\\s+|<[^>]*>|\\s+\\z", "");
str = format_string::replace(str, "\\s+", " ");
str = decode::entities(str);
return str;
}
extern char* optarg;
extern int optind;
static void usage (FILE* io = stdout)
{
fprintf(io, "%1$s %2$.1f (" COMPILE_DATE " revision %3$zu)\n", getprogname(), AppVersion, AppRevision);
fprintf(io, "Usage: %s list [Cius] [«bundle» ..]\n", getprogname());
fprintf(io, " %s install [Cs] «bundle» ..\n", getprogname());
fprintf(io, " %s uninstall [Cs] «bundle» ..\n", getprogname());
fprintf(io, " %s show [C] «bundle» ..\n", getprogname());
fprintf(io, " %s dependencies [Cs] [«bundle» ..]\n", getprogname());
fprintf(io, " %s dependents [Ci] [«bundle» ..]\n", getprogname());
fprintf(io, " %s update [C]\n", getprogname());
fprintf(io, " %s help\n", getprogname());
fprintf(io, "\n");
fprintf(io, "Options:\n");
fprintf(io, " -h/--help Show this help.\n");
fprintf(io, " -v/--version Show version number.\n");
fprintf(io, " -C/--directory «path» Use «path» for local bundles.\n");
fprintf(io, " -i/--installed Only include installed bundles.\n");
fprintf(io, " -u/--updated Only include bundles which have pending updates.\n");
fprintf(io, " -s/--source «name» Restrict bundles to the given source. Multiple sources are allowed.\n");
fprintf(io, "\n");
fprintf(io, "Globs: Both source and bundle names can contain wild cards:\n");
fprintf(io, " ? Match any single character\n");
fprintf(io, " * Match any characters\n");
fprintf(io, " {«a»,«b»,«c»} Match «a», «b», or «c».\n");
}
static bool matches (std::string const& str, std::vector<std::string> const& globs)
{
if(globs.empty())
return true;
iterate(glob, globs)
{
if(path::glob_t(*glob).does_match(str))
return true;
}
return false;
}
static std::vector<bundles_db::bundle_ptr> filtered_bundles (std::vector<bundles_db::bundle_ptr> const& index, std::vector<std::string> const& sourceNames, std::vector<std::string> const& bundleNames)
{
std::vector<bundles_db::bundle_ptr> res;
std::set<oak::uuid_t> seen;
iterate(bundle, index)
{
if(matches((*bundle)->source() ? (*bundle)->source()->identifier() : NULL_STR, sourceNames) && matches(text::lowercase((*bundle)->name()), bundleNames))
{
if(seen.find((*bundle)->uuid()) == seen.end())
{
seen.insert((*bundle)->uuid());
res.push_back(*bundle);
}
}
}
return res;
}
static std::string short_bundle_info (bundles_db::bundle_ptr bundle, int width)
{
int descWidth = std::max(10, width - 23);
char status = bundle->installed() ? (bundle->has_update() ? 'U' : 'I') : ' ';
std::string desc = bundle->description();
desc = desc == NULL_STR ? "(no description)" : textify(desc);
if(desc.size() > descWidth)
desc.resize(descWidth);
return text::format("%c %-20.20s %s", status, bundle->name().c_str(), desc.c_str());
}
static void version ()
{
fprintf(stdout, "%1$s %2$.1f (" COMPILE_DATE " revision %3$zu)\n", getprogname(), AppVersion, AppRevision);
}
int main (int argc, char const* argv[])
{
oak::application_t::set_support(path::join(path::home(), "Library/Application Support/TextMate"));
oak::application_t app(argc, argv);
static struct option const longopts[] = {
{ "directory", required_argument, 0, 'C' },
{ "source", required_argument, 0, 's' },
{ "updated", no_argument, 0, 'u' },
{ "installed", no_argument, 0, 'i' },
{ "help", no_argument, 0, 'h' },
{ "version", no_argument, 0, 'v' },
{ 0, 0, 0, 0 }
};
std::vector<std::string> sourceNames;
std::string installDir = NULL_STR;
bool onlyUpdated = false;
bool onlyInstalled = false;
unsigned int ch;
while((ch = getopt_long(argc, (char* const*)argv, "s:uiC:hv", longopts, NULL)) != -1)
{
switch(ch)
{
case 'u': onlyUpdated = true; break;
case 'i': onlyInstalled = true; break;
case 'C': installDir = optarg; break;
case 's': sourceNames.push_back(optarg); break;
case 'v': version(); return 0;
case 'h': usage(); return 0;
case '?': /* unknown option */ return 1;
case ':': /* missing option */ return 1;
default: usage(stderr); return 1;
}
}
if(optind == argc)
return usage(stderr), 1;
std::string command = argv[optind];
if(command == "help")
return usage(stdout), 0;
std::vector<std::string> bundleNames;
for(int i = optind + 1; i < argc; ++i)
bundleNames.push_back(text::lowercase(argv[i]));
static std::string const CommandsNeedingBundleList[] = { "install", "uninstall", "show", "dependents" };
if(bundleNames.empty() && oak::contains(beginof(CommandsNeedingBundleList), endof(CommandsNeedingBundleList), command))
{
fprintf(stderr, "no bundles specified\n");
return 1;
}
static std::string const CommandsNeedingUpdatedSources[] = { "update", "install", "list", "show" };
if(oak::contains(beginof(CommandsNeedingUpdatedSources), endof(CommandsNeedingUpdatedSources), command))
bundles_db::update_sources(installDir);
std::vector<bundles_db::bundle_ptr> index = bundles_db::index(installDir);
if(command == "list")
{
citerate(bundle, filtered_bundles(index, sourceNames, bundleNames))
{
if(!((onlyInstalled && !(*bundle)->installed()) || (onlyUpdated && !onlyInstalled && !(*bundle)->has_update())))
fprintf(stdout, "%s\n", short_bundle_info(*bundle, get_width()).c_str());
}
}
else if(command == "install")
{
citerate(bundle, filtered_bundles(index, sourceNames, bundleNames))
{
if(!(*bundle)->installed())
{
fprintf(stderr, "Installing %s...", (*bundle)->name().c_str());
if(install(*bundle, installDir))
fprintf(stderr, "ok!\n");
else fprintf(stderr, " *** failed!\n");
}
else
{
fprintf(stderr, "skip %s (%s) -- already installed\n", (*bundle)->name().c_str(), (*bundle)->origin().c_str());
}
}
save_index(index, installDir);
}
else if(command == "uninstall")
{
citerate(bundle, filtered_bundles(index, sourceNames, bundleNames))
{
if((*bundle)->installed())
{
fprintf(stderr, "Uninstalling %s...", (*bundle)->name().c_str());
if(uninstall(*bundle, installDir))
fprintf(stderr, "ok!\n");
else fprintf(stderr, " *** failed!\n");
}
else
{
fprintf(stderr, "skip %s (%s) -- not installed\n", (*bundle)->name().c_str(), (*bundle)->origin().c_str());
}
}
save_index(index, installDir);
}
else if(command == "show")
{
bool first = true;
citerate(bundle, filtered_bundles(index, sourceNames, bundleNames))
{
if(first)
first = false;
else fprintf(stdout, "\n");
fprintf(stdout, "name: %s\n", (*bundle)->name().c_str());
fprintf(stdout, "source: %s\n", (*bundle)->source() ? (*bundle)->source()->identifier().c_str() : "«no remote source»");
fprintf(stdout, "uuid: %s\n", to_s((*bundle)->uuid()).c_str());
bool hasName = (*bundle)->contact_name() != NULL_STR;
bool hasEmail = (*bundle)->contact_email() != NULL_STR;
char const* fmt[] = { "", "contact: %1$s\n", "contact: <%2$s>\n\0%1$s", "contact: %1$s <%2$s>\n" };
fprintf(stdout, fmt[(hasName ? 1 : 0) + (hasEmail ? 2 : 0)], (*bundle)->contact_name().c_str(), (*bundle)->contact_email().c_str());
if((*bundle)->url_updated())
fprintf(stdout, "date: %s\n", to_s((*bundle)->url_updated()).c_str());
if((*bundle)->url() != NULL_STR)
fprintf(stdout, "url: %s (%d bytes)\n", (*bundle)->url().c_str(), (*bundle)->size());
if((*bundle)->path() != NULL_STR)
fprintf(stdout, "path: %s\n", path::with_tilde((*bundle)->path()).c_str());
if((*bundle)->origin() != NULL_STR)
fprintf(stdout, "origin: %s\n", (*bundle)->origin().c_str());
citerate(grammar, (*bundle)->grammars())
{
std::vector<std::string> fileTypes;
citerate(ext, (*grammar)->file_types())
fileTypes.push_back(*ext);
if((*grammar)->mode_line() != NULL_STR)
fileTypes.push_back(text::format("/%s/", (*grammar)->mode_line().c_str()));
if(fileTypes.empty())
fileTypes.push_back((*grammar)->scope());
fprintf(stdout, "grammar: %s (%s)\n", (*grammar)->name().c_str(), text::join(fileTypes, ", ").c_str());
}
std::vector<std::string> dependencies;
citerate(dependency, (*bundle)->dependencies(index))
dependencies.push_back((*dependency)->name());
if(!dependencies.empty())
fprintf(stderr, "dependencies: %s\n", text::join(dependencies, ", ").c_str());
if((*bundle)->description() != NULL_STR)
fprintf(stdout, "description: %s\n", textify((*bundle)->description()).c_str());
}
}
else if(command == "update")
{
citerate(bundle, filtered_bundles(index, sourceNames, bundleNames))
{
if((*bundle)->has_update())
{
fprintf(stderr, "Updating %s...", (*bundle)->name().c_str());
if(update(*bundle, installDir))
fprintf(stderr, "ok!\n");
else fprintf(stderr, " *** failed!\n");
}
}
save_index(index, installDir);
}
else if(command == "dependencies")
{
std::vector<bundles_db::bundle_ptr> bundles;
citerate(bundle, filtered_bundles(index, sourceNames, bundleNames))
{
if(!bundleNames.empty() || (*bundle)->installed())
bundles.push_back(*bundle);
}
citerate(bundle, dependencies(index, bundles, false))
fprintf(stdout, "%s\n", short_bundle_info(*bundle, get_width()).c_str());
}
else if(command == "dependents")
{
citerate(bundle, dependents(index, filtered_bundles(index, sourceNames, bundleNames), onlyInstalled))
fprintf(stdout, "%s\n", short_bundle_info(*bundle, get_width()).c_str());
}
else
{
fprintf(stderr, "unknown command: %s\n", command.c_str());
return 1;
}
return 0;
}

2
Applications/bl/target Normal file
View File

@@ -0,0 +1,2 @@
SOURCES = src/*.cc
LINK += text io updater regexp

View File

@@ -0,0 +1,24 @@
#import <OSAKit/OSAKit.h>
static double const AppVersion = 1.0;
static size_t const AppRevision = APP_REVISION;
int main (int argc, char const* argv[])
{
if(argc == 2 && (strcmp(argv[1], "--version") == 0 || strcmp(argv[1], "-v") == 0))
{
fprintf(stdout, "%1$s %2$.1f (" COMPILE_DATE " revision %3$zu)\n", getprogname(), AppVersion, AppRevision);
return 0;
}
NSAutoreleasePool* pool = [NSAutoreleasePool new];
NSDictionary* errorInfo = nil;
OSAScript* script = [[OSAScript alloc] initWithCompiledData:[[NSFileHandle fileHandleWithStandardInput] readDataToEndOfFile] error:&errorInfo];
std::string str = [[script source] UTF8String];
[pool drain];
std::replace(str.begin(), str.end(), '\r', '\n');
write(STDOUT_FILENO, str.data(), str.size());
return 0;
}

View File

@@ -0,0 +1,2 @@
SOURCES = src/*.mm
FRAMEWORKS = Foundation OSAKit

175
Applications/gtm/src/gtm.cc Normal file
View File

@@ -0,0 +1,175 @@
#include <parse/grammar.h>
#include <parse/parse.h>
#include <test/bundle_index.h>
#include <oak/duration.h>
#include <oak/oak.h>
#include <iostream>
static double const AppVersion = 1.0;
static size_t const AppRevision = APP_REVISION;
void version ()
{
fprintf(stdout, "%1$s %2$.1f (" COMPILE_DATE " revision %3$zu)\n", getprogname(), AppVersion, AppRevision);
}
void usage (FILE* io)
{
fprintf(io,
"%1$s %2$.1f (" COMPILE_DATE " revision %3$zu)\n"
"Usage: %1$s [-g<selector>td<string>lhv] grammar ...\n"
"Options:\n"
" -g, --grammar <selector> Which grammar to use.\n"
" -t, --trim Show only first letter of each scope fragment.\n"
" -d, --delimiters <string> Surround scopes using argument. See example.\n"
" -l, --verbose Be verbose (output timings).\n"
" -h, --help Show this information.\n"
" -v, --version Print version information.\n"
"\n", getprogname(), AppVersion, AppRevision
);
}
template <typename _InputIter, typename _OutputIter>
_OutputIter entity_escape (_InputIter first, _InputIter const& last, _OutputIter out)
{
ASSERT(!(last < first));
static typename std::iterator_traits<_InputIter>::value_type const special[] = { '<', '>', '&' };
static char const* const escaped[] = { "&lt;", "&gt;", "&amp;" };
while(first != last)
{
_InputIter it = std::find_first_of(first, last, special, special + sizeofA(special));
out = std::copy(first, it, out);
first = it;
if(first != last)
{
size_t idx = std::find(special, special + sizeofA(special), *first) - special;
out = std::copy(beginof(escaped[idx]), endof(escaped[idx]), out);
++first;
}
}
return out;
}
void parse_stdin (std::string const& grammarSelector = "text.plain", bool verbose = false)
{
citerate(item, bundles::query(bundles::kFieldGrammarScope, grammarSelector, scope::wildcard, bundles::kItemTypeGrammar))
{
if(parse::grammar_ptr grammar = parse::parse_grammar(*item))
{
parse::stack_ptr stack = grammar->seed();
scope::scope_t lastScope(grammarSelector);
oak::duration_t timer;
size_t bytes = 0;
static char buf[16384];
while(fgets(buf, sizeof(buf), stdin))
{
std::map<size_t, scope::scope_t> scopes;
stack = parse::parse(buf, buf + strlen(buf), stack, scopes, bytes == 0);
bytes += strlen(buf);
size_t lastPos = 0;
iterate(it, scopes)
{
entity_escape(buf + lastPos, buf + it->first, std::ostream_iterator<char>(std::cout));
// std::copy(buf + lastPos, buf + it->first, std::ostream_iterator<char>(std::cout));
std::cout << xml_difference(lastScope, it->second, "<", ">");
lastScope = it->second;
lastPos = it->first;
}
entity_escape(buf + lastPos, buf + strlen(buf), std::ostream_iterator<char>(std::cout));
// std::copy(buf + lastPos, buf + strlen(buf), std::ostream_iterator<char>(std::cout));
}
std::cout << xml_difference(lastScope, grammarSelector, "<", ">") << std::endl;
if(verbose)
fprintf(stderr, "parsed %zu bytes in %.1fs (%.0f bytes/s)\n", bytes, timer.duration(), bytes / timer.duration());
return;
}
}
fprintf(stderr, "%s: unable to find grammar for selector %s\n", getprogname(), grammarSelector.c_str());
exit(1);
}
int main (int argc, char* const* argv)
{
extern char* optarg;
extern int optind;
static struct option const longopts[] = {
{ "grammar", required_argument, 0, 'g' },
{ "trim", no_argument, 0, 't' },
{ "delimiters", required_argument, 0, 'd' },
{ "verbose", no_argument, 0, 'l' },
{ "help", no_argument, 0, 'h' },
{ "version", no_argument, 0, 'v' },
{ 0, 0, 0, 0 }
};
bool verbose = false, trim = false;
std::string grammar = NULL_STR, delimiters = NULL_STR;
int ch;
while((ch = getopt_long(argc, argv, "g:td:lhv", longopts, NULL)) != -1)
{
switch(ch)
{
case 'g': grammar = optarg; break;
case 't': trim = true; break; // TODO
case 'd': delimiters = optarg; break; // TODO
case 'l': verbose = true; break;
case 'h': usage(stdout); return 0;
case 'v': version(); return 0;
default: usage(stderr); return 1;
}
}
argc -= optind;
argv += optind;
size_t grammars = 0;
test::bundle_index_t bundleIndex;
for(int i = 0; i < argc; ++i)
{
if(access(argv[i], R_OK) != 0)
{
fprintf(stderr, "%s: error reading grammar %s\n", getprogname(), argv[i]);
exit(1);
}
std::string tmp;
plist::dictionary_t plist = plist::load(argv[i]);
if(!plist::get_key_path(plist, "scopeName", tmp))
{
fprintf(stderr, "%s: error parsing grammar %s\n", getprogname(), argv[i]);
exit(1);
}
if(!bundleIndex.add(bundles::kItemTypeGrammar, plist))
{
fprintf(stderr, "%s: error parsing grammar %s\n", getprogname(), argv[i]);
exit(1);
}
if(grammar == NULL_STR)
grammar = tmp;
++grammars;
}
if(grammars == 0 || !bundleIndex.commit())
{
fprintf(stderr, "%s: no grammars loaded\n", getprogname());
exit(1);
}
parse_stdin(grammar, verbose);
return 0;
}

2
Applications/gtm/target Normal file
View File

@@ -0,0 +1,2 @@
SOURCES = src/*.cc
LINK += bundles parse

View File

@@ -0,0 +1,118 @@
#include <text/indent.h>
#include <text/tokenize.h>
#include <io/path.h>
#include <regexp/indent.h>
#include <plist/plist.h>
#include <oak/oak.h>
static double const AppVersion = 1.0;
static size_t const AppRevision = APP_REVISION;
extern char* optarg;
extern int optind;
static void usage (FILE* io = stdout)
{
fprintf(io, "%1$s %2$.1f (" COMPILE_DATE " revision %3$zu)\n", getprogname(), AppVersion, AppRevision);
fprintf(io, "Usage: %s [ptishv] «file» ..\n", getprogname());
fprintf(io, "\n");
fprintf(io, "Options:\n");
fprintf(io, " -p/--patterns «plist» A tmPreferences file with indent settings.\n");
fprintf(io, " -t/--tab «size» Set tab size (default: 3).\n");
fprintf(io, " -i/--indent «size» Set indent size (default: tab size).\n");
fprintf(io, " -s/--spaces Use spaces instead of tabs for indent.\n");
fprintf(io, " -h/--help Show this help.\n");
fprintf(io, " -v/--version Show version number.\n");
}
static void version ()
{
fprintf(stdout, "%1$s %2$.1f (" COMPILE_DATE " revision %3$zu)\n", getprogname(), AppVersion, AppRevision);
}
int main (int argc, char const* argv[])
{
static struct option const longopts[] = {
{ "patterns", required_argument, 0, 'p' },
{ "tab", required_argument, 0, 't' },
{ "indent", required_argument, 0, 'i' },
{ "spaces", no_argument, 0, 's' },
{ "help", no_argument, 0, 'h' },
{ "version", no_argument, 0, 'v' },
{ 0, 0, 0, 0 }
};
std::string patterns = NULL_STR;
size_t tabSize = 3;
size_t indentSize = 0;
bool softTabs = false;
unsigned int ch;
while((ch = getopt_long(argc, (char* const*)argv, "p:t:i:shv", longopts, NULL)) != -1)
{
switch(ch)
{
case 'p': patterns = optarg; break;
case 't': tabSize = strtol(optarg, NULL, 10); break;
case 'i': indentSize = strtol(optarg, NULL, 10); break;
case 's': softTabs = true; break;
case 'h': usage(); return 0;
case 'v': version(); return 0;
case '?': /* unknown option */ return 1;
case ':': /* missing option */ return 1;
default: usage(stderr); return 1;
}
}
if(optind == argc)
return usage(stderr), 0;
std::vector<std::string> files;
for(int i = optind; i < argc; ++i)
files.push_back(argv[i]);
patterns = patterns == NULL_STR ? "/Users/duff/Library/Application Support/Avian/Bundles/php.tmbundle/Preferences/Indentation Rules.tmPreferences" : patterns;
indentSize = indentSize == 0 ? tabSize : indentSize;
if(files.empty())
files.push_back("/Users/duff/Desktop/indent-test.html");
plist::dictionary_t plist = plist::load(patterns);
regexp::pattern_t array[4];
static std::string const keys[] = { "increaseIndentPattern", "decreaseIndentPattern", "indentNextLinePattern", "unIndentedLinePattern" };
iterate(key, keys)
{
std::string tmp;
if(plist::get_key_path(plist, "settings." + *key, tmp))
array[key - keys] = tmp;
}
// ==========
// = Indent =
// ==========
text::indent_t indent(tabSize, indentSize, softTabs);
iterate(file, files)
{
indent::fsm_t fsm(array, 1, 1);
std::string const str = path::content(*file);
std::string::size_type n = 0;
while(n != std::string::npos)
{
while(n < str.size() && (str[n] == ' ' || str[n] == '\t'))
++n;
std::string::size_type eol = str.find('\n', n);
if(eol < str.size())
++eol;
std::string line = str.substr(n, eol - n);
fprintf(stdout, "%s%s", indent.create(0, fsm.scan_line(line)).c_str(), line.c_str());
n = eol;
}
}
return 0;
}

View File

@@ -0,0 +1,2 @@
SOURCES = src/*.cc
LINK += text io regexp plist

View File

@@ -0,0 +1,415 @@
#include <authorization/constants.h>
#include <authorization/authorization.h>
#include <oak/oak.h>
#include <text/format.h>
#include <cf/cf.h>
#include <io/path.h>
static double const AppVersion = 2.0;
static size_t const AppRevision = APP_REVISION;
#define SOCKET_PATH "/tmp/avian.sock"
// mate returns when all files specified has been opened.
// If - is used for filename, stdin is read and opened as a new buffer.
// If -w is given, mate waits for all files to be closed, before it returns.
// If -w is used with -, the buffer is written to stdout
// If -w is used without any file names, - is implied
struct disable_sudo_helper_t
{
disable_sudo_helper_t () : _uid(geteuid()), _gid(getegid())
{
if(_uid != 0)
return;
if(getenv("SUDO_UID") && getenv("SUDO_GID"))
{
setegid(atoi(getenv("SUDO_GID")));
seteuid(atoi(getenv("SUDO_UID")));
}
}
~disable_sudo_helper_t ()
{
if(_uid != 0)
return;
seteuid(_uid);
setegid(_gid);
}
private:
uid_t _uid;
gid_t _gid;
};
static bool find_app (FSRef* outAppRef, std::string* outAppStr)
{
disable_sudo_helper_t helper;
CFURLRef appURL;
OSStatus err = LSFindApplicationForInfo(kLSUnknownCreator, CFSTR("com.macromates.TextMate.preview"), NULL, outAppRef, &appURL);
if(err != noErr)
return fprintf(stderr, "Cant find TextMate.app (error %d)\n", (int)err), false;
if(outAppStr)
{
if(CFStringRef appPath = CFURLCopyFileSystemPath(appURL, kCFURLPOSIXPathStyle))
{
*outAppStr = cf::to_s(appPath);
CFRelease(appPath);
}
CFRelease(appURL);
}
return err == noErr;
}
static void launch_app ()
{
disable_sudo_helper_t helper;
FSRef appFSRef;
if(!find_app(&appFSRef, NULL))
abort();
std::map<std::string, std::string> tmp;
tmp["OAK_DISABLE_UNTITLED_FILE"] = "YES";
cf::dictionary_t environment(tmp);
struct LSApplicationParameters const appParams = { 0, kLSLaunchDontAddToRecents|kLSLaunchDontSwitch|kLSLaunchAndDisplayErrors, &appFSRef, NULL, environment, NULL, NULL };
OSStatus err = LSOpenApplication(&appParams, NULL);
if(err != noErr)
{
fprintf(stderr, "Cant launch TextMate.app (error %d)", (int)err);
abort();
}
}
static void install_auth_tool ()
{
if(geteuid() == 0 && (!path::exists(kAuthToolPath) || !path::exists(kAuthPlistPath) || AuthorizationRightGet(kAuthRightName, NULL) == errAuthorizationDenied))
{
std::string appStr = NULL_STR;
if(!find_app(NULL, &appStr))
abort();
std::string toolPath = path::join(appStr, "Contents/Resources/PrivilegedTool");
char const* arg0 = toolPath.c_str();
if(access(arg0, X_OK) != 0)
{
fprintf(stderr, "No such executable file: %s\n", arg0);
abort();
}
pid_t pid = vfork();
if(pid == 0)
{
execl(arg0, arg0, "--install", NULL);
_exit(errno);
}
if(pid != -1)
{
int status = 0;
if(waitpid(pid, &status, 0) == pid && WIFEXITED(status) && WEXITSTATUS(status) != 0)
fprintf(stderr, "%s: %s\n", arg0, strerror(WEXITSTATUS(status)));
}
}
}
static void usage (FILE* io)
{
std::string pad(10 - std::min(strlen(getprogname()), size_t(10)), ' ');
fprintf(io,
"%1$s %2$.1f (" COMPILE_DATE " revision %3$zu)\n"
"Usage: %1$s [-awl<number>t<filetype>rdnhv] [file ...]\n"
"Options:\n"
" -a, --async Do not wait for file to be closed by TextMate.\n"
" -w, --wait Wait for file to be closed by TextMate.\n"
" -l, --line <number> Place caret on line <number> after loading file.\n"
" -t, --type <filetype> Treat file as having <filetype>.\n"
" -m, --name <name> The display name shown in TextMate.\n"
" -r, --recent Add file to Open Recent menu.\n"
" -d, --change-dir Change TextMates working directory to that of the file.\n"
" -n, --no-reactivation After edit with -w, do not re-activate the calling app.\n"
" -h, --help Show this information.\n"
" -v, --version Print version information.\n"
"\n"
"If multiple files are given, a project is created consisting of these\n"
"files, -a is then default and -w will be ignored (e.g. \"%1$s *.tex\").\n"
"\n"
"By default %1$s will not wait for the file to be closed\nexcept when used as filter:\n"
" ls *.tex|%1$s|sh%4$s-w implied\n"
" %1$s -|cat -n %4$s-w implied (read from stdin)\n"
"\n"
"An exception is made if the command is started as something which ends\nwith \"_wait\". "
"So to have a command with --wait as default, you can\ncreate a symbolic link like this:\n"
" ln -s %1$s %1$s_wait\n"
"\n", getprogname(), AppVersion, AppRevision, pad.c_str()
);
}
static void version ()
{
fprintf(stdout, "%1$s %2$.1f (" COMPILE_DATE " revision %3$zu)\n", getprogname(), AppVersion, AppRevision);
}
static void append (std::string const& str, std::vector<std::string>& v)
{
std::string::size_type from = 0;
while(from < str.size() && from != std::string::npos)
{
std::string::size_type to = str.find(',', from);
v.push_back(std::string(str.begin() + from, to == std::string::npos ? str.end() : str.begin() + to));
from = to == std::string::npos ? to : to + 1;
}
}
static void write_key_pair (int fd, std::string const& key, std::string const& value)
{
std::string const str = key + ": " + value + "\r\n";
write(fd, str.data(), str.size());
}
int main (int argc, char* argv[])
{
extern char* optarg;
extern int optind;
static struct option const longopts[] = {
{ "async", no_argument, 0, 'a' },
{ "wait", no_argument, 0, 'w' },
{ "line", required_argument, 0, 'l' },
{ "type", required_argument, 0, 't' },
{ "name", required_argument, 0, 'm' },
{ "project", required_argument, 0, 'p' },
{ "recent", no_argument, 0, 'r' },
{ "change-dir", no_argument, 0, 'd' },
{ "help", no_argument, 0, 'h' },
{ "version", no_argument, 0, 'v' },
{ "server", no_argument, 0, 's' },
{ 0, 0, 0, 0 }
};
std::vector<std::string> files, lines, types, names, projects;
bool add_to_recent = false;
bool change_dir = false;
bool server = false;
int should_wait = -1, ch;
if(strlen(getprogname()) > 5 && strcmp(getprogname() + strlen(getprogname()) - 5, "_wait") == 0)
should_wait = true;
install_auth_tool();
while((ch = getopt_long(argc, argv, "awrdhvl:t:m:sp:", longopts, NULL)) != -1)
{
switch(ch)
{
case 'a': should_wait = false; break;
case 'w': should_wait = true; break;
case 'l': append(optarg, lines); break;
case 't': append(optarg, types); break;
case 'm': append(optarg, names); break;
case 'p': append(optarg, projects); break;
case 'r': add_to_recent = true; break;
case 'd': change_dir = true; break;
case 'h': usage(stdout); return 0;
case 'v': version(); return 0;
case 's': server = true; break;
case '?': /* unknown option */ return 1;
case ':': /* missing option */ return 1;
default: usage(stderr); return 1;
}
}
argc -= optind;
argv += optind;
for(int i = 0; i < argc; ++i)
{
char* path = argv[i];
if(path[0] == 0)
continue;
if(strcmp(path, "-") != 0 && path[0] != '/') // relative path, make absolute
{
if(char* cwd = getcwd(NULL, (size_t)-1))
{
asprintf(&path, "%s/%s", cwd, path);
free(cwd);
}
else
{
fprintf(stderr, "failed to get current working directory\n");
exit(1);
}
}
files.push_back(path);
}
std::string defaultProject = projects.empty() ? (getenv("TM_PROJECT_UUID") ?: "") : projects.back();
bool stdinIsAPipe = isatty(STDIN_FILENO) == 0;
if(files.empty() && (should_wait == true || stdinIsAPipe))
files.push_back("-");
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr = { 0, AF_UNIX, SOCKET_PATH };
addr.sun_len = SUN_LEN(&addr);
bool didLaunch = false;
while(-1 == connect(fd, (sockaddr*)&addr, sizeof(addr)))
{
if(!didLaunch)
launch_app();
didLaunch = true;
usleep(500000);
}
char buf[1024];
ssize_t len = read(fd, buf, sizeof(buf));
if(len == -1)
exit(-1);
// fprintf(stderr, "%.*s", (int)len, buf);
for(size_t i = 0; i < files.size(); ++i)
{
write(fd, "open\r\n", 6);
if(files[i] == "-")
{
if(!stdinIsAPipe)
fprintf(stderr, "Reading from stdin, press ^D to stop\n");
while(ssize_t len = read(STDIN_FILENO, buf, sizeof(buf)))
{
if(len == -1)
break;
write_key_pair(fd, "data", text::format("%zu", len));
write(fd, buf, len);
}
bool stdoutIsAPipe = isatty(STDOUT_FILENO) == 0;
bool wait = should_wait == true || (should_wait != false && stdoutIsAPipe);
write_key_pair(fd, "display-name", i < names.size() ? names[i] : "untitled (stdin)");
write_key_pair(fd, "data-on-close", wait && stdoutIsAPipe ? "yes" : "no");
write_key_pair(fd, "wait", wait ? "yes" : "no");
write_key_pair(fd, "re-activate", wait ? "yes" : "no");
}
else
{
write_key_pair(fd, "path", files[i]);
write_key_pair(fd, "display-name", i < names.size() ? names[i] : "");
write_key_pair(fd, "wait", should_wait == true ? "yes" : "no");
write_key_pair(fd, "re-activate", should_wait == true ? "yes" : "no");
}
osx::authorization_t* auth = new osx::authorization_t; // we deliberately do not dispose this since it need to be valid when the server acts on it
if(geteuid() == 0 && auth->obtain_right(kAuthRightName))
write_key_pair(fd, "authorization", *auth);
write_key_pair(fd, "selection", i < lines.size() ? lines[i] : "");
write_key_pair(fd, "file-type", i < types.size() ? types[i] : "");
write_key_pair(fd, "project-uuid", i < projects.size() ? projects[i] : defaultProject);
write_key_pair(fd, "add-to-recents", add_to_recent ? "yes" : "no");
write_key_pair(fd, "change-directory", change_dir ? "yes" : "no");
write(fd, "\r\n", 2);
}
write(fd, ".\r\n", 3);
// =========================
// = Now wait for messages =
// =========================
enum { command, arguments, data, done } state = command;
ssize_t bytesLeft = 0;
std::string line;
while(state != done && (len = read(fd, buf, sizeof(buf))))
{
if(len == -1)
{
perror("read()");
break;
}
if(state == data)
{
ssize_t dataLen = std::min(len, bytesLeft);
// fprintf(stderr, "Got data, %zd bytes\n", dataLen);
write(STDOUT_FILENO, buf, dataLen);
memmove(buf, buf + dataLen, len - dataLen);
bytesLeft -= dataLen;
len -= dataLen;
state = bytesLeft == 0 ? arguments : data;
}
line.insert(line.end(), buf, buf + len);
if(state == data && bytesLeft != 0)
continue;
while(line.find('\n') != std::string::npos)
{
std::string::size_type eol = line.find('\n');
std::string str = line.substr(0, eol);
if(!str.empty() && str[str.size()-1] == '\r')
str.resize(str.size()-1);
line.erase(line.begin(), line.begin() + eol + 1);
if(str.empty())
{
state = command;
// fprintf(stderr, "Got end of record\n");
}
else if(state == command)
{
if(str == "close")
{
state = arguments;
}
// fprintf(stderr, "Got command %s\n", str.c_str());
}
else if(state == arguments)
{
std::string::size_type n = str.find(':');
if(n != std::string::npos)
{
std::string key = str.substr(0, n);
std::string value = str.substr(n+2);
if(key == "data")
{
bytesLeft = strtol(value.c_str(), NULL, 10);
// fprintf(stderr, "Got data of size %zd\n", bytesLeft);
size_t dataLen = std::min((ssize_t)line.size(), bytesLeft);
// fprintf(stderr, "Got data, %zd bytes\n", dataLen);
write(STDOUT_FILENO, line.data(), dataLen);
line.erase(line.begin(), line.begin() + dataLen);
bytesLeft -= dataLen;
state = bytesLeft == 0 ? arguments : data;
}
else
{
// fprintf(stderr, "Got argument: %s = %s\n", key.c_str(), value.c_str());
}
}
}
}
}
close(fd);
return 0;
}

3
Applications/mate/target Normal file
View File

@@ -0,0 +1,3 @@
SOURCES = src/*.cc
LINK += authorization
FRAMEWORKS = ApplicationServices Security

View File

@@ -0,0 +1,90 @@
#include <plist/ascii.h>
static double const AppVersion = 2.0;
static size_t const AppRevision = APP_REVISION;
void version ()
{
fprintf(stdout, "%1$s %2$.1f (" COMPILE_DATE " revision %3$zu)\n", getprogname(), AppVersion, AppRevision);
}
void usage (FILE* io)
{
fprintf(io,
"%1$s %2$.1f (" COMPILE_DATE " revision %3$zu)\n"
"Usage: %1$s [-axhv]\n"
"Description:\n"
" Reads a property list from standard input and outputs ASCII version to stdout.\n"
"Options:\n"
" -a, --ascii Parse TextMates ASCII property list variant.\n"
" -x, --extended Output TextMates ASCII property list variant.\n"
" -h, --help Show this information.\n"
" -v, --version Print version information.\n"
"\n", getprogname(), AppVersion, AppRevision
);
}
int main (int argc, char* const* argv)
{
// extern char* optarg;
extern int optind;
static struct option const longopts[] = {
{ "ascii", no_argument, 0, 'a' },
{ "extended", no_argument, 0, 'x' },
{ "help", no_argument, 0, 'h' },
{ "version", no_argument, 0, 'v' },
{ 0, 0, 0, 0 }
};
bool ascii = false, extended = false;
int ch;
while((ch = getopt_long(argc, argv, "axhv", longopts, NULL)) != -1)
{
switch(ch)
{
case 'a': ascii = true; break;
case 'x': extended = true; break;
case 'h': usage(stdout); return 0;
case 'v': version(); return 0;
default: usage(stderr); return 1;
}
}
argc -= optind;
argv += optind;
std::string data;
char buf[1024];
while(size_t len = fread(buf, 1, sizeof(buf), stdin))
data.insert(data.end(), buf, buf + len);
if(ascii)
{
if(CFPropertyListRef cfPlist = plist::create_cf_property_list(plist::parse_ascii(data)))
{
if(CFURLRef url = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, CFSTR("/dev/stdout"), kCFURLPOSIXPathStyle, false))
{
if(CFWriteStreamRef writeStream = CFWriteStreamCreateWithFile(kCFAllocatorDefault, url))
{
CFStringRef error = NULL;
CFWriteStreamOpen(writeStream);
CFPropertyListWriteToStream(cfPlist, writeStream, kCFPropertyListXMLFormat_v1_0, &error);
CFWriteStreamClose(writeStream);
CFRelease(writeStream);
}
CFRelease(url);
}
CFRelease(cfPlist);
}
}
else
{
std::string const& str = to_s(plist::parse(data), extended ? plist::kPreferSingleQuotedStrings : 0) + "\n";
write(STDOUT_FILENO, str.data(), str.size());
}
return 0;
}

View File

@@ -0,0 +1,2 @@
SOURCES = src/*.cc
LINK += plist

View File

@@ -0,0 +1,67 @@
#include <scm/scm.h>
#include <io/path.h>
#include <oak/oak.h>
static double const AppVersion = 1.0;
static size_t const AppRevision = APP_REVISION;
static void version ()
{
fprintf(stdout, "%1$s %2$.1f (" COMPILE_DATE " revision %3$zu)\n", getprogname(), AppVersion, AppRevision);
}
static void usage (FILE* io = stdout)
{
fprintf(io,
"%1$s %2$.1f (" COMPILE_DATE " revision %3$zu)\n"
"Usage: %1$s [-hv] ...\n"
"Options:\n"
" -h, --help Show this information.\n"
" -v, --version Print version information.\n"
"\n", getprogname(), AppVersion, AppRevision
);
}
static void scmls (std::string const& dir)
{
citerate(pair, scm::tracked_files(dir, scm::status::modified|scm::status::added|scm::status::deleted|scm::status::conflicted))
fprintf(stderr, "%s %s\n", to_s(pair->second).c_str(), pair->first.c_str());
}
int main (int argc, char* const* argv)
{
// extern char* optarg;
extern int optind;
static struct option const longopts[] = {
{ "help", no_argument, 0, 'h' },
{ "version", no_argument, 0, 'v' },
{ 0, 0, 0, 0 }
};
int ch;
while((ch = getopt_long(argc, argv, "hv", longopts, NULL)) != -1)
{
switch(ch)
{
case 'h': usage(); return 0;
case 'v': version(); return 0;
default: usage(stderr); return 1;
}
}
argc -= optind;
argv += optind;
if(argc == 0)
{
scmls(path::cwd());
}
else
{
for(int i = 0; i < argc; ++i)
scmls(path::join(path::cwd(), argv[i]));
}
return 0;
}

View File

@@ -0,0 +1,2 @@
SOURCES = src/*.cc
LINK += scm io

3
Applications/target Normal file
View File

@@ -0,0 +1,3 @@
BUILD = app
FLAGS += -D'APP_REVISION=$APP_REVISION' -DCOMPILE_DATE=\"`date +%Y-%m-%d`\"
TARGETS = */target

12
COPYING Normal file
View File

@@ -0,0 +1,12 @@
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 553 B

Some files were not shown because too many files have changed in this diff Show More