XCode8起,XCode不在支持插件,Alcatraz不能再使用了。苹果提供了XCodeKit源码编辑器拓展,使用它开发出XCode拓展来代替以前的插件。
环境
macOS 10.12
XCode 8.2.1
新建工程
新建macOS Cocoa Application,命名为MyComment
新建一个"Xcode Source Editor Extension"的target,命名为MyCommentExt
注意:如果弹出询问是否激活scheme提示,选择激活
编辑scheme
在编辑框里找到Run->Info->Executable选择Other,然后找到XCode.app
之后运行这个拓展Target就会出现灰色Xcode,这个就是可用测试我们工具的运行实例。
修改命令
MyCommentExt中Info.plist修改XCSourceEditorCommandIdentifier, XCSourceEditorCommandName内容。这个是命名菜单里有我们的命令。
运行查看前,需要添加证书。Signing里使用自动管理证书。MyComment,MyCommentExt都需要添加。
点运行后,随便选一个工程打开,开一个源文件查看
添加代码实现我们的功能
在我们创建的工程里有两个swift文件,SourceEditorExtension.swift, SourceEditorCommand.swift。前者可以用来自定义命令,而命令在后者里实现。通过继承XCSourceEditorCommand,XCode在调用这个命令时将会执行perform方法。我们可以在perform里实现我们要的功能。下面引用了XCodeCComment的代码。(稍微修改了一下它的小问题。)
// SourceEditorCommand.swift
import Foundation
import XcodeKit
enum CommentStatus {
case Plain
case Unpair
case Pair(range: NSRange)
}
class SourceEditorCommand: NSObject, XCSourceEditorCommand {
func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: @escaping (Error?) -> Swift.Void ) {
let buffer = invocation.buffer
let selections = buffer.selections
selections.forEach {
let range = $0 as! XCSourceTextRange
print("forEach", range.start, range.end)
if range.start.line == range.end.line && range.start.column == range.end.column {
let fake: XCSourceTextRange = range
let lineText = buffer.lines[fake.start.line] as! String
fake.start.column = 0
fake.end.column = lineText.distance(from: lineText.startIndex, to: lineText.endIndex) - 1
if fake.end.column > fake.start.column {
let ref = handle(range: fake, inBuffer: buffer)
if ref == true {
range.end.column += 4
}
}
} else {
let ref = handle(range: range, inBuffer: buffer)
if ref == true {
range.end.column += 4
}
}
}
completionHandler(nil)
}
func handle(range: XCSourceTextRange, inBuffer buffer: XCSourceTextBuffer) -> Bool {
let selectedText = text(inRange: range, inBuffer: buffer)
let status = selectedText.commentStatus
switch status {
case .Unpair:
break
case .Plain:
insert(position: range.end, length: 1, with: "*/", inBuffer: buffer)
insert(position: range.start, length: 0, with: "/*", inBuffer: buffer)
return true
case .Pair(let commentedRange):
let startPair = position(range.start, offsetBy: commentedRange.location, inBuffer: buffer)
let endPair = position(range.start, offsetBy: commentedRange.location + commentedRange.length, inBuffer: buffer)
replace(position: endPair, length: -("*/".characters.count), with: "", inBuffer: buffer)
replace(position: startPair, length: "/*".characters.count, with: "", inBuffer: buffer)
}
return false
}
func text(inRange textRange: XCSourceTextRange, inBuffer buffer: XCSourceTextBuffer) -> String {
if textRange.start.line == textRange.end.line {
let lineText = buffer.lines[textRange.start.line] as! String
let from = lineText.index(lineText.startIndex, offsetBy: textRange.start.column)
let to = lineText.index(lineText.startIndex, offsetBy: textRange.end.column)
return lineText[from...to]
}
var text = ""
for aLine in textRange.start.line...textRange.end.line {
let lineText = buffer.lines[aLine] as! String
switch aLine {
case textRange.start.line:
text += lineText.substring(from: lineText.index(lineText.startIndex, offsetBy: textRange.start.column))
case textRange.end.line:
text += lineText.substring(to: lineText.index(lineText.startIndex, offsetBy: textRange.end.column + 1))
default:
text += lineText
}
}
return text
}
func position(_ i: XCSourceTextPosition, offsetBy: Int, inBuffer buffer: XCSourceTextBuffer) -> XCSourceTextPosition {
var aLine = i.line
var aLineColumn = i.column
var n = offsetBy
repeat {
let aLineCount = (buffer.lines[aLine] as! String).characters.count
let leftInLine = aLineCount - aLineColumn
if leftInLine <= n {
n -= leftInLine
} else {
return XCSourceTextPosition(line: aLine, column: aLineColumn + n)
}
aLine += 1
aLineColumn = 0
} while aLine < buffer.lines.count
return i
}
func replace(position: XCSourceTextPosition, length: Int, with newElements: String, inBuffer buffer: XCSourceTextBuffer) {
var lineText = buffer.lines[position.line] as! String
var start = lineText.index(lineText.startIndex, offsetBy: position.column)
var end = lineText.index(start, offsetBy: length)
if length < 0 {
swap(&start, &end)
}
lineText.replaceSubrange(start..<end, with: newElements)
lineText.remove(at: lineText.index(before: lineText.endIndex)) //remove end "\n"
buffer.lines[position.line] = lineText
}
func insert(position: XCSourceTextPosition, length: Int, with newElements: String, inBuffer buffer: XCSourceTextBuffer) {
var lineText = buffer.lines[position.line] as! String
var start = lineText.index(lineText.startIndex, offsetBy: position.column + length)
if start >= lineText.endIndex {
start = lineText.index(before: lineText.endIndex)
}
lineText.insert(contentsOf: newElements.characters, at: start)
lineText.remove(at: lineText.index(before: lineText.endIndex)) //remove end "\n"
buffer.lines[position.line] = lineText
}
}
extension String {
var commentedRange: NSRange? {
do {
let expression = try NSRegularExpression(pattern: "/\\*[\\s\\S]*\\*/", options: [])
let matches = expression.matches(in: self, options: [], range: NSRange(location: 0, length: self.distance(from: self.startIndex, to: self.endIndex)))
return matches.first?.range
} catch {
}
return nil
}
var isUnpairCommented: Bool {
if let i = characters.index(of: "*") {
if i > startIndex && i < endIndex {
if characters[index(before: i)] == "/" ||
characters[index(after: i)] == "/" {
return true
}
}
}
return false
}
var commentStatus: CommentStatus {
if let range = commentedRange {
return .Pair(range: range)
}
if isUnpairCommented {
return .Unpair
}
return .Plain
}
}
perform中invocation.buffer可以获取到我们执行命令时的缓冲文本信息,包括选中的文本内容,每行内容等。buffer.sections是所选文本内容数组,由XCSourceTextRange组成,它标示文本的位置区间,可以找到所选文本的起始位置。buffer.lines是缓冲文本的每行对应的内容,通过对它进行操作起到操作文本的作用。
再次运行代码可以看到我们的命令生效了
安装拓展
找到Products在Finder,将之拷贝到应用程序文件夹内,再重启XCode就可以看到生效了。
另外,在系统的“偏好设置”的“拓展”中也可以看到拓展。如果我们从AppStore或第三方下载到拓展,可以这里进行启用和禁用。
遗留一个问题
从我运行过这个程序后,XCode自身带有的注释功能就不可用了。但是如果我调试Run了本工程时,却又可以用。停止调试后又不能用了。应该是XCode的bug吧,暂时没有找到方法解决。知道解决的告诉我一下,谢谢。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。