How to Create an Xcode Source Editor Extension

How to Create an Xcode Source Editor Extension

Final product image
What You'll Be Creating

Introduction

Xcode is the main IDE (Integrated Development Environment) used by thousands and thousands of developers every day. It is an awesome tool, but sometimes you want to customize some of its features and behaviors to better fit your workflow.

Until Xcode 7, it was possible to inject code into Xcode at runtime to create plugins. Plugins could be submitted and distributed through a great app called Alcatraz. This is no longer possible in Xcode 8.

Xcode 8 validates each library and bundle to prevent malicious code from running without your permission. When Xcode starts, previously installed plugins with Alcatraz are not loaded anymore. Not everything is lost though, Apple also announced at WWDC the possibility to develop Xcode source editor extensions so that everyone can extend existing source editing features. Let's take a look about what we can achieve with such extensions.

1. Getting Started

Xcode 8 source editor extensions are a first step in the right direction. If you have been working with Xcode for a while, you may have found yourself in a situation where you wished that a specific task could be automated within Xcode. Source editor extensions allow third party applications to modify a source file, which is exactly what you need to speed up your workflow.

At this moment, extensions can only interact with the source code. This means that not every plugin available through Alcatraz can be replaced by a source editor extension. But who knows what the future brings.

It is important to understand that each extension must be contained in a macOS app. You could, for example, add preferences and explanations about what the extension does in the macOS app and distribute it through the Mac App Store. Also note that each extension runs in a separate process. If the extension crashes, it won't crash Xcode. Instead, it will display a message that the extension was not able to finish its work.

Furthermore, extensions don't have a user interface and they can only modify the code directly when the user invokes your command. They can't, for example, run in the background.

I recommend watching the WWDC 2016 session about source editor extensions. Not only does it explain how to get started developing Xcode source editor extensions, it also shows tips and shortcuts to speed up your development.

2. Overview

In this tutorial, we are going to develop an extension that cleans up the closure syntax in Swift. Xcode autocompletes a closure syntax with the parentheses, but they can be omitted for brevity. This is a task that can be easily be automated by wrapping it into a source editor extension.

The shorter explanation of what we are going to develop is an extension that transforms any closure to the simpler and cleaner syntax. Take a look at the example below.

3. Project Setup

It goes without saying that this tutorial requires Xcode 8. You can download it from Apple's developer website. It runs on both OS X 10.11 and macOS 10.12.

Create a new OS X project of type Cocoa Application and give it the name CleanClosureSyntax. Make sure that you have set the language of the project to Swift. We will use the new Swift 3 syntax in this tutorial.

Create A New Cocoa Application Project in Xcode 8

We will leave the macOS app empty for now and we can focus on creating the Xcode source editor extension. From the File menu, choose New > Target.... In the left sidebar, choose OS X and select Xcode Source Editor Extension from the list.

Add a New Target of Type Xcode Source Editor Extension

Click Next and set Product Name to Cleaner. A new target will be created for you. Click Activate if Xcode asks you if the newly created Scheme should be activated.

4. Project Structure

Let's first analyze what Xcode just created for us. Expand the Cleaner folder to see its contents.

Xcode Project Contents and Layout

We won't modify SourceEditorExtension.swift in this tutorial, but it can be used to further customize your extension. The extensionDidFinishLaunching() method is called as soon as the extension is launched so that you can perform any initialization if needed. The commandDefinitions property getter can be used if you want to dynamically show or hide certain commands.

SourceEditorCommand.swift is the hear of the extension. This file is where you will implement the logic for the extension. The perform(with:completionHandler:) method is called when the user launches your extension. The XCSourceEditorCommandInvocation object contains a buffer property, which is used to access the source code in the currently selected file. The completion handler should be called with value nil if everything went well, otherwise pass it an NSError instance.

5. Implementing the Extension

Now that the project contains all the required targets, we are ready to start writing the extension. To recap, we want to remove the parentheses from any closure in a Swift file. This can be done in three steps:

  • find the lines that contain a closure
  • remove the two parentheses from that particular line
  • substitute back the modified line

