The new `Clang-Formatter` extension

2023, May 22    

Since the original tool ClangFormat-Xcode now no longer supports Xcode 11 and above. My recent iOS project still contains quite a bunch of legacy ObjC code. Hence I decide to build a new Xcode plugin for it, to facilitate my coding process :p.

1. The ClangFormat Tool

The ClangFormat is a tool provided by llvm project to format C/C++/ObjC/C# code. There are wide range of preset styles can be selected: LLVM, GNU, Google, Chromium, Microsoft, Mozilla, WebKit. In our cases, the team will choose Google style.

There are mainly two ways of using the clangFormat cmd:

  • execute the command via standard input:
# cmd
clang-format <<< 'NSString *shellString = [environmentDict objectForKey:@"SHELL"]   ?:    @"/bin/bash";'
# result
NSString *shellString = [environmentDict objectForKey:@"SHELL"] ?: @"/bin/bash";
  • execute the command with a file containing the list of files to be formatted.
# cmd & result will be updated into the files
/Users/samuelzhaoy/Desktop/clang-format -i --style=Google  --files=filename

It is essential to maintain your own .clang-format in the root dir of the project. To fine adjust the style on your preference, please refer to Clang-Format Style Options.

2. The Xcode Extension

As which has been already mentioned in my previously blog interactive xcode plugin, now we would like to use the previous project as the extension boiler plate to quickly build up the extension.

//
//  SourceEditorCommand.swift
//  Extension
//
//  Created by Samuel
//

import Foundation
import XcodeKit

class LintCommand: NSObject, XCSourceEditorCommand {

