Xcode is the core tool for Apple development. Although it is well-integrated with the most development workflows, from time to time you might feel like missing some basic features. In this article you will learn how to create Xcode Source Editor Extension that adds some extra functionality to Xcode.

Explaining Xcode Source Editor Extensions

You create extensions to the source editor by means of XcodeKit framework. Extensions have quite limited functionality. They can read and modify the contents of current source file, select and deselect text within that file.

Most notable classes from XcodeKit are:

  • XCSourceEditorExtension - the protocol that every Xcode Source Editor Extension must implement. You can think of it as an AppDelegate from your iOS and macOS apps.
  • XCSourceEditorCommand - the protocol that stands for the source editor command handler. You can think of it as a sink where one or more command invocations are handled. You must implement at least one of these in your extension.
  • XCSourceEditorCommandInvocation - an instance of the command sent to your extension. It contains a buffer and an identifier. As already noted, multiple invocations can be handled by a single XCSourceEditorCommand.
  • XCSourceTextBuffer - a buffer used to manipulate the text contents and selections in a source editor.

Extension’s commands are accessible from Xcode Editor dropdown menu. By the end of this article, your command will look something like this:

Xcode Extension Tutorial: Getting Started - Locating Command in Xcode Editor Menu

Creating Xcode Project

Xcode editor extensions cannot exist on their own and must be wired up to a macOS application.

First off, we create a macOS project in Xcode named LinesSorter.

Xcode Extension Tutorial: Getting Started - Creating New MacOS Project in Xcode

Now add Xcode Source Editor Extension Target to your newly created project. Let’s call it SourceEditorExtension. Tap Activate when it prompts you Activate “SourceEditorExtension” scheme.

Xcode Extension Tutorial: Getting Started - Creating Xcode Source Editor Extension

At this point the targets must look like this:

Xcode Extension Tutorial: Getting Started - Targets List

Configuring Source Editor Command

All editor extension targets contain an extra entry in their Info.plist files named NSExtension. Lets unfold it and inspect inner properties.

Xcode Extension Tutorial: Getting Started - Editing Info.plist

The ones of particular interest are:

  • XCSourceEditorCommandClassName - a name of the command class. Xcode has already created a command class with exact same name once you created the source editor target.
  • XCSourceEditorCommandIdentifier - a command invocation identifier. Make sure you set it to something unique within your extension.
  • XCSourceEditorCommandName - a command name as it will be displayed in the second level of Xcode Editor menu. Let’s change it into Sort Selected Lines.

Bundle display name stands for your extension name in Editor menu. Let’s change it into Lines Sorter. There is no need to make any other edits as long as we have only one command.

Here is how extension name and command names are displayed in Editor menu:

Xcode Extension Tutorial: Getting Started - Editor Menu Explained

Implementing the Sorting Command

As you might have already guessed, our extension will sort selected lines of code. Sounds like an easy task? It sure is after you learning the basics of XcodeKit and the structure of source editor extension target.

I got so much carried away by writing this article that I ended up with LinesSorter extension and open sourced it recently. Make sure to check it out after reading this article.

We already know that as soon as the command is activated from Xcode Editor menu, an invocation instance is sent to a the command class.

Open SourceEditorCommand.swift and paste the method there:

1
2
3
4
5
6
7
8
9
10
11
12
func sort(_ inputLines: NSMutableArray, in range: CountableClosedRange<Int>, by comparator: (String, String) -> Bool) {
    guard range.upperBound < inputLines.count, range.lowerBound >= 0 else {
        return
    }

    let lines = inputLines.compactMap { $0 as? String }
    let sorted = Array(lines[range]).sorted(by: comparator)

    for lineIndex in range {
        inputLines[lineIndex] = sorted[lineIndex - range.lowerBound]
    }
}

Now we can sort selected lines by mutating invocation.buffer.selections which is an NSMutableArray of strings. All changes to the buffer will be reflected in Xcode source editor.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: @escaping (Error?) -> Void ) -> Void {
    defer { completionHandler(nil) }

    // At least something is selected
    guard let firstSelection = invocation.buffer.selections.firstObject as? XCSourceTextRange,
        let lastSelection = invocation.buffer.selections.lastObject as? XCSourceTextRange else {
            return
    }

    // One line is selected
    guard firstSelection.start.line < lastSelection.end.line else {
        return
    }

    sort(invocation.buffer.lines, in: firstSelection.start.line...lastSelection.end.line, by: <)
}

Few things going on here:

  1. Do some sanity checks that at least two lines of code are selected within the editor.
  2. Call sort method to perform the actual sorting by mutating the input NSMutableArray in place. We pass < operator to sort alphabetically.

Lines of codes tend to have different indentation levels that affects their sorting. We usually don’t want to take indentation into account as we sort. Let’s define a custom comparator that takes care of it.

1
2
3
func isLessThanIgnoringLeadingWhitespacesAndTabs(_ lhs: String, _ rhs: String) -> Bool {
    return lhs.trimmingCharacters(in: .whitespaces) < rhs.trimmingCharacters(in: .whitespaces)
}

And now pass it to sort method:

sort(invocation.buffer.lines, in: firstSelection.start.line...lastSelection.end.line, by: isLessThanIgnoringLeadingWhitespacesAndTabs)

Testing the Command

Finally let’s see our work in action. Testing Xcode Source Editor Extensions is different from what you have used to when developing macOS and iOS apps.

  1. First of all, both the app target and the source editor extension must be signed with your developer certificate. I will not dive too deeply into details, but Apple got you covered with this tutorial.
  2. Run SourceEditorExtension target and select Xcode app from the list. Source editor extension will be launched in a separate instance of Xcode that can be distinguished by a darker top bar.

Xcode Extension Tutorial: Getting Started - Run Extension

Now trigger the Sort Lines command:

  1. Select several lines of code.
  2. Go to Editor > Lines Sorter > Sort Selected Lines.

Voila, your lines must be ordered alphabetically now.

Like any other editor command, you can assign a keys combination to yours. Go to Xcode > Preferences > Key Bindings > search for “Lines Sorter”.

Xcode Extension Tutorial: Getting Started - Key Binding

Summary

In this article we learned how you can push Xcode IDE to the next limits by writing your own extension.

Source editor extensions are quite limited in their functionality and are capable of editing and selecting lines of code within a single file.

Creating Xcode Source Editor Extension might seem daunting at first glance. After learning XcodeKit, the process of setting up an Xcode project and testing the extension it does not appear as such.

Check out LinesSorter that is an extended version of the project we created during this tutorial. It also shows how to setup Unit tests and Continuous Integration for Xcode Source Editor Extension project.


I’d love to meet you in Twitter: @V8tr. And don’t forget to share this article if you find it useful.