gh-ypresto-SwiftLintXcode/SwiftLintXcode/Formatter.swift
2016-04-06 18:23:39 +09:00

138 lines
5.9 KiB
Swift

//
// Formatter.swift
// SwiftLintXcode
//
// Created by yuya.tanaka on 2016/04/04.
// Copyright (c) 2016 Yuya Tanaka. All rights reserved.
//
import Foundation
import Cocoa
final class Formatter {
static var sharedInstance = Formatter()
private static let pathExtension = "SwiftLintXcode"
private let fileManager = NSFileManager.defaultManager()
private struct CursorPosition {
let line: Int
let column: Int
}
class func isFormattableDocument(document: NSDocument) -> Bool {
return (document.fileURL?.pathExtension?.lowercaseString == "swift") ?? false
}
func tryFormatDocument(document: IDESourceCodeDocument) -> Bool {
do {
try formatDocument(document)
return true
} catch let error as NSError {
NSAlert(error: error).runModal()
} catch {
NSAlert(error: NSError(domain: "net.ypresto.SwiftLintXcode", code: 0, userInfo: [
NSLocalizedDescriptionKey: "Unknown error occured: \(error)"
])).runModal()
}
return false
}
func formatDocument(document: IDESourceCodeDocument) throws {
let textStorage: DVTSourceTextStorage = document.textStorage()
let originalString = textStorage.string
let formattedString = try formatString(originalString)
if formattedString == originalString { return }
let selectedRange = SwiftLintXcodeTRVSXcode.textView().selectedRange()
let cursorPosition = cursorPositionForSelectedRange(selectedRange, textStorage: textStorage)
textStorage.beginEditing()
textStorage.replaceCharactersInRange(NSRange(location: 0, length: textStorage.length), withString: formattedString, withUndoManager: document.undoManager())
textStorage.endEditing()
let newLocation = locationForCursorPosition(cursorPosition, textStorage: textStorage)
SwiftLintXcodeTRVSXcode.textView().setSelectedRange(NSRange(location: newLocation, length: 0))
}
private func cursorPositionForSelectedRange(selectedRange: NSRange, textStorage: DVTSourceTextStorage) -> CursorPosition {
let line = textStorage.lineRangeForCharacterRange(selectedRange).location
let column = selectedRange.location - startLocationOfLine(line, textStorage: textStorage)
return CursorPosition(line: line, column: column)
}
private func locationForCursorPosition(cursorPosition: CursorPosition, textStorage: DVTSourceTextStorage) -> Int {
let startOfLine = startLocationOfLine(cursorPosition.line, textStorage: textStorage)
let locationOfNextLine = textStorage.characterRangeForLineRange(NSRange(location: cursorPosition.line + 1, length: 0)).location
// XXX: Can reach EOF..? Cursor position may be trimmed one charactor when cursor is on EOF.
return min(startOfLine + cursorPosition.column, locationOfNextLine - 1)
}
private func startLocationOfLine(line: Int, textStorage: DVTSourceTextStorage) -> Int {
return textStorage.characterRangeForLineRange(NSRange(location: line, length: 0)).location
}
private func formatString(string: String) throws -> String {
return try withTempporaryFile { (filePath) in
try string.writeToFile(filePath, atomically: false, encoding: NSUTF8StringEncoding)
let swiftlintPath = try self.getExecutableOnPath("swiftlint")
let task = NSTask.launchedTaskWithLaunchPath(swiftlintPath, arguments: [
"autocorrect", "--path", filePath
])
task.waitUntilExit()
if task.terminationStatus != 0 {
throw NSError(domain: "net.ypresto.SwiftLintXcode", code: 0, userInfo: [
NSLocalizedDescriptionKey: "Executing swiftlint exited with non-zero status."
])
}
return try String(contentsOfFile: filePath, encoding: NSUTF8StringEncoding)
}
}
private func getExecutableOnPath(name: String) throws -> String {
let pipe = NSPipe()
let task = NSTask()
task.launchPath = "/bin/bash"
task.arguments = [
"-l", "-c", "which \(name)"
]
task.standardOutput = pipe
task.launch()
task.waitUntilExit()
if task.terminationStatus != 0 {
throw NSError(domain: "net.ypresto.SwiftLintXcode", code: 0, userInfo: [
NSLocalizedDescriptionKey: "Executing `which swiftlint` exited with non-zero status."
])
}
let data = pipe.fileHandleForReading.readDataToEndOfFile()
guard let pathString = String(data: data, encoding: NSUTF8StringEncoding) else {
throw NSError(domain: "net.ypresto.SwiftLintXcode", code: 0, userInfo: [
NSLocalizedDescriptionKey: "Cannot read result of `which swiftlint`."
])
}
let path = pathString.stringByTrimmingCharactersInSet(NSCharacterSet.newlineCharacterSet())
if !fileManager.isExecutableFileAtPath(path) {
throw NSError(domain: "net.ypresto.SwiftLintXcode", code: 0, userInfo: [
NSLocalizedDescriptionKey: "swiftlint at \(path) is not executable."
])
}
return path
}
private func withTempporaryFile<T>(@noescape callback: (filePath: String) throws -> T) throws -> T {
let filePath = createTemporaryPath()
if fileManager.fileExistsAtPath(filePath) {
throw NSError(domain: "net.ypresto.SwiftLintXcode", code: 0, userInfo: [
NSLocalizedDescriptionKey: "Cannot write to \(filePath), file already exists."
])
}
defer { _ = try? fileManager.removeItemAtPath(filePath) }
return try callback(filePath: filePath)
}
private func createTemporaryPath() -> String {
return NSURL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
.URLByAppendingPathComponent("SwiftLintXcode_\(NSUUID().UUIDString).swift").path!
}
}