TextMate is a graphical text editor for OS X 10.7+
修訂 | 92193a54e9a815a455a7a9e80e2aff022f67798e (tree) |
---|---|
時間 | 2012-08-20 04:08:06 |
作者 | Allan Odgaard <git@abet...> |
Commiter | Allan Odgaard |
Add preliminary bundle install support.
@@ -1,6 +1,7 @@ | ||
1 | 1 | #import "AppController.h" |
2 | 2 | #import "Favorites.h" |
3 | 3 | #import "CreditsWindowController.h" |
4 | +#import "InstallBundleItems.h" | |
4 | 5 | #import <oak/CocoaSTL.h> |
5 | 6 | #import <oak/oak.h> |
6 | 7 | #import <oak/debug.h> |
@@ -24,9 +25,15 @@ OAK_DEBUG_VAR(AppController); | ||
24 | 25 | void OakOpenDocuments (NSArray* paths) |
25 | 26 | { |
26 | 27 | std::vector<document::document_ptr> documents; |
28 | + NSMutableArray* itemsToInstall = [NSMutableArray array]; | |
27 | 29 | for(NSString* path in paths) |
28 | 30 | { |
29 | - if(path::is_directory(to_s(path))) | |
31 | + static std::string const tmItemExtensions[] = { "tmbundle", "tmcommand", "tmdragcommand", "tmlanguage", "tmmacro", "tmplugin", "tmpreferences", "tmsnippet", "tmtheme" }; | |
32 | + if(oak::contains(beginof(tmItemExtensions), endof(tmItemExtensions), to_s([[path pathExtension] lowercaseString])) && !([NSEvent modifierFlags] & NSAlternateKeyMask)) | |
33 | + { | |
34 | + [itemsToInstall addObject:path]; | |
35 | + } | |
36 | + else if(path::is_directory(to_s(path))) | |
30 | 37 | { |
31 | 38 | document::show_browser(to_s(path)); |
32 | 39 | } |
@@ -36,6 +43,9 @@ void OakOpenDocuments (NSArray* paths) | ||
36 | 43 | } |
37 | 44 | } |
38 | 45 | |
46 | + if([itemsToInstall count]) | |
47 | + InstallBundleItems(itemsToInstall); | |
48 | + | |
39 | 49 | document::show(documents); |
40 | 50 | } |
41 | 51 |
@@ -0,0 +1,3 @@ | ||
1 | +#import <oak/misc.h> | |
2 | + | |
3 | +PUBLIC void InstallBundleItems (NSArray* itemPaths); |
@@ -0,0 +1,210 @@ | ||
1 | +#import "InstallBundleItems.h" | |
2 | +#import <BundleEditor/BundleEditor.h> | |
3 | +#import <OakFoundation/NSString Additions.h> | |
4 | +#import <bundles/bundles.h> | |
5 | +#import <text/ctype.h> | |
6 | +#import <regexp/format_string.h> | |
7 | +#import <io/io.h> | |
8 | +#import <ns/ns.h> | |
9 | + | |
10 | +static std::map<std::string, bundles::item_ptr> installed_items () | |
11 | +{ | |
12 | + std::map<std::string, bundles::item_ptr> res; | |
13 | + citerate(item, bundles::query(bundles::kFieldAny, NULL_STR, scope::wildcard, bundles::kItemTypeAny, oak::uuid_t(), false, true)) | |
14 | + { | |
15 | + citerate(path, (*item)->paths()) | |
16 | + res.insert(std::make_pair(*path, *item)); | |
17 | + } | |
18 | + return res; | |
19 | +} | |
20 | + | |
21 | +void InstallBundleItems (NSArray* itemPaths) | |
22 | +{ | |
23 | + struct info_t | |
24 | + { | |
25 | + info_t (std::string const& path, std::string const& name, oak::uuid_t const& uuid, bool isBundle, bundles::item_ptr installed = bundles::item_ptr()) : path(path), name(name), uuid(uuid), is_bundle(isBundle), installed(installed) { } | |
26 | + | |
27 | + std::string path; | |
28 | + std::string name; | |
29 | + oak::uuid_t uuid; | |
30 | + bool is_bundle; | |
31 | + bundles::item_ptr installed; | |
32 | + }; | |
33 | + | |
34 | + std::map<std::string, bundles::item_ptr> const installedItems = installed_items(); | |
35 | + std::vector<info_t> installed, toInstall, delta, malformed; | |
36 | + | |
37 | + for(NSString* path in itemPaths) | |
38 | + { | |
39 | + bool isDelta; | |
40 | + std::string bundleName; | |
41 | + oak::uuid_t bundleUUID; | |
42 | + bundles::item_ptr installedItem; | |
43 | + | |
44 | + bool isBundle = [[[path pathExtension] lowercaseString] isEqualToString:@"tmbundle"]; | |
45 | + std::string const loadPath = isBundle ? path::join(to_s(path), "info.plist") : to_s(path); | |
46 | + plist::dictionary_t const infoPlist = plist::load(loadPath); | |
47 | + | |
48 | + auto it = installedItems.find(loadPath); | |
49 | + if(it != installedItems.end()) | |
50 | + installedItem = it->second; | |
51 | + | |
52 | + if(plist::get_key_path(infoPlist, "isDelta", isDelta) && isDelta) | |
53 | + { | |
54 | + delta.push_back(info_t(to_s(path), NULL_STR, oak::uuid_t(), isBundle, installedItem)); | |
55 | + } | |
56 | + else if(plist::get_key_path(infoPlist, "name", bundleName) && plist::get_key_path(infoPlist, "uuid", bundleUUID)) | |
57 | + { | |
58 | + if(installedItem) | |
59 | + installed.push_back(info_t(to_s(path), bundleName, bundleUUID, isBundle, installedItem)); | |
60 | + else toInstall.push_back(info_t(to_s(path), bundleName, bundleUUID, isBundle, installedItem)); | |
61 | + } | |
62 | + else | |
63 | + { | |
64 | + malformed.push_back(info_t(to_s(path), NULL_STR, oak::uuid_t(), isBundle, installedItem)); | |
65 | + } | |
66 | + } | |
67 | + | |
68 | + iterate(info, delta) | |
69 | + { | |
70 | + char const* type = info->is_bundle ? "bundle" : "bundle item"; | |
71 | + std::string const name = path::name(path::strip_extension(info->path)); | |
72 | + std::string const title = text::format("The %s “%s” could not be installed because it is in delta format.", type, name.c_str()); | |
73 | + NSRunAlertPanel([NSString stringWithCxxString:title], @"Contact the author of this %s to get a properly exported version.", @"OK", nil, nil, type); | |
74 | + } | |
75 | + | |
76 | + iterate(info, malformed) | |
77 | + { | |
78 | + char const* type = info->is_bundle ? "bundle" : "bundle item"; | |
79 | + std::string const name = path::name(path::strip_extension(info->path)); | |
80 | + std::string const title = text::format("The %s “%s” could not be installed because it is malformed.", type, name.c_str()); | |
81 | + NSRunAlertPanel([NSString stringWithCxxString:title], @"The %s lacks mandatory keys in its property list file.", @"OK", nil, nil, type); | |
82 | + } | |
83 | + | |
84 | + iterate(info, installed) | |
85 | + { | |
86 | + char const* type = info->is_bundle ? "bundle" : "bundle item"; | |
87 | + std::string const name = info->name; | |
88 | + std::string const title = text::format("The %s “%s” is already installed.", type, name.c_str()); | |
89 | + int choice = NSRunAlertPanel([NSString stringWithCxxString:title], @"You can edit the installed %s to inspect it.", @"OK", @"Edit", nil, type); | |
90 | + if(choice == NSAlertAlternateReturn) // "Edit" | |
91 | + [[BundleEditor sharedInstance] revealBundleItem:info->installed]; | |
92 | + } | |
93 | + | |
94 | + iterate(info, toInstall) | |
95 | + { | |
96 | + if(info->is_bundle) | |
97 | + { | |
98 | + int choice = NSRunAlertPanel([NSString stringWithFormat:@"Would you like to install the “%@” bundle?", [NSString stringWithCxxString:info->name]], @"Installing a bundle adds new functionality to TextMate.", @"Install", @"Cancel", nil); | |
99 | + if(choice == NSAlertDefaultReturn) // "Install" | |
100 | + { | |
101 | + std::string const installDir = path::join(path::home(), "Library/Application Support/Avian/Pristine Copy/Bundles"); | |
102 | + if(path::make_dir(installDir)) | |
103 | + { | |
104 | + std::string const installPath = path::unique(path::join(installDir, path::name(info->path))); | |
105 | + if(path::copy(info->path, installPath)) | |
106 | + { | |
107 | + fprintf(stderr, "installed bundle at: %s\n", installPath.c_str()); | |
108 | + continue; | |
109 | + } | |
110 | + } | |
111 | + fprintf(stderr, "failed to install bundle: %s\n", info->path.c_str()); | |
112 | + } | |
113 | + } | |
114 | + else | |
115 | + { | |
116 | + oak::uuid_t defaultBundle; | |
117 | + if(path::extension(info->path) == ".tmTheme") | |
118 | + { | |
119 | + citerate(item, bundles::query(bundles::kFieldAny, NULL_STR, scope::wildcard, bundles::kItemTypeBundle, "A4380B27-F366-4C70-A542-B00D26ED997E")) | |
120 | + defaultBundle = (*item)->uuid(); | |
121 | + } | |
122 | + | |
123 | + std::map<std::string, std::string> vars; | |
124 | + vars.insert(std::make_pair("TM_FULLNAME", getpwuid(getuid())->pw_gecos ?: "John Doe")); | |
125 | + std::string personalBundleName = format_string::expand("${TM_FULLNAME/^(\\S+).*$/$1/}’s Bundle", vars); | |
126 | + // std::string personalBundleName = format_string::expand("${TM_FULLNAME/^(\\S+).*$/$1/}’s Bundle", std::map<std::string, std::string>{ { "TM_FULLNAME", getpwuid(getuid())->pw_gecos ?: "John Doe" } }); | |
127 | + if(!defaultBundle) | |
128 | + { | |
129 | + citerate(item, bundles::query(bundles::kFieldName, personalBundleName, scope::wildcard, bundles::kItemTypeBundle)) | |
130 | + defaultBundle = (*item)->uuid(); | |
131 | + } | |
132 | + | |
133 | + NSPopUpButton* bundleChooser = [[[NSPopUpButton alloc] initWithFrame:NSZeroRect pullsDown:NO] autorelease]; | |
134 | + [bundleChooser.menu removeAllItems]; | |
135 | + [bundleChooser.menu addItemWithTitle:@"Create new bundle…" action:NULL keyEquivalent:@""]; | |
136 | + [bundleChooser.menu addItem:[NSMenuItem separatorItem]]; | |
137 | + | |
138 | + std::multimap<std::string, bundles::item_ptr, text::less_t> ordered; | |
139 | + citerate(item, bundles::query(bundles::kFieldAny, NULL_STR, scope::wildcard, bundles::kItemTypeBundle)) | |
140 | + ordered.insert(std::make_pair((*item)->name(), *item)); | |
141 | + NSMenuItem* selectedItem = nil; | |
142 | + iterate(pair, ordered) | |
143 | + { | |
144 | + NSMenuItem* menuItem = [bundleChooser.menu addItemWithTitle:[NSString stringWithCxxString:pair->first] action:NULL keyEquivalent:@""]; | |
145 | + [menuItem setRepresentedObject:[NSString stringWithCxxString:to_s(pair->second->uuid())]]; | |
146 | + if(defaultBundle && defaultBundle == pair->second->uuid()) | |
147 | + selectedItem = menuItem; | |
148 | + } | |
149 | + if(selectedItem) | |
150 | + [bundleChooser selectItem:selectedItem]; | |
151 | + | |
152 | + [bundleChooser sizeToFit]; | |
153 | + NSRect frame = [bundleChooser frame]; | |
154 | + if(NSWidth(frame) > 200) | |
155 | + [bundleChooser setFrameSize:NSMakeSize(200, NSHeight(frame))]; | |
156 | + | |
157 | + NSAlert* alert = [NSAlert alertWithMessageText:[NSString stringWithFormat:@"Would you like to install the “%@” bundle item?", [NSString stringWithCxxString:info->name]] defaultButton:@"Install" alternateButton:@"Cancel" otherButton:nil informativeTextWithFormat:@"Installing a bundle item adds new functionality to TextMate."]; | |
158 | + [alert setAccessoryView:bundleChooser]; | |
159 | + if([alert runModal] == NSAlertDefaultReturn) // "Install" | |
160 | + { | |
161 | + static struct { std::string extension; std::string directory; } DirectoryMap[] = | |
162 | + { | |
163 | + { ".tmCommand", "Commands" }, | |
164 | + { ".tmDragCommand", "DragCommands" }, | |
165 | + { ".tmMacro", "Macros" }, | |
166 | + { ".tmPreferences", "Preferences" }, | |
167 | + { ".tmSnippet", "Snippets" }, | |
168 | + { ".tmLanguage", "Syntaxes" }, | |
169 | + { ".tmProxy", "Proxies" }, | |
170 | + { ".tmTheme", "Themes" }, | |
171 | + }; | |
172 | + | |
173 | + if(NSString* bundleUUID = [[bundleChooser selectedItem] representedObject]) | |
174 | + { | |
175 | + citerate(item, bundles::query(bundles::kFieldAny, NULL_STR, scope::wildcard, bundles::kItemTypeBundle, to_s(bundleUUID))) | |
176 | + { | |
177 | + if((*item)->local() || (*item)->save()) | |
178 | + { | |
179 | + std::string dest = path::parent((*item)->paths().front()); | |
180 | + iterate(iter, DirectoryMap) | |
181 | + { | |
182 | + if(path::extension(info->path) == iter->extension) | |
183 | + { | |
184 | + dest = path::join(dest, iter->directory); | |
185 | + if(path::make_dir(dest)) | |
186 | + { | |
187 | + dest = path::join(dest, path::name(info->path)); | |
188 | + dest = path::unique(dest); | |
189 | + if(path::copy(info->path, dest)) | |
190 | + break; | |
191 | + else fprintf(stderr, "error: copy(‘%s’, ‘%s’)\n", info->path.c_str(), dest.c_str()); | |
192 | + } | |
193 | + else | |
194 | + { | |
195 | + fprintf(stderr, "error: makedir(‘%s’)\n", dest.c_str()); | |
196 | + } | |
197 | + } | |
198 | + } | |
199 | + | |
200 | + } | |
201 | + } | |
202 | + } | |
203 | + else | |
204 | + { | |
205 | + fprintf(stderr, "Create new bundle for item\n"); | |
206 | + } | |
207 | + } | |
208 | + } | |
209 | + } | |
210 | +} |