Files
SwiftFormat/EditorExtension/Extension/FormatSelectedSourceCommand.swift
T
2016-10-21 08:35:37 +01:00

118 lines
5.2 KiB
Swift

//
// FormatSelectedSourceCommand.swift
// Swift Formatter
//
// Created by Tony Arnold on 5/10/16.
// Copyright 2016 Nick Lockwood
//
// Distributed under the permissive zlib license
// Get the latest version from here:
//
// https://github.com/nicklockwood/SwiftFormat
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
// claim that you wrote the original software. If you use this software
// in a product, an acknowledgment in the product documentation would be
// appreciated but is not required.
//
// 2. Altered source versions must be plainly marked as such, and must not be
// misrepresented as being the original software.
//
// 3. This notice may not be removed or altered from any source distribution.
//
import Foundation
import XcodeKit
class FormatSelectedSourceCommand: NSObject, XCSourceEditorCommand {
func perform(with invocation: XCSourceEditorCommandInvocation,
completionHandler: @escaping (Error?) -> Void) -> Void {
guard invocation.buffer.contentUTI == "public.swift-source" else {
return completionHandler(FormatCommandError.notSwiftLanguage)
}
guard let selection = invocation.buffer.selections.firstObject as? XCSourceTextRange else {
return completionHandler(FormatCommandError.noSelection)
}
// Grab the selected source to format using entire lines of text
let selectionRange = selection.start.line ... min(selection.end.line, invocation.buffer.lines.count - 1)
let sourceToFormat = selectionRange.flatMap { invocation.buffer.lines[$0] as? String }.joined()
// Remove all selections to avoid a crash when changing the contents of the buffer.
invocation.buffer.selections.removeAllObjects()
let indent = indentationString(for: invocation.buffer)
let options = FormatOptions(indent: indent, fragment: true)
do {
let formattedSource = try format(sourceToFormat, rules: defaultRules, options: options)
invocation.buffer.lines.removeObjects(in: NSMakeRange(selection.start.line, selectionRange.count))
invocation.buffer.lines.insert(formattedSource, at: selection.start.line)
let updatedSelectionRange = rangeForDifferences(
in: selection, between: sourceToFormat, and: formattedSource)
invocation.buffer.selections.add(updatedSelectionRange)
} catch let error {
return completionHandler(error)
}
return completionHandler(nil)
}
/// Given a source text range, an original source string and a modified target string this
/// method will calculate the differences, and return a usable XCSourceTextRange based upon the original.
///
/// - Parameters:
/// - textRange: Existing source text range
/// - sourceText: Original text
/// - targetText: Modified text
/// - Returns: Source text range that should be usable with the passed modified text
private func rangeForDifferences(in textRange: XCSourceTextRange,
between sourceText: String, and targetText: String) -> XCSourceTextRange {
// Ensure that we're not greedy about end selections — this can cause empty lines to be removed
let lineCountOfTarget = targetText.components(separatedBy: CharacterSet.newlines).count
let finalLine = (textRange.end.column > 0) ? textRange.end.line : textRange.end.line - 1
let range = textRange.start.line ... finalLine
let difference = range.count - lineCountOfTarget
let start = XCSourceTextPosition(line: textRange.start.line, column: 0)
let end = XCSourceTextPosition(line: finalLine - difference, column: 0)
return XCSourceTextRange(start: start, end: end)
}
/// Calculates the indentation string representation for a given source text buffer.
///
/// - Parameter buffer: Source text buffer
/// - Returns: Indentation represented as a string
private func indentationString(for buffer: XCSourceTextBuffer) -> String {
// NOTE: we cannot exactly replicate Xcode's indent logic in SwiftFormat because
// SwiftFormat doesn't support the concept of mixed tabs/spaces that Xcode does.
//
// But that's OK because mixing tabs and spaces is really stupid.
//
// So in the event that the user has chosen to use tabs, but their chosen indentation
// width is not a multiple of the tab width, we'll just use spaces instead.
if buffer.usesTabsForIndentation {
let tabCount = buffer.indentationWidth / buffer.tabWidth
if tabCount * buffer.tabWidth == buffer.indentationWidth {
return String(repeating: "\t", count: tabCount)
}
}
return String(repeating: " ", count: buffer.indentationWidth)
}
}