Let's get started.

We can use a regex (regular expression) to parse each line of code and see if it contains a closure. You can refer to Akiel's tutorial about Swift and regular expressions if you want to learn more about regular expressions. You can use RegExr to test your regular expressions. Take a look at the following screenshot to see how I tested my regex.

Regex to Parse A Closure Syntax

Open SourceEditorCommand.swift and modify the perform(with:completionHandler:) method to look like this:

Find Lines With Closure Syntax

We first create and array of Int values that will contain the line indexes of the modified lines. This is because we don't want to substitute all the lines. We want to replace only the lines that we modify.

We enumerate over all the lines of the invocation.buffer object and we try to find a match for the RegularExpression object. If I remove the escaping characters from the regex, it looks like the following:

This regex matches when a string has the following characteristics:

  • It has a curly open bracket ({), which is followed by 0 or more characters, except a new line character (n).
  • An open parenthesis (() must be found again, followed by 0 or more characters. This part should contain the parameters of the closure.
  • We then need to find a closing parenthesis ()), followed by 0 or more characters, which are the optional return types.
  • Finally, the in keyword should be found.

If the RegularExpression object fails to find a match (for example, if the regex is not valid), we call the completionHandler with the error as a parameter. If a string that matches all these conditions is found on a line, we have correctly localized a closure.

Clean Up Syntax

When a match is found, we call a utility method on NSString that removes the parentheses. We also need to pass in the range of the match to avoid removing some other parentheses outside of the closure.

Update Lines

The last part of code checks that at least a line was changed. If this is true, we call setArray() to substitute back the new lines and the correct indexes. The completion handler is called with the value nil so that Xcode knows that everything went well.

We still have to implement the remove(characters:range:) method on NSString. Let's add this extension at the top of file.

This method calls replacingOccurrences(of:with:range:) on NSString for each character that we want to remove.

6. Testing

Xcode 8 comes with a great solution to test extensions. First of all, if you are running OS X 10.11 El Capitan, open Terminal, execute the following command, and restart your Mac.

After doing this, build and run your extension by selecting the appropriate scheme. When it asks which app to run, search for Xcode and make sure to select the beta version of Xcode 8. A new Xcode version will be launched with the application icon grayed out so that you can recognize in which instance of Xcode you are testing the extension.

In the new Xcode instance, create a new project or open an existing one, and go to Editor > Clean Closure > Source Editor Command. Make sure to have at least one closure in the currently focused Swift file to see the result. As you can see in the following animation, our extension works.

The Final Result

Source Editor Command is the default name for a command. You can modify it in the Info.plist file of the extension. Open it and change the string to Clean Syntax.

We can also assign a shortcut to automatically invoke the Clean Syntax command. Open Xcode's Preferences and select the Key Bindings tab. Search for Clean Syntax and the command will appear. Click to the right of it and press the shortcut you would like to use, for example, Command-Alt-Shift-+. You can now go back to the source file and press that shortcut to invoke it directly.

Xcode Preferences to Set a Key Binding for an Extension Command

7. Tips and Tricks

Xcode 8 and source editor extensions are still in beta at the time of writing. The following tips can help you debug some issues you may be running into.

If your extension is not selectable in the Xcode testing instance, kill the com.apple.dt.Xcode.AttachToXPCService process and run the extension again.

Xcode Has A Process That You Can Kill if the Extension Is No Visible

Only substitute back the lines that you modify in the buffer. This makes the extension run faster and it will have less chances to be killed by Xcode. The latter can happen if Xcode believes a command of your extension is taking too long.

If you want to show multiple commands, assign to each command a different identifier and use the commandIdentifier property on the XCSourceEditorCommandInvocation object to recognize which one the user triggered.

Conclusion

Creating Xcode source editor extension is really easy. If you can improve your workflow and speed up your development by creating a source editor extension, then go ahead and make it happen. Apple introduced a new way for developers to share signed plugins through the Mac App Store so that you can release your work and watch other developers benefit from it.

You can find the source code of the example of this tutorial on GitHub.

Source: Tuts Plus

About the Author