    func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: @escaping (Error?) -> Void ) -> Void {

        // get selected code:
        guard let firstSelection = invocation.buffer.selections.firstObject as? XCSourceTextRange,
              let lastSelection = invocation.buffer.selections.lastObject as? XCSourceTextRange else {
            completionHandler(nil)
            return
        }

        var length = lastSelection.end.line - firstSelection.start.line + 1
        if length > invocation.buffer.lines.count {
            length = invocation.buffer.lines.count
        }
        let selectedRange = NSMakeRange(firstSelection.start.line, length)
        var selectedSection = invocation.buffer.lines.subarray(with:selectedRange)

        // modify content:
        let input = (selectedSection as! [String]).joined(separator: " ")
        let result = shell("/Users/$(whoami)/Downloads/clang-format --style=\(style()) <<< '\(input)'")
        let refactoredLines = result.components(separatedBy: .newlines)
        selectedSection.removeAll()

        // update lines:
        invocation.buffer.lines.removeObjects(at: NSIndexSet(indexesIn: selectedRange) as IndexSet)
        let insertionIndexSets = NSIndexSet(indexesIn: NSMakeRange(firstSelection.start.line, refactoredLines.count))
        invocation.buffer.lines.insert(refactoredLines, at: insertionIndexSets as IndexSet)

        completionHandler(nil)
    }

    // Reference: http://clang.llvm.org/docs/ClangFormatStyleOptions.html
    func style() -> String {

        var builder = "'{"
        builder += "BasedOnStyle: Google,"
        builder += "Language: ObjC,"

        /*
         Comments in the end of continious lines should get aligned:
         ```
         int a;  // comment
         int ab; // comment

         int abc;  // comment
         int abcd; // comment
         ```
         */
        builder += "AlignTrailingComments: { Kind: Always, OverEmptyLines: 2 },"

        /*
         Add a space before each trailing comments
         // foo1
         */
        builder += "SpacesBeforeTrailingComments: 1,"

        /*
         Pointer & references should align to the right:
         ```
         int *a;
         ```
         */
        builder += "PointerAlignment: Right,"

        /*
         Indent width of 4
         */
        builder += "IndentWidth: 4,"

        /*
         Align requires expression body relative to the indentation
         level of the outer scope the requires expression resides in:
         ```
         template <typename T>
         concept C = requires(T t) {
           ...
         }
         ```
         */
        builder += "RequiresExpressionIndentation: OuterScope,"

        /*
         Horizontally aligns arguments after an open bracket,
         support angle brackets and square brackets as well:
         ```
         someLongFunction(argument1,
                          argument2);
         ```
         */
        builder += "AlignAfterOpenBracket: Align,"

        /*
         Whether indent for switch `case` label:
         ```
         switch (type) {
             case LOGIN_TYPE_TWITTER:
         }
         ```
         */
        builder += "IndentCaseLabels: true,"

        /*
         Space between property and attribute decalrations:
         `@property (strong, atomic) NSString *prop1;`
         */
        builder += "ObjCSpaceAfterProperty: true,"

        /*
         The indentation inside of ObjC blocks:
         ```
         [operation setCompletionBlock:^{
             [self onOperationDone];
         }];
         ```
         */
        builder += "ObjCBlockIndentWidth: 4,"

        /*
         Add a space in front of an Objective-C protocol list,
         ```
         Foo <Protocol>
         ```
         */
        builder += "ObjCSpaceBeforeProtocolList: true,"

        /*
         When there is a block parameter,
         break the arguments into lines for increasing the readability
         ```
         - (void)_aMethod
         {
            [self.test1 t:self
                         w:self
                callback:^(typeof(self) self, NSNumber *u, NSNumber *v) {
                     u = c;
                 }]
         }
         ```
         */
        builder += "ObjCBreakBeforeNestedBlockParam: true,"

        /*
         🦄️ Considered the style of ObjC requires extra brackes and spacing perline,
         make the col limit longer: https://github.com/flutter/engine/pull/3585
         following google style.
         */
        builder += "ColumnLimit: 100,"

        /*
         Max empty lines allowed
         */
        builder += "MaxEmptyLinesToKeep: 1,"

        /*
         Allow empty line at the start of block, to enhance the readability
         ```
         if (foo) {

           bar();
         }
         ```
         */
        builder += "KeepEmptyLinesAtTheStartOfBlocks: true,"

        /*
         No space near square brackets allowed, follows ObjC conventions
         */
        builder += "SpacesInSquareBrackets: false,"

        /*
         No space in the start/end of parenthesis, follows ObjC conventions
         */
        builder += "SpacesInParentheses : false,"

        /*
         Space in container litrals, such as dictionary:
         ```
         @{
            @"key" : @"val"
         }
         ```
         */
        builder += "SpacesInContainerLiterals: true,"

        /*
         Space before assignment operator is required:
         `int a = 0;`
         */
        builder += "SpaceBeforeAssignmentOperators: true,"

        /*
         Spaces should not be inserted into ().
         */
        builder += "SpaceInEmptyParentheses: false,"

        /*
         Generics and protocol angles should not contain extra spaces
         */
        builder += "SpacesInAngles: false,"

        /*
         Lambda also follows the indentation rule of ObjC
         */
        builder += "LambdaBodyIndentation: OuterScope,"

        /*
         No need to align consecutive assignments:
         ```
         int a = 1;
         int somelongname = 2;
         double c = 3;
         ```
         */
        builder += "AlignConsecutiveAssignments: false,"

        /*
         If choose line break on arguments, all args should be called on separate lines:
         ```
         callFunction(
             a,
             b,
             c,
             d
         );
         ```
         */
        builder += "AllowAllArgumentsOnNextLine: false,"

        /*
         Similar to 'AllowAllArgumentsOnNextLine', but this refers to
         the style specification on method signature.
         */
        builder += "AllowAllParametersOfDeclarationOnNextLine: false,"

        /*
         For all infix operators, break line aftert the operator:
         ```
         int a = b + c +
            d +
            e +
            f;
         ```
         */
        builder += "BreakBeforeBinaryOperators: None,"

        /*
         Align macros to enhance the code readability:
         ```
         #define SHORT_NAME       42
         #define LONGER_NAME      0x007f
         #define EVEN_LONGER_NAME (2)
         #define foo(x)           (x * x)
         #define bar(y, z)        (y + z)
         ```
         */
        builder += "AlignConsecutiveMacros: { Enabled: true, AcrossEmptyLines: true, AcrossComments: false },"

        /*
         Force line break for each function call.
         */
        builder += "AllowShortFunctionsOnASingleLine: None,"

        /*
         Labels like `break;return;goto` in switch case must go to a different line.
         */
        builder += "AllowShortCaseLabelsOnASingleLine: false,"

        /*
         No same line if statement allowed.
         */
        builder += "AllowShortIfStatementsOnASingleLine: false,"

        /*
         No same line while clause short circuit allowed. e.g. `continue`
         */
        builder += "AllowShortLoopsOnASingleLine: false,"

        /*
         No same line while blocks allowed.
         */
        builder += "AllowShortBlocksOnASingleLine: false,"

        /*
         A function declaration’s or function definition’s parameters
         will either all be on the same line
         or will have one line each.
         */
        builder += "BinPackParameters: false,"

        /*
         A summary of brace wrapping condition:
         currently only brace wrapper for class is mandatary
         */
        builder += "BreakBeforeBraces: Custom,"
        builder += "BraceWrapping: {AfterClass: true, AfterControlStatement: false, AfterEnum: false, AfterFunction: false, AfterNamespace: false, AfterStruct: false, AfterUnion: false, BeforeCatch: false}"

        builder += "}'"

        return builder
    }

    func shell(_ command: String) -> String {
        let task = Process()
        let pipe = Pipe()

        task.standardOutput = pipe
        task.standardError = pipe
        task.arguments = ["-c", command]
        task.launchPath = "/bin/bash"
        task.standardInput = nil
        task.launch()

        let data = pipe.fileHandleForReading.readDataToEndOfFile()
        let output = String(data: data, encoding: .utf8)!

        return output
    }
}

Current issues I am facing: due to apple’s sandbox restriction, in order to use a specific scripts, the scripts need to be grant specific permissions. (Refer to here). Temproraly it is solved by putting the clang-format under download folder.

I tried to invoke a NSOpenPanel to ask the user provide the access permission for the file, however it seems any direct UI invocation in the extension layer is prohibited. So there will be two possible solution:

  • I found out there is a cpp header file for clang-format: format.h, so which means we are likely to invoke the format command directly from the extention swift code.
  • Another more common approach is to set up a socket connection with a mac app. Let the extension keep sending commands and let the mac app execute.

As the current project goes on, I will explore both the two options.

TOC