├── .gitignore ├── LICENSE.md ├── MarvinPlugIn └── Source │ └── MarvinPlugIn │ ├── LegacyXcodeManager.h │ ├── LegacyXcodeManager.m │ ├── MarvinPlugin-Bridging-Header.h │ ├── MarvinPlugin.swift │ ├── MarvinSettingsWindowControllerSwift.swift │ └── XcodeManager.swift ├── MarvinPlugin.xcodeproj └── project.pbxproj ├── MarvinPlugin ├── Source │ └── MarvinPlugin │ │ ├── Defaults.plist │ │ ├── NSDocument+Propersave.swift │ │ ├── Settings.xib │ │ └── XcodePrivate.h └── Supporting Files │ ├── MarvinPlugIn-Info.plist │ ├── MarvinPlugIn-Prefix.pch │ └── en.lproj │ └── InfoPlist.strings ├── README.md ├── keyboard-shortcuts.png └── screenshot.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | .DS_Store 3 | build/ 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | *.xcworkspace 13 | !default.xcworkspace 14 | xcuserdata 15 | profile 16 | *.moved-aside 17 | DerivedData 18 | .idea/ 19 | tmp/ -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Licensed under the **MIT** license 2 | 3 | > Copyright (c) 2014 Christoffer Winterkvist 4 | > 5 | > Permission is hereby granted, free of charge, to any person obtaining 6 | > a copy of this software and associated documentation files (the 7 | > "Software"), to deal in the Software without restriction, including 8 | > without limitation the rights to use, copy, modify, merge, publish, 9 | > distribute, sublicense, and/or sell copies of the Software, and to 10 | > permit persons to whom the Software is furnished to do so, subject to 11 | > the following conditions: 12 | > 13 | > The above copyright notice and this permission notice shall be 14 | > included in all copies or substantial portions of the Software. 15 | > 16 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | > EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | > MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | > IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | > CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | > TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | > SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /MarvinPlugIn/Source/MarvinPlugIn/LegacyXcodeManager.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface LegacyXcodeManager : NSObject 4 | 5 | - (id)currentEditor; 6 | - (IDESourceCodeDocument *)currentSourceCodeDocument; 7 | - (IDEEditorDocument *)currentDocument; 8 | - (void)save; 9 | 10 | @end 11 | -------------------------------------------------------------------------------- /MarvinPlugIn/Source/MarvinPlugIn/LegacyXcodeManager.m: -------------------------------------------------------------------------------- 1 | #import "LegacyXcodeManager.h" 2 | 3 | @implementation LegacyXcodeManager 4 | 5 | #pragma mark - Getters 6 | 7 | - (id)currentEditor { 8 | NSWindowController *currentWindowController = [[NSApp keyWindow] windowController]; 9 | 10 | if ([currentWindowController isKindOfClass:NSClassFromString(@"IDEWorkspaceWindowController")]) { 11 | IDEWorkspaceWindowController *workspaceController = (IDEWorkspaceWindowController *)currentWindowController; 12 | IDEEditorArea *editorArea = [workspaceController editorArea]; 13 | IDEEditorContext *editorContext = [editorArea lastActiveEditorContext]; 14 | return [editorContext editor]; 15 | } 16 | 17 | return nil; 18 | } 19 | 20 | - (IDESourceCodeDocument *)currentSourceCodeDocument { 21 | if ([[self currentEditor] isKindOfClass:NSClassFromString(@"IDESourceCodeEditor")]) { 22 | IDESourceCodeEditor *editor = [self currentEditor]; 23 | return editor.sourceCodeDocument; 24 | } 25 | 26 | if ([[self currentEditor] isKindOfClass:NSClassFromString(@"IDESourceCodeComparisonEditor")]) { 27 | IDESourceCodeComparisonEditor *editor = [self currentEditor]; 28 | if ([[editor primaryDocument] isKindOfClass:NSClassFromString(@"IDESourceCodeDocument")]) { 29 | IDESourceCodeDocument *document = (IDESourceCodeDocument *)editor.primaryDocument; 30 | return document; 31 | } 32 | } 33 | 34 | return nil; 35 | } 36 | 37 | - (IDEEditorDocument *)currentDocument { 38 | NSWindowController *currentWindowController = [[NSApp keyWindow] windowController]; 39 | 40 | if ([currentWindowController isKindOfClass:NSClassFromString(@"IDEWorkspaceWindowController")]) { 41 | IDEWorkspaceWindowController *workspaceController = (IDEWorkspaceWindowController *)currentWindowController; 42 | IDEEditorArea *editorArea = [workspaceController editorArea]; 43 | return editorArea.primaryEditorDocument; 44 | } 45 | 46 | return nil; 47 | } 48 | 49 | - (void)save { 50 | if ([[self currentSourceCodeDocument] isEqualTo:[self currentDocument]]) { 51 | [[self currentDocument] saveDocument:nil]; 52 | } else { 53 | [[self currentSourceCodeDocument] saveDocument:nil]; 54 | } 55 | } 56 | 57 | @end 58 | -------------------------------------------------------------------------------- /MarvinPlugIn/Source/MarvinPlugIn/MarvinPlugin-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "XcodePrivate.h" 2 | #import "LegacyXcodeManager.h" 3 | -------------------------------------------------------------------------------- /MarvinPlugIn/Source/MarvinPlugIn/MarvinPlugin.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | var marvinPlugin: MarvinPlugin? = nil 4 | 5 | extension NSObject { 6 | 7 | class func pluginDidLoad(_ bundle: Bundle) { 8 | guard let appName = Bundle.main.infoDictionary?["CFBundleName"] as? String 9 | , marvinPlugin == nil && appName == "Xcode" else { 10 | return 11 | } 12 | 13 | marvinPlugin = MarvinPlugin() 14 | marvinPlugin?.settingsController = MarvinSettingsWindowController(bundle: bundle) 15 | SaveSwizzler.swizzle() 16 | } 17 | } 18 | 19 | class MarvinPlugin: NSObject { 20 | 21 | lazy var xcode = XcodeManager() 22 | var settingsController: MarvinSettingsWindowController? 23 | 24 | deinit { 25 | NotificationCenter.default.removeObserver(self) 26 | } 27 | 28 | override init() { 29 | super.init() 30 | 31 | NotificationCenter.default.addObserver(self, selector: #selector(NSApplicationDelegate.applicationDidFinishLaunching(_:)), name: NSNotification.Name.NSApplicationDidFinishLaunching, object: nil) 32 | 33 | NotificationCenter.default.addObserver(self, selector: #selector(MarvinPlugin.properSave), name: NSNotification.Name(rawValue: "Save properly"), object: nil) 34 | } 35 | 36 | func applicationDidFinishLaunching(_ notification: Notification) { 37 | guard let mainMenu = NSApp.mainMenu else { return } 38 | let editMenuItem = mainMenu.item(withTitle: "Edit") 39 | 40 | if let editMenuItem = editMenuItem, let submenu = editMenuItem.submenu { 41 | let marvinMenu = NSMenu.init(title: "Marvin") 42 | var items = [NSMenuItem]() 43 | 44 | items.append(NSMenuItem.init(title: "Settings", action: #selector(MarvinPlugin.settingsMenuItemSelected), keyEquivalent: "")) 45 | items.append(NSMenuItem.separator()) 46 | items.append(NSMenuItem.init(title: "Delete Line", action: #selector(MarvinPlugin.deleteLineAction), keyEquivalent: "")) 47 | items.append(NSMenuItem.init(title: "Duplicate Line", action: #selector(MarvinPlugin.duplicateLineAction), keyEquivalent: "")) 48 | items.append(NSMenuItem.init(title: "Join Line", action: #selector(MarvinPlugin.joinLineAction), keyEquivalent: "")) 49 | items.append(NSMenuItem.init(title: "Move To EOL and Insert LF", action: #selector(MarvinPlugin.moveToEOLAndInsertLFAction), keyEquivalent: "")) 50 | items.append(NSMenuItem.init(title: "Select Current Word", action: #selector(MarvinPlugin.selectWordAction), keyEquivalent: "")) 51 | items.append(NSMenuItem.init(title: "Select Line Contents", action: #selector(MarvinPlugin.selectLineContentsAction), keyEquivalent: "")) 52 | items.append(NSMenuItem.init(title: "Select Next Word", action: #selector(MarvinPlugin.selectNextWordAction), keyEquivalent: "")) 53 | items.append(NSMenuItem.init(title: "Select Previous Word", action: #selector(MarvinPlugin.selectPreviousWordAction), keyEquivalent: "")) 54 | items.append(NSMenuItem.init(title: "Select Word Above", action: #selector(MarvinPlugin.selectWordAboveAction), keyEquivalent: "")) 55 | items.append(NSMenuItem.init(title: "Select Word Below", action: #selector(MarvinPlugin.selectWordBelowAction), keyEquivalent: "")) 56 | items.append(NSMenuItem.init(title: "Sort Lines", action: #selector(MarvinPlugin.sortLines), keyEquivalent: "")) 57 | 58 | items.forEach { $0.target = self; marvinMenu.addItem($0) } 59 | 60 | if let infoDictionary = Bundle(for: type(of: self)).infoDictionary, 61 | let version = infoDictionary["CFBundleVersion"] { 62 | let marvinMenuItem = NSMenuItem.init(title: "Marvin \(version)", action: nil, keyEquivalent: "") 63 | marvinMenuItem.submenu = marvinMenu 64 | 65 | submenu.addItem(NSMenuItem.separator()) 66 | submenu.addItem(marvinMenuItem) 67 | } 68 | } 69 | } 70 | 71 | func validResponder() -> Bool { 72 | guard let window = NSApp.keyWindow else { return false } 73 | let firstResponder = window.firstResponder 74 | let responderClass = NSStringFromClass(type(of: firstResponder)) 75 | 76 | return ["NSKVONotifying_DVTSourceTextView", "NSKVONotifying_IDEPlaygroundTextView"].contains(responderClass) && xcode.documentLength() > 1 77 | } 78 | 79 | func settingsMenuItemSelected() { 80 | marvinPlugin?.settingsController?.openWindow() 81 | } 82 | 83 | func selectLineContentsAction() { 84 | guard validResponder() else { return } 85 | xcode.selectedRange = xcode.lineContentsRange() 86 | } 87 | 88 | func selectWordAction() { 89 | guard validResponder() else { return } 90 | xcode.selectedRange = xcode.currentWordRange() 91 | } 92 | 93 | func selectWordAboveAction() { 94 | guard validResponder() && xcode.selectedRange.location > 0 else { return } 95 | 96 | let validSet = CharacterSet(charactersIn: "0123456789ABCDEFGHIJKOLMNOPQRSTUVWXYZÅÄÆÖØabcdefghijkolmnopqrstuvwxyzåäæöø_") 97 | var currentRange = xcode.selectedRange 98 | 99 | if currentRange.location >= xcode.contents().characters.count { 100 | currentRange.location -= 1 101 | } 102 | 103 | let characterAtCursorStart: Character = xcode.contents()[xcode.contents().characters.index(xcode.contents().startIndex, offsetBy: currentRange.location)] 104 | let characterAtCursorEnd: Character = xcode.contents()[xcode.contents().characters.index(xcode.contents().startIndex, offsetBy: currentRange.location-1)] 105 | 106 | if xcode.selectedRange.length == 0 && isChar(characterAtCursorStart, inSet: validSet) { 107 | selectWordAction() 108 | } else if xcode.selectedRange.length == 0 && isChar(characterAtCursorEnd, inSet: validSet) { 109 | selectPreviousWordAction() 110 | } else { 111 | perform(keyboardEvent: 126) 112 | 113 | let delayTime = DispatchTime.now() + 0.025 114 | DispatchQueue.main.asyncAfter(deadline: delayTime) { [unowned self] in 115 | let currentRange = self.xcode.selectedRange 116 | 117 | let characterAtCursorStart: Character = self.xcode.contents()[self.xcode.contents().characters.index(self.xcode.contents().startIndex, offsetBy: currentRange.location)] 118 | 119 | if self.isChar(characterAtCursorStart, inSet: validSet) { 120 | self.selectWordAction() 121 | } else { 122 | self.selectPreviousWordAction() 123 | } 124 | } 125 | } 126 | } 127 | 128 | func selectWordBelowAction() { 129 | guard validResponder() else { return } 130 | 131 | perform(keyboardEvent: 125) 132 | 133 | let delayTime = DispatchTime.now() + 0.025 134 | DispatchQueue.main.asyncAfter(deadline: delayTime) { [unowned self] in 135 | self.selectWordAction() 136 | } 137 | } 138 | 139 | func selectPreviousWordAction() { 140 | guard validResponder() else { return } 141 | xcode.selectedRange = xcode.previousWordRange() 142 | xcode.selectedRange = xcode.currentWordRange() 143 | } 144 | 145 | func selectNextWordAction() { 146 | guard validResponder() else { return } 147 | selectWordAction() 148 | } 149 | 150 | func deleteLineAction() { 151 | guard validResponder() else { return } 152 | xcode.replaceCharactersInRange(xcode.lineRange(), withString: "") 153 | } 154 | 155 | func duplicateLineAction() { 156 | guard validResponder() else { return } 157 | 158 | let range = xcode.lineRange() 159 | var string = self.xcode.contentsOfRange(range) 160 | let duplicateRange = NSMakeRange(range.location+range.length, 0) 161 | var offset = 0 162 | 163 | if duplicateRange.location >= xcode.contents().characters.count && 164 | xcode.selectedRange.location == xcode.contents().characters.count { 165 | string = "\n\(string)" 166 | offset = 1 167 | } 168 | 169 | xcode.replaceCharactersInRange(duplicateRange, withString: string) 170 | xcode.selectedRange = NSMakeRange(duplicateRange.location + duplicateRange.length + string.characters.count - 1 + offset, 0) 171 | } 172 | 173 | func joinLineAction() { 174 | guard validResponder() else { return } 175 | 176 | let range = xcode.joinRange() 177 | 178 | guard range.location != NSNotFound && 179 | range.location + range.length < xcode.contents().characters.count 180 | else { return } 181 | 182 | xcode.replaceCharactersInRange(range, 183 | withString: xcode.lineContentsRange().length > 0 ? " " : "") 184 | } 185 | 186 | func moveToEOLAndInsertLFAction() { 187 | guard validResponder() else { return } 188 | 189 | let lineRange = xcode.lineRange() 190 | let endOfLine = lineRange.location + lineRange.length - 1 191 | let currentLine = (xcode.contents() as NSString).substring(with: lineRange) 192 | let trimmedString = currentLine.trimmingCharacters(in: CharacterSet.whitespaces) 193 | let spacing = currentLine.replacingOccurrences(of: trimmedString, with: "") 194 | 195 | xcode.replaceCharactersInRange(NSMakeRange(endOfLine, 0), withString: "\n\(spacing)") 196 | xcode.selectedRange = NSMakeRange(endOfLine + spacing.characters.count + 1, 0) 197 | } 198 | 199 | func sortLines() { 200 | guard validResponder() else { return } 201 | 202 | var lineRange = xcode.lineRange() 203 | lineRange.length -= 1 204 | let selectedContent = xcode.contentsOfRange(lineRange) 205 | let lines = selectedContent.components(separatedBy: "\n") 206 | 207 | var sortedLines = lines.sorted { $0 > $1 } 208 | var sortedLinesString = (sortedLines as NSArray).componentsJoined(by: "\n") 209 | 210 | let shouldSortDescending = (selectedContent as NSString).trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) == (sortedLinesString as NSString).substring(from: 1).trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) 211 | 212 | if shouldSortDescending { 213 | sortedLines = lines.sorted { $0 < $1 } 214 | sortedLinesString = (sortedLines as NSArray).componentsJoined(by: "\n") 215 | } 216 | 217 | xcode.replaceCharactersInRange(lineRange, withString: sortedLinesString) 218 | xcode.selectedRange = lineRange 219 | } 220 | 221 | func properSave() { 222 | removeTrailingWhitespace { [unowned self] in 223 | self.addNewlineAtEOF() 224 | self.xcode.save() 225 | } 226 | } 227 | 228 | // MARK: - Private methods 229 | 230 | fileprivate func addNewlineAtEOF() { 231 | guard validResponder() else { return } 232 | 233 | if let eof = xcode.contents().characters.last 234 | , eof != "\n" { 235 | let selectedRange = xcode.selectedRange 236 | let replaceRange = NSMakeRange(xcode.contents().characters.count, 0) 237 | let replaceString = "\n" 238 | 239 | xcode.replaceCharactersInRange(replaceRange, withString: replaceString) 240 | xcode.selectedRange = selectedRange 241 | } 242 | } 243 | 244 | fileprivate func removeTrailingWhitespace(_ closure: @escaping () -> Void) { 245 | guard validResponder() else { closure(); return } 246 | 247 | let key = "MarvinRemoveWhitespace" 248 | let shouldRemoveWhitespace = UserDefaults.standard.bool(forKey: key) 249 | 250 | if !shouldRemoveWhitespace { 251 | closure() 252 | return 253 | } 254 | 255 | let regex = try! NSRegularExpression(pattern: "([ \t]+)\r?\n", options: .caseInsensitive) 256 | let currentRange = xcode.selectedRange 257 | let string = xcode.contents() 258 | 259 | var results = [NSTextCheckingResult]() 260 | 261 | regex.enumerateMatches(in: string, options: .reportProgress, range: NSMakeRange(0, string.characters.count)) { (result, flags, stop) -> Void in 262 | if let result = result , !NSLocationInRange(currentRange.location, result.range) { 263 | results.append(result) 264 | } 265 | } 266 | 267 | if results.isEmpty { closure(); return } 268 | 269 | let enumerator = results.reversed() 270 | 271 | DispatchQueue.main.async { [unowned self] in 272 | enumerator.forEach { textResult in 273 | var range = textResult.range 274 | range.length -= 1 275 | self.xcode.replaceCharactersInRange(range, withString: "") 276 | } 277 | closure() 278 | } 279 | } 280 | 281 | fileprivate func perform(keyboardEvent virtualKey: CGKeyCode) { 282 | let event = CGEvent(keyboardEventSource: nil, virtualKey: virtualKey, keyDown: true) 283 | event?.flags = CGEventFlags(rawValue: 0) 284 | event?.post(tap: .cghidEventTap) 285 | } 286 | 287 | fileprivate func isChar(_ char: Character, inSet set: CharacterSet) -> Bool { 288 | var found = false 289 | for ch in String(char).utf16 { 290 | if set.contains(UnicodeScalar(ch)!) { found = true; break } 291 | } 292 | return found 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /MarvinPlugIn/Source/MarvinPlugIn/MarvinSettingsWindowControllerSwift.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class MarvinSettingsWindowController: NSWindowController { 4 | 5 | var bundle: Bundle? 6 | @IBOutlet weak var shouldRemoveWhitespace: NSButton? 7 | 8 | convenience init(bundle: Bundle) { 9 | self.init(window: nil) 10 | self.bundle = bundle 11 | } 12 | 13 | func openWindow() { 14 | guard let bundle = bundle else { return } 15 | bundle.loadNibNamed("Settings", owner: self, topLevelObjects: nil) 16 | showWindow(self) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /MarvinPlugIn/Source/MarvinPlugIn/XcodeManager.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class XcodeManager: NSObject { 4 | 5 | var textView: NSTextView? { 6 | get { 7 | if let currentEditor = LegacyXcodeManager().currentEditor(), 8 | let className = NSClassFromString("IDESourceCodeEditor") 9 | , (currentEditor as AnyObject).isKind(of: className) { 10 | return (currentEditor as AnyObject).textView 11 | } 12 | 13 | if let currentEditor = LegacyXcodeManager().currentEditor(), 14 | let className = NSClassFromString("IDESourceCodeComparisonEditor") 15 | , (currentEditor as AnyObject).isKind(of: className) { 16 | return (currentEditor as AnyObject).keyTextView 17 | } 18 | 19 | return nil 20 | } 21 | } 22 | 23 | var selectedRange: NSRange { 24 | set(value) { 25 | guard value.location != NSNotFound else { return } 26 | 27 | if value.location + value.length > self.contents().characters.count { 28 | var value = value 29 | value.length = self.contents().characters.count - value.location 30 | } 31 | 32 | textView?.selectedRange = value 33 | } 34 | get { 35 | return textView?.selectedRange ?? NSRange(location: 0, length: 0) 36 | } 37 | } 38 | 39 | func save() { 40 | LegacyXcodeManager().save() 41 | } 42 | func needsDisplay() { 43 | textView?.needsDisplay = true 44 | } 45 | 46 | func contents() -> String { 47 | return textView?.string ?? "" 48 | } 49 | 50 | func documentLength() -> Int { 51 | return (textView?.string?.characters.count ?? 0) - 1 52 | } 53 | 54 | func currentWordRange() -> NSRange { 55 | let validSet = CharacterSet(charactersIn: "0123456789ABCDEFGHIJKOLMNOPQRSTUVWXYZÅÄÆÖØabcdefghijkolmnopqrstuvwxyzåäæöø_") 56 | let spaceSet = CharacterSet(charactersIn: "#-<>/(){}[],;:. \n`*\"' ") 57 | var selectedRange = self.selectedRange 58 | 59 | guard selectedRange.location + selectedRange.length < contents().characters.count else { return selectedRange } 60 | 61 | var character: Character 62 | if self.hasSelection() { 63 | character = self.contents()[self.contents().characters.index(self.contents().startIndex, offsetBy: selectedRange.location+selectedRange.length)] 64 | } else { 65 | character = self.contents()[self.contents().characters.index(self.contents().startIndex, offsetBy: selectedRange.location)] 66 | } 67 | 68 | if !isChar(character, inSet:validSet) { 69 | selectedRange.location = selectedRange.location + selectedRange.length 70 | } 71 | 72 | let scanner = Scanner(string: self.contents()) 73 | scanner.scanLocation = selectedRange.location 74 | 75 | var length = selectedRange.location 76 | 77 | while !scanner.isAtEnd { 78 | if scanner.scanCharacters(from: validSet, into: nil) { 79 | length = scanner.scanLocation 80 | break 81 | } 82 | 83 | scanner.scanLocation = scanner.scanLocation + 1 84 | } 85 | 86 | let whitespaceRange = (self.contents() as NSString).rangeOfCharacter(from: spaceSet, 87 | options: .backwards, 88 | range: NSRange(location: 0, length: length)) 89 | 90 | let location = whitespaceRange.location != NSNotFound ? whitespaceRange.location + 1 : 0 91 | 92 | if length - location > self.documentLength() { 93 | length = 0 94 | } 95 | 96 | var range = NSRange(location: 0, length: 0) 97 | if location >= 0 { 98 | range = NSRange(location: location, length: length - location) 99 | return range 100 | } else if location == 0 && range.location != selectedRange.location && range.length != selectedRange.length { 101 | scanner.scanLocation = 0 102 | while !scanner.isAtEnd { 103 | if scanner.scanCharacters(from: validSet, into: nil) { 104 | length = scanner.scanLocation 105 | break 106 | } 107 | scanner.scanLocation = scanner.scanLocation + 1 108 | 109 | range.location = location 110 | range.length = length - location 111 | } 112 | 113 | if range.location == NSNotFound { range.location = 0 } 114 | 115 | if range.length > self.contents().characters.count { 116 | range.length = self.contents().characters.count 117 | } 118 | 119 | return range 120 | } 121 | 122 | return selectedRange 123 | } 124 | 125 | func previousWordRange() -> NSRange { 126 | let selectedRange = self.selectedRange 127 | let validSet = CharacterSet(charactersIn: "0123456789ABCDEFGHIJKOLMNOPQRSTUVWXYZÅÄÆÖØabcdefghijkolmnopqrstuvwxyzåäæöø_") 128 | var location = (self.contents() as NSString).rangeOfCharacter(from: validSet, options: .backwards, range: NSMakeRange(0,selectedRange.location)).location 129 | 130 | if location == NSNotFound { 131 | location = 0 132 | } 133 | 134 | return NSRange(location: location, length: 0) 135 | } 136 | 137 | func lineContentsRange() -> NSRange { 138 | let lineRange = self.lineRange() 139 | let currentLine = (self.contents() as NSString).substring(with: lineRange) 140 | let trimmedString = currentLine.trimmingCharacters(in: CharacterSet.whitespaces) 141 | let spacing = currentLine.replacingOccurrences(of: trimmedString, with: "") 142 | 143 | return NSRange(location: lineRange.location + spacing.characters.count, length: lineRange.length - spacing.characters.count - 1) 144 | } 145 | 146 | func lineRange() -> NSRange { 147 | var selectedRange = self.selectedRange 148 | 149 | if selectedRange.location == self.contents().characters.count { 150 | selectedRange.location -= 1 151 | } 152 | 153 | let newLineSet = CharacterSet(charactersIn: "\n") 154 | 155 | var location = (self.contents() as NSString).rangeOfCharacter(from: newLineSet, options: .backwards, range: NSRange(location: 0, length: selectedRange.location)).location 156 | var length = (self.contents() as NSString) 157 | .rangeOfCharacter(from: newLineSet, 158 | options: .caseInsensitive, 159 | range: NSRange(location: selectedRange.location + selectedRange.length, 160 | length: self.contents().characters.count - (selectedRange.location + selectedRange.length))) 161 | .location 162 | 163 | if length == NSNotFound { 164 | length = self.contents().characters.count - location 165 | return NSRange(location: location, length: length) 166 | } 167 | 168 | location = location == NSNotFound ? 0 : location + 1 169 | length = location == 0 ? length + 1 : length + 1 - location 170 | 171 | if length > self.contents().characters.count { 172 | length = self.contents().characters.count - location 173 | } 174 | 175 | return NSRange(location: location, length: length) 176 | } 177 | 178 | func contentsOfRange(_ range: NSRange) -> String { 179 | guard let textView = self.textView, let contents = textView.string else { return "" } 180 | return (contents as NSString).substring(with: range) 181 | } 182 | 183 | func joinRange() -> NSRange { 184 | let lineRange = self.lineRange() 185 | let joinRange = NSRange(location: lineRange.location + lineRange.length - 1, length: 0) 186 | let validSet = CharacterSet(charactersIn: "0123456789ABCDEFGHIJKOLMNOPQRSTUVWXYZÅÄÆÖØabcdefghijkolmnopqrstuvwxyzåäæöø_{}().$[]") 187 | let length = (self.contents() as NSString).rangeOfCharacter(from: validSet, options: .caseInsensitive, range: NSRange(location: joinRange.location, length: self.contents().characters.count - joinRange.location)).location 188 | 189 | return NSRange(location: joinRange.location, length: length - joinRange.location) 190 | } 191 | 192 | func selectedText() -> String { 193 | guard let textView = self.textView else { return "" } 194 | return contentsOfRange(textView.selectedRange) 195 | } 196 | 197 | func hasSelection() -> Bool { 198 | return self.textView?.selectedRange.length ?? 0 > 0 199 | } 200 | 201 | func emptySelection() -> Bool { 202 | return self.hasSelection() == false 203 | } 204 | 205 | func layoutManager() -> NSLayoutManager? { 206 | return self.textView?.layoutManager 207 | } 208 | 209 | func insertText(_ string: String) { 210 | self.textView?.insertText(string) 211 | 212 | let delayTime = DispatchTime.now() + 0.025 213 | DispatchQueue.main.asyncAfter(deadline: delayTime) { 214 | NotificationCenter.default.post(name: Notification.Name(rawValue: "Add change mark"), object: string) 215 | } 216 | } 217 | 218 | func replaceCharactersInRange(_ range: NSRange, withString string: String) { 219 | if range.location + range.length > self.contents().characters.count { 220 | var range = range 221 | range.length = self.contents().characters.count - range.location 222 | } 223 | 224 | let document = LegacyXcodeManager().currentSourceCodeDocument() 225 | let textStorage = document?.textStorage() 226 | 227 | textStorage?.replaceCharacters(in: range, with: string, withUndoManager: document?.undoManager) 228 | 229 | let delayTime = DispatchTime.now() + 0.025 230 | DispatchQueue.main.asyncAfter(deadline: delayTime) { 231 | NotificationCenter.default.post(name: Notification.Name(rawValue: "Add change mark"), object: string) 232 | } 233 | } 234 | 235 | fileprivate func isChar(_ char: Character, inSet set: CharacterSet) -> Bool { 236 | var found = false 237 | for ch in String(char).utf16 { 238 | if set.contains(UnicodeScalar(ch)!) { found = true; break } 239 | } 240 | return found 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /MarvinPlugin.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 524A06551B1F839600F0553A /* Settings.xib in Resources */ = {isa = PBXBuildFile; fileRef = 524A06541B1F839600F0553A /* Settings.xib */; }; 11 | 524A065A1B1F8F1500F0553A /* Defaults.plist in Resources */ = {isa = PBXBuildFile; fileRef = 524A06591B1F8F1500F0553A /* Defaults.plist */; }; 12 | 6F44E9F617DD324B0064F1B7 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6F44E9F517DD324B0064F1B7 /* Cocoa.framework */; }; 13 | BD10ABEB1C25A81400936E1B /* MarvinPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD10ABEA1C25A81400936E1B /* MarvinPlugin.swift */; }; 14 | BD4F5BFF1C258B2A00424FE1 /* NSDocument+Propersave.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4F5BFE1C258B2A00424FE1 /* NSDocument+Propersave.swift */; }; 15 | BD5EBC6519C9F93200F2C8CD /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BD5EBC6119C9F93200F2C8CD /* InfoPlist.strings */; }; 16 | BD632D301C26C8FB00A6132F /* XcodeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD632D2F1C26C8FB00A6132F /* XcodeManager.swift */; }; 17 | BD9A8A3119CA09AD00321368 /* LegacyXcodeManager.m in Sources */ = {isa = PBXBuildFile; fileRef = BD9A8A3019CA09AD00321368 /* LegacyXcodeManager.m */; }; 18 | BDDAB6A91C2727D500C5905F /* MarvinSettingsWindowControllerSwift.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDDAB6A81C2727D500C5905F /* MarvinSettingsWindowControllerSwift.swift */; }; 19 | /* End PBXBuildFile section */ 20 | 21 | /* Begin PBXFileReference section */ 22 | 524A06541B1F839600F0553A /* Settings.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = Settings.xib; path = MarvinPlugin/Source/MarvinPlugin/Settings.xib; sourceTree = SOURCE_ROOT; }; 23 | 524A06591B1F8F1500F0553A /* Defaults.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Defaults.plist; path = MarvinPlugin/Source/MarvinPlugin/Defaults.plist; sourceTree = SOURCE_ROOT; }; 24 | 6F44E9F217DD324B0064F1B7 /* MarvinPlugin.xcplugin */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MarvinPlugin.xcplugin; sourceTree = BUILT_PRODUCTS_DIR; }; 25 | 6F44E9F517DD324B0064F1B7 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; 26 | 6F44E9F817DD324B0064F1B7 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; 27 | 6F44E9F917DD324B0064F1B7 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; }; 28 | 6F44E9FA17DD324B0064F1B7 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 29 | BD10ABEA1C25A81400936E1B /* MarvinPlugin.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = MarvinPlugin.swift; sourceTree = ""; tabWidth = 2; }; 30 | BD4F5BFD1C258B2A00424FE1 /* MarvinPlugin-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MarvinPlugin-Bridging-Header.h"; sourceTree = ""; }; 31 | BD4F5BFE1C258B2A00424FE1 /* NSDocument+Propersave.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.swift; name = "NSDocument+Propersave.swift"; path = "MarvinPlugin/Source/MarvinPlugin/NSDocument+Propersave.swift"; sourceTree = SOURCE_ROOT; tabWidth = 2; }; 32 | BD5351FA19CA1159006D3B78 /* XcodePrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XcodePrivate.h; sourceTree = ""; }; 33 | BD5EBC6219C9F93200F2C8CD /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 34 | BD5EBC6319C9F93200F2C8CD /* MarvinPlugIn-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "MarvinPlugIn-Info.plist"; sourceTree = ""; }; 35 | BD5EBC6419C9F93200F2C8CD /* MarvinPlugIn-Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MarvinPlugIn-Prefix.pch"; sourceTree = ""; }; 36 | BD632D2F1C26C8FB00A6132F /* XcodeManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = XcodeManager.swift; sourceTree = ""; tabWidth = 2; }; 37 | BD9A8A2F19CA09AD00321368 /* LegacyXcodeManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LegacyXcodeManager.h; sourceTree = ""; }; 38 | BD9A8A3019CA09AD00321368 /* LegacyXcodeManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LegacyXcodeManager.m; sourceTree = ""; }; 39 | BDBA8FA61AC9E04C00D6F948 /* DVTFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = DVTFoundation.framework; path = ../../../../Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework; sourceTree = ""; }; 40 | BDBA8FA81AC9E06B00D6F948 /* IDEKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IDEKit.framework; path = ../../../../Applications/Xcode.app/Contents/Frameworks/IDEKit.framework; sourceTree = ""; }; 41 | BDBA8FAA1AC9E08600D6F948 /* DVTKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = DVTKit.framework; path = ../../../../Applications/Xcode.app/Contents/SharedFrameworks/DVTKit.framework; sourceTree = ""; }; 42 | BDDAB6A81C2727D500C5905F /* MarvinSettingsWindowControllerSwift.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = MarvinSettingsWindowControllerSwift.swift; sourceTree = ""; tabWidth = 2; }; 43 | /* End PBXFileReference section */ 44 | 45 | /* Begin PBXFrameworksBuildPhase section */ 46 | 6F44E9EF17DD324B0064F1B7 /* Frameworks */ = { 47 | isa = PBXFrameworksBuildPhase; 48 | buildActionMask = 2147483647; 49 | files = ( 50 | 6F44E9F617DD324B0064F1B7 /* Cocoa.framework in Frameworks */, 51 | ); 52 | runOnlyForDeploymentPostprocessing = 0; 53 | }; 54 | /* End PBXFrameworksBuildPhase section */ 55 | 56 | /* Begin PBXGroup section */ 57 | 6F44E9E917DD324B0064F1B7 = { 58 | isa = PBXGroup; 59 | children = ( 60 | BD5EBC6019C9F93200F2C8CD /* Supporting Files */, 61 | BD5EBC5B19C9F91400F2C8CD /* Source */, 62 | 6F44E9F417DD324B0064F1B7 /* Frameworks */, 63 | 6F44E9F317DD324B0064F1B7 /* Products */, 64 | ); 65 | indentWidth = 4; 66 | sourceTree = ""; 67 | tabWidth = 4; 68 | usesTabs = 0; 69 | }; 70 | 6F44E9F317DD324B0064F1B7 /* Products */ = { 71 | isa = PBXGroup; 72 | children = ( 73 | 6F44E9F217DD324B0064F1B7 /* MarvinPlugin.xcplugin */, 74 | ); 75 | name = Products; 76 | sourceTree = ""; 77 | }; 78 | 6F44E9F417DD324B0064F1B7 /* Frameworks */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | BDBA8FAA1AC9E08600D6F948 /* DVTKit.framework */, 82 | BDBA8FA81AC9E06B00D6F948 /* IDEKit.framework */, 83 | BDBA8FA61AC9E04C00D6F948 /* DVTFoundation.framework */, 84 | 6F44E9F517DD324B0064F1B7 /* Cocoa.framework */, 85 | 6F44E9F717DD324B0064F1B7 /* Other Frameworks */, 86 | ); 87 | name = Frameworks; 88 | sourceTree = ""; 89 | }; 90 | 6F44E9F717DD324B0064F1B7 /* Other Frameworks */ = { 91 | isa = PBXGroup; 92 | children = ( 93 | 6F44E9F817DD324B0064F1B7 /* AppKit.framework */, 94 | 6F44E9F917DD324B0064F1B7 /* CoreData.framework */, 95 | 6F44E9FA17DD324B0064F1B7 /* Foundation.framework */, 96 | ); 97 | name = "Other Frameworks"; 98 | sourceTree = ""; 99 | }; 100 | BD5EBC5B19C9F91400F2C8CD /* Source */ = { 101 | isa = PBXGroup; 102 | children = ( 103 | BD5EBC6719C9F94700F2C8CD /* MarvinPlugin */, 104 | ); 105 | name = Source; 106 | path = MarvinPlugIn/Source; 107 | sourceTree = ""; 108 | }; 109 | BD5EBC6019C9F93200F2C8CD /* Supporting Files */ = { 110 | isa = PBXGroup; 111 | children = ( 112 | BD5EBC6119C9F93200F2C8CD /* InfoPlist.strings */, 113 | BD5EBC6319C9F93200F2C8CD /* MarvinPlugIn-Info.plist */, 114 | BD5EBC6419C9F93200F2C8CD /* MarvinPlugIn-Prefix.pch */, 115 | ); 116 | name = "Supporting Files"; 117 | path = "MarvinPlugIn/Supporting Files"; 118 | sourceTree = ""; 119 | }; 120 | BD5EBC6719C9F94700F2C8CD /* MarvinPlugin */ = { 121 | isa = PBXGroup; 122 | children = ( 123 | 524A06591B1F8F1500F0553A /* Defaults.plist */, 124 | 524A06541B1F839600F0553A /* Settings.xib */, 125 | BD9A8A2F19CA09AD00321368 /* LegacyXcodeManager.h */, 126 | BD9A8A3019CA09AD00321368 /* LegacyXcodeManager.m */, 127 | BD5351FA19CA1159006D3B78 /* XcodePrivate.h */, 128 | BD4F5BFE1C258B2A00424FE1 /* NSDocument+Propersave.swift */, 129 | BD4F5BFD1C258B2A00424FE1 /* MarvinPlugin-Bridging-Header.h */, 130 | BD10ABEA1C25A81400936E1B /* MarvinPlugin.swift */, 131 | BD632D2F1C26C8FB00A6132F /* XcodeManager.swift */, 132 | BDDAB6A81C2727D500C5905F /* MarvinSettingsWindowControllerSwift.swift */, 133 | ); 134 | name = MarvinPlugin; 135 | path = MarvinPlugIn; 136 | sourceTree = ""; 137 | }; 138 | /* End PBXGroup section */ 139 | 140 | /* Begin PBXNativeTarget section */ 141 | 6F44E9F117DD324B0064F1B7 /* MarvinPlugin */ = { 142 | isa = PBXNativeTarget; 143 | buildConfigurationList = 6F44EA0417DD324B0064F1B7 /* Build configuration list for PBXNativeTarget "MarvinPlugin" */; 144 | buildPhases = ( 145 | 6F44E9EE17DD324B0064F1B7 /* Sources */, 146 | 6F44E9EF17DD324B0064F1B7 /* Frameworks */, 147 | 6F44E9F017DD324B0064F1B7 /* Resources */, 148 | ); 149 | buildRules = ( 150 | ); 151 | dependencies = ( 152 | ); 153 | name = MarvinPlugin; 154 | productName = XcodePlusDeleteLines; 155 | productReference = 6F44E9F217DD324B0064F1B7 /* MarvinPlugin.xcplugin */; 156 | productType = "com.apple.product-type.bundle"; 157 | }; 158 | /* End PBXNativeTarget section */ 159 | 160 | /* Begin PBXProject section */ 161 | 6F44E9EA17DD324B0064F1B7 /* Project object */ = { 162 | isa = PBXProject; 163 | attributes = { 164 | LastSwiftUpdateCheck = 0720; 165 | LastUpgradeCheck = 0800; 166 | ORGANIZATIONNAME = "Octalord Information Inc."; 167 | TargetAttributes = { 168 | 6F44E9F117DD324B0064F1B7 = { 169 | LastSwiftMigration = 0800; 170 | }; 171 | }; 172 | }; 173 | buildConfigurationList = 6F44E9ED17DD324B0064F1B7 /* Build configuration list for PBXProject "MarvinPlugin" */; 174 | compatibilityVersion = "Xcode 3.2"; 175 | developmentRegion = English; 176 | hasScannedForEncodings = 0; 177 | knownRegions = ( 178 | en, 179 | ); 180 | mainGroup = 6F44E9E917DD324B0064F1B7; 181 | productRefGroup = 6F44E9F317DD324B0064F1B7 /* Products */; 182 | projectDirPath = ""; 183 | projectRoot = ""; 184 | targets = ( 185 | 6F44E9F117DD324B0064F1B7 /* MarvinPlugin */, 186 | ); 187 | }; 188 | /* End PBXProject section */ 189 | 190 | /* Begin PBXResourcesBuildPhase section */ 191 | 6F44E9F017DD324B0064F1B7 /* Resources */ = { 192 | isa = PBXResourcesBuildPhase; 193 | buildActionMask = 2147483647; 194 | files = ( 195 | 524A06551B1F839600F0553A /* Settings.xib in Resources */, 196 | BD5EBC6519C9F93200F2C8CD /* InfoPlist.strings in Resources */, 197 | 524A065A1B1F8F1500F0553A /* Defaults.plist in Resources */, 198 | ); 199 | runOnlyForDeploymentPostprocessing = 0; 200 | }; 201 | /* End PBXResourcesBuildPhase section */ 202 | 203 | /* Begin PBXSourcesBuildPhase section */ 204 | 6F44E9EE17DD324B0064F1B7 /* Sources */ = { 205 | isa = PBXSourcesBuildPhase; 206 | buildActionMask = 2147483647; 207 | files = ( 208 | BD632D301C26C8FB00A6132F /* XcodeManager.swift in Sources */, 209 | BD10ABEB1C25A81400936E1B /* MarvinPlugin.swift in Sources */, 210 | BD4F5BFF1C258B2A00424FE1 /* NSDocument+Propersave.swift in Sources */, 211 | BD9A8A3119CA09AD00321368 /* LegacyXcodeManager.m in Sources */, 212 | BDDAB6A91C2727D500C5905F /* MarvinSettingsWindowControllerSwift.swift in Sources */, 213 | ); 214 | runOnlyForDeploymentPostprocessing = 0; 215 | }; 216 | /* End PBXSourcesBuildPhase section */ 217 | 218 | /* Begin PBXVariantGroup section */ 219 | BD5EBC6119C9F93200F2C8CD /* InfoPlist.strings */ = { 220 | isa = PBXVariantGroup; 221 | children = ( 222 | BD5EBC6219C9F93200F2C8CD /* en */, 223 | ); 224 | name = InfoPlist.strings; 225 | sourceTree = ""; 226 | }; 227 | /* End PBXVariantGroup section */ 228 | 229 | /* Begin XCBuildConfiguration section */ 230 | 6F44EA0217DD324B0064F1B7 /* Debug */ = { 231 | isa = XCBuildConfiguration; 232 | buildSettings = { 233 | ALWAYS_SEARCH_USER_PATHS = NO; 234 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 235 | CLANG_CXX_LIBRARY = "libc++"; 236 | CLANG_WARN_BOOL_CONVERSION = YES; 237 | CLANG_WARN_CONSTANT_CONVERSION = YES; 238 | CLANG_WARN_EMPTY_BODY = YES; 239 | CLANG_WARN_ENUM_CONVERSION = YES; 240 | CLANG_WARN_INFINITE_RECURSION = YES; 241 | CLANG_WARN_INT_CONVERSION = YES; 242 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 243 | CLANG_WARN_UNREACHABLE_CODE = YES; 244 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 245 | COPY_PHASE_STRIP = NO; 246 | ENABLE_STRICT_OBJC_MSGSEND = YES; 247 | ENABLE_TESTABILITY = YES; 248 | GCC_C_LANGUAGE_STANDARD = gnu99; 249 | GCC_DYNAMIC_NO_PIC = NO; 250 | GCC_ENABLE_OBJC_EXCEPTIONS = YES; 251 | GCC_NO_COMMON_BLOCKS = YES; 252 | GCC_OPTIMIZATION_LEVEL = 0; 253 | GCC_PREPROCESSOR_DEFINITIONS = ( 254 | "DEBUG=1", 255 | "$(inherited)", 256 | ); 257 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 258 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 259 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 260 | GCC_WARN_UNDECLARED_SELECTOR = YES; 261 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 262 | GCC_WARN_UNUSED_FUNCTION = YES; 263 | GCC_WARN_UNUSED_VARIABLE = YES; 264 | MACOSX_DEPLOYMENT_TARGET = 10.9; 265 | ONLY_ACTIVE_ARCH = YES; 266 | SDKROOT = macosx; 267 | }; 268 | name = Debug; 269 | }; 270 | 6F44EA0317DD324B0064F1B7 /* Release */ = { 271 | isa = XCBuildConfiguration; 272 | buildSettings = { 273 | ALWAYS_SEARCH_USER_PATHS = NO; 274 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 275 | CLANG_CXX_LIBRARY = "libc++"; 276 | CLANG_WARN_BOOL_CONVERSION = YES; 277 | CLANG_WARN_CONSTANT_CONVERSION = YES; 278 | CLANG_WARN_EMPTY_BODY = YES; 279 | CLANG_WARN_ENUM_CONVERSION = YES; 280 | CLANG_WARN_INFINITE_RECURSION = YES; 281 | CLANG_WARN_INT_CONVERSION = YES; 282 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 283 | CLANG_WARN_UNREACHABLE_CODE = YES; 284 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 285 | COPY_PHASE_STRIP = YES; 286 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 287 | ENABLE_STRICT_OBJC_MSGSEND = YES; 288 | GCC_C_LANGUAGE_STANDARD = gnu99; 289 | GCC_ENABLE_OBJC_EXCEPTIONS = YES; 290 | GCC_NO_COMMON_BLOCKS = YES; 291 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 292 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 293 | GCC_WARN_UNDECLARED_SELECTOR = YES; 294 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 295 | GCC_WARN_UNUSED_FUNCTION = YES; 296 | GCC_WARN_UNUSED_VARIABLE = YES; 297 | MACOSX_DEPLOYMENT_TARGET = 10.9; 298 | SDKROOT = macosx; 299 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 300 | }; 301 | name = Release; 302 | }; 303 | 6F44EA0517DD324B0064F1B7 /* Debug */ = { 304 | isa = XCBuildConfiguration; 305 | buildSettings = { 306 | CLANG_ENABLE_MODULES = YES; 307 | CLANG_ENABLE_OBJC_ARC = YES; 308 | COMBINE_HIDPI_IMAGES = YES; 309 | CURRENT_PROJECT_VERSION = 1.5.4; 310 | DEPLOYMENT_LOCATION = YES; 311 | DSTROOT = "${HOME}"; 312 | FRAMEWORK_SEARCH_PATHS = ( 313 | "$(inherited)", 314 | "$(SYSTEM_APPS_DIR)/Xcode.app/Contents/SharedFrameworks", 315 | "$(SYSTEM_APPS_DIR)/Xcode.app/Contents/Frameworks", 316 | ); 317 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 318 | GCC_PREFIX_HEADER = "MarvinPlugIn/Supporting Files/MarvinPlugIn-Prefix.pch"; 319 | INFOPLIST_FILE = "MarvinPlugIn/Supporting Files/MarvinPlugIn-Info.plist"; 320 | INSTALL_PATH = "/Library/Application Support/Developer/Shared/Xcode/Plug-ins"; 321 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks $(DT_TOOLCHAIN_DIR)/usr/lib/swift/macosx"; 322 | PRODUCT_BUNDLE_IDENTIFIER = "com.zenangst.${PRODUCT_NAME:rfc1034identifier}"; 323 | PRODUCT_NAME = MarvinPlugin; 324 | SWIFT_OBJC_BRIDGING_HEADER = "MarvinPlugIn/Source/MarvinPlugIn/MarvinPlugin-Bridging-Header.h"; 325 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 326 | SWIFT_VERSION = 3.0; 327 | VERSIONING_SYSTEM = "apple-generic"; 328 | WRAPPER_EXTENSION = xcplugin; 329 | }; 330 | name = Debug; 331 | }; 332 | 6F44EA0617DD324B0064F1B7 /* Release */ = { 333 | isa = XCBuildConfiguration; 334 | buildSettings = { 335 | CLANG_ENABLE_MODULES = YES; 336 | CLANG_ENABLE_OBJC_ARC = YES; 337 | COMBINE_HIDPI_IMAGES = YES; 338 | CURRENT_PROJECT_VERSION = 1.5.4; 339 | DEPLOYMENT_LOCATION = YES; 340 | DSTROOT = "${HOME}"; 341 | FRAMEWORK_SEARCH_PATHS = ( 342 | "$(inherited)", 343 | "$(SYSTEM_APPS_DIR)/Xcode.app/Contents/SharedFrameworks", 344 | "$(SYSTEM_APPS_DIR)/Xcode.app/Contents/Frameworks", 345 | ); 346 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 347 | GCC_PREFIX_HEADER = "MarvinPlugIn/Supporting Files/MarvinPlugIn-Prefix.pch"; 348 | INFOPLIST_FILE = "MarvinPlugIn/Supporting Files/MarvinPlugIn-Info.plist"; 349 | INSTALL_PATH = "/Library/Application Support/Developer/Shared/Xcode/Plug-ins"; 350 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks $(DT_TOOLCHAIN_DIR)/usr/lib/swift/macosx"; 351 | PRODUCT_BUNDLE_IDENTIFIER = "com.zenangst.${PRODUCT_NAME:rfc1034identifier}"; 352 | PRODUCT_NAME = MarvinPlugin; 353 | SWIFT_OBJC_BRIDGING_HEADER = "MarvinPlugIn/Source/MarvinPlugIn/MarvinPlugin-Bridging-Header.h"; 354 | SWIFT_VERSION = 3.0; 355 | VERSIONING_SYSTEM = "apple-generic"; 356 | WRAPPER_EXTENSION = xcplugin; 357 | }; 358 | name = Release; 359 | }; 360 | /* End XCBuildConfiguration section */ 361 | 362 | /* Begin XCConfigurationList section */ 363 | 6F44E9ED17DD324B0064F1B7 /* Build configuration list for PBXProject "MarvinPlugin" */ = { 364 | isa = XCConfigurationList; 365 | buildConfigurations = ( 366 | 6F44EA0217DD324B0064F1B7 /* Debug */, 367 | 6F44EA0317DD324B0064F1B7 /* Release */, 368 | ); 369 | defaultConfigurationIsVisible = 0; 370 | defaultConfigurationName = Release; 371 | }; 372 | 6F44EA0417DD324B0064F1B7 /* Build configuration list for PBXNativeTarget "MarvinPlugin" */ = { 373 | isa = XCConfigurationList; 374 | buildConfigurations = ( 375 | 6F44EA0517DD324B0064F1B7 /* Debug */, 376 | 6F44EA0617DD324B0064F1B7 /* Release */, 377 | ); 378 | defaultConfigurationIsVisible = 0; 379 | defaultConfigurationName = Release; 380 | }; 381 | /* End XCConfigurationList section */ 382 | }; 383 | rootObject = 6F44E9EA17DD324B0064F1B7 /* Project object */; 384 | } 385 | -------------------------------------------------------------------------------- /MarvinPlugin/Source/MarvinPlugin/Defaults.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MarvinRemoveWhitespace 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MarvinPlugin/Source/MarvinPlugin/NSDocument+Propersave.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Cocoa 3 | 4 | final class SaveSwizzler { 5 | 6 | fileprivate static var swizzled = false 7 | 8 | fileprivate init() { 9 | fatalError() 10 | } 11 | 12 | class func swizzle() { 13 | if swizzled { return } 14 | swizzled = true 15 | 16 | var original, swizzle: Method 17 | 18 | original = class_getInstanceMethod(NSDocument.self, #selector(NSDocument.save(withDelegate:didSave:contextInfo:))) 19 | swizzle = class_getInstanceMethod(NSDocument.self, #selector(NSDocument.zen_saveDocumentWithDelegate(_:didSaveSelector:contextInfo:))) 20 | 21 | method_exchangeImplementations(original, swizzle) 22 | } 23 | } 24 | 25 | extension NSDocument { 26 | 27 | dynamic func zen_saveDocumentWithDelegate(_ delegate: AnyObject?, didSaveSelector: Selector, contextInfo: UnsafeMutableRawPointer) { 28 | if shouldFormat() { 29 | NotificationCenter.default.post(name: Notification.Name(rawValue: "Save properly"), object: nil) 30 | } 31 | 32 | let delayTime = DispatchTime.now() + 0.25 33 | DispatchQueue.main.asyncAfter(deadline: delayTime) { 34 | self.zen_saveDocumentWithDelegate(delegate, didSaveSelector: didSaveSelector, contextInfo: contextInfo) 35 | } 36 | } 37 | 38 | func shouldFormat() -> Bool { 39 | guard let fileURL = fileURL 40 | else { return false } 41 | 42 | let pathExtension = fileURL.pathExtension 43 | 44 | return [ 45 | "", 46 | "c", 47 | "cc", 48 | "cpp", 49 | "h", 50 | "hpp", 51 | "ipp", 52 | "m", "mm", 53 | "plist", 54 | "rb", 55 | "strings", 56 | "swift", 57 | "playground", 58 | "md", 59 | "yml" 60 | ] 61 | .contains(pathExtension.lowercased()) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /MarvinPlugin/Source/MarvinPlugin/Settings.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /MarvinPlugin/Source/MarvinPlugin/XcodePrivate.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Benoît on 11/01/14. 3 | // Copyright (c) 2014 Pragmatic Code. All rights reserved. 4 | // 5 | 6 | #import 7 | 8 | @interface DVTTextDocumentLocation : NSObject 9 | @property (readonly) NSRange characterRange; 10 | @property (readonly) NSRange lineRange; 11 | @end 12 | 13 | @interface DVTTextPreferences : NSObject 14 | + (id)preferences; 15 | @property BOOL trimWhitespaceOnlyLines; 16 | @property BOOL trimTrailingWhitespace; 17 | @property BOOL useSyntaxAwareIndenting; 18 | @end 19 | 20 | @interface DVTSourceTextStorage : NSTextStorage 21 | - (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)string withUndoManager:(id)undoManager; 22 | - (NSRange)lineRangeForCharacterRange:(NSRange)range; 23 | - (NSRange)characterRangeForLineRange:(NSRange)range; 24 | - (void)indentCharacterRange:(NSRange)range undoManager:(id)undoManager; 25 | @end 26 | 27 | @interface DVTFileDataType : NSObject 28 | @property (readonly) NSString *identifier; 29 | @end 30 | 31 | @interface DVTFilePath : NSObject 32 | @property (readonly) NSURL *fileURL; 33 | @property (readonly) DVTFileDataType *fileDataTypePresumed; 34 | @end 35 | 36 | @interface IDEContainerItem : NSObject 37 | @property (readonly) DVTFilePath *resolvedFilePath; 38 | @end 39 | 40 | @interface IDEGroup : IDEContainerItem 41 | 42 | @end 43 | 44 | @interface IDEFileReference : IDEContainerItem 45 | 46 | @end 47 | 48 | @interface IDENavigableItem : NSObject 49 | @property (readonly) IDENavigableItem *parentItem; 50 | @property (readonly) NSArray *childItems; 51 | @property (readonly) id representedObject; 52 | @end 53 | 54 | @interface IDEFileNavigableItem : IDENavigableItem 55 | @property (readonly) DVTFileDataType *documentType; 56 | @property (readonly) NSURL *fileURL; 57 | @end 58 | 59 | @interface IDEGroupNavigableItem : IDENavigableItem 60 | @property (readonly) IDEGroup *group; 61 | @end 62 | 63 | @interface IDEStructureNavigator : NSObject 64 | @property (retain) NSArray *selectedObjects; 65 | @end 66 | 67 | @interface IDENavigableItemCoordinator : NSObject 68 | - (id)structureNavigableItemForDocumentURL:(id)arg1 inWorkspace:(id)arg2 error:(id *)arg3; 69 | @end 70 | 71 | @interface IDENavigatorArea : NSObject 72 | - (id)currentNavigator; 73 | @end 74 | 75 | @interface IDEWorkspaceTabController : NSObject 76 | @property (readonly) IDENavigatorArea *navigatorArea; 77 | @end 78 | 79 | @interface IDEDocumentController : NSDocumentController 80 | + (id)editorDocumentForNavigableItem:(id)arg1; 81 | + (id)retainedEditorDocumentForNavigableItem:(id)arg1 error:(id *)arg2; 82 | + (void)releaseEditorDocument:(id)arg1; 83 | @end 84 | 85 | @interface IDEEditorDocument : NSDocument 86 | - (void)ide_saveDocument:(id)arg1; 87 | @end 88 | 89 | @interface IDESourceCodeDocument : IDEEditorDocument 90 | - (DVTSourceTextStorage *)textStorage; 91 | - (NSUndoManager *)undoManager; 92 | @end 93 | 94 | @interface IDESourceCodeComparisonEditor : NSObject 95 | @property (readonly) NSTextView *keyTextView; 96 | @property (retain) NSDocument *primaryDocument; 97 | @end 98 | 99 | @interface IDESourceCodeEditor : NSObject 100 | @property (retain) NSTextView *textView; 101 | - (IDESourceCodeDocument *)sourceCodeDocument; 102 | @end 103 | 104 | @interface IDEEditorContext : NSObject 105 | - (id)editor; // returns the current editor. If the editor is the code editor, the class is `IDESourceCodeEditor` 106 | @end 107 | 108 | @interface IDEEditorArea : NSObject 109 | - (IDEEditorContext *)lastActiveEditorContext; 110 | @property(readonly) IDEEditorDocument *primaryEditorDocument; 111 | @end 112 | 113 | @interface IDEWorkspaceWindowController : NSObject 114 | @property (readonly) IDEWorkspaceTabController *activeWorkspaceTabController; 115 | - (IDEEditorArea *)editorArea; 116 | @end 117 | 118 | @interface IDEWorkspace : NSObject 119 | @property (readonly) DVTFilePath *representingFilePath; 120 | @end 121 | 122 | @interface IDEWorkspaceDocument : NSDocument 123 | @property (readonly) IDEWorkspace *workspace; 124 | @end 125 | -------------------------------------------------------------------------------- /MarvinPlugin/Supporting Files/MarvinPlugIn-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | English 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ${PRODUCT_NAME} 17 | CFBundlePackageType 18 | BNDL 19 | CFBundleShortVersionString 20 | 2.0.8 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 2.0.8 25 | DVTPlugInCompatibilityUUIDs 26 | 27 | FEC992CC-CA4A-4CFD-8881-77300FCB848A 28 | 63FC1C47-140D-42B0-BB4D-A10B2D225574 29 | 37B30044-3B14-46BA-ABAA-F01000C27B63 30 | 640F884E-CE55-4B40-87C0-8869546CAB7A 31 | A2E4D43F-41F4-4FB9-BB94-7177011C9AED 32 | AD68E85B-441B-4301-B564-A45E4919A6AD 33 | C4A681B0-4A26-480E-93EC-1218098B9AA0 34 | 992275C1-432A-4CF7-B659-D84ED6D42D3F 35 | A16FF353-8441-459E-A50C-B071F53F51B7 36 | 9F75337B-21B4-4ADC-B558-F9CADF7073A7 37 | E969541F-E6F9-4D25-8158-72DC3545A6C6 38 | AABB7188-E14E-4433-AD3B-5CD791EAD9A3 39 | 7FDF5C7A-131F-4ABB-9EDC-8C5F8F0B8A90 40 | 0420B86A-AA43-4792-9ED0-6FE0F2B16A13 41 | 7265231C-39B4-402C-89E1-16167C4CC990 42 | F41BD31E-2683-44B8-AE7F-5F09E919790E 43 | ACA8656B-FEA8-4B6D-8E4A-93F4C95C362C 44 | 8A66E736-A720-4B3C-92F1-33D9962C69DF 45 | 46 | NSHumanReadableCopyright 47 | Copyright © 2014 Christoffer Winterkvist. All rights reserved. 48 | NSPrincipalClass 49 | MarvinPlugin 50 | XC4Compatible 51 | 52 | XCGCReady 53 | 54 | XCPluginHasUI 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /MarvinPlugin/Supporting Files/MarvinPlugIn-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header for all source files of the 'MarvinPlugins' target in the 'MarvinPlugins' project 3 | // 4 | 5 | #ifdef __OBJC__ 6 | #import 7 | #endif 8 | 9 | #import "XcodePrivate.h" 10 | -------------------------------------------------------------------------------- /MarvinPlugin/Supporting Files/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Marvin for Xcode 2 | 3 | ####Marvin is a plugin for Xcode, it adds a large collection of text selections, duplication and deletion commands. 4 | 5 | Accessible via `Xcode -> Edit -> Marvin` 6 | 7 | It includes the following commands (some might seem obvious but some need a little more detail to describe its function and value). 8 | 9 | - Delete Line 10 | - Duplicate Line 11 | - Join Line 12 | - Move To EOL and Insert LF 13 | - Select Current Word 14 | - Select Line Contents 15 | - This differs a bit from Select Line as it will exclude whitespace characters until it reaches the first valid character at both the beginning and end of the current line 16 | - Select Next Word 17 | - Select Previous Word 18 | - Select Word Above 19 | - Select Word Below 20 | - Sort lines 21 | 22 | As an added bonus, on save, Marvin also 23 | 24 | - Magically cleans up whitespace on save. 25 | - Adds a LF at the end of the document 26 | 27 | #### Install via Alcatraz 28 | 29 | * Install plugin and restart Xcode. 30 | 31 | #### Build from Source 32 | 33 | * Build the Xcode project. The plug-in will automatically be installed in `~/Library/Application Support/Developer/Shared/Xcode/Plug-ins`. 34 | 35 | * Relaunch Xcode. 36 | 37 | To uninstall, just remove the plugin from `~/Library/Application Support/Developer/Shared/Xcode/Plug-ins` and restart Xcode. 38 | 39 | ### Customize 40 | 41 | You can configure Marvin's keyboard shortcuts by adding them to `System Preferences > Keyboard > Shortcuts`. Note that the `Menu Title` needs to match Marvin commands 42 | 43 | Keyboard Shortcuts 44 | 45 | ## Contribute 46 | 47 | 1. Fork it 48 | 2. Create your feature branch (`git checkout -b my-new-feature`) 49 | 3. Commit your changes (`git commit -am 'Add some feature'`) 50 | 4. Push to the branch (`git push origin my-new-feature`) 51 | 5. Create pull request 52 | 53 | ## Thanks 54 | 55 | A big shout out goes out to Benoît Bourdon [@benoitsan](https://github.com/benoitsan). 56 | He made [BBUncrustifyPlugin-Xcode](https://github.com/benoitsan/BBUncrustifyPlugin-Xcode) which includes private Xcode headers and some convenience methods that is being used in this project. 57 | Without his tremendous work this might not have ever happened. 58 | -------------------------------------------------------------------------------- /keyboard-shortcuts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenangst/MarvinXcode/155fed02c09363b7000a08287c6534cc92f55241/keyboard-shortcuts.png -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenangst/MarvinXcode/155fed02c09363b7000a08287c6534cc92f55241/screenshot.png --------------------------------------------------------------------------------