mirror of
https://github.com/nicklockwood/SwiftFormat.git
synced 2026-06-16 10:34:34 +00:00
118 lines
5.2 KiB
Swift
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)
|
|
}
|
|
}
|