import Foundation import SourceKittenFramework public extension String { func hasTrailingWhitespace() -> Bool { if isEmpty { return false } if let unicodescalar = unicodeScalars.last { return CharacterSet.whitespaces.contains(unicodescalar) } return false } func isUppercase() -> Bool { self == uppercased() } func isLowercase() -> Bool { self == lowercased() } private subscript (range: Range) -> String { let nsrange = NSRange(location: range.lowerBound, length: range.upperBound - range.lowerBound) if let indexRange = nsrangeToIndexRange(nsrange) { return String(self[indexRange]) } queuedFatalError("invalid range") } func substring(from: Int, length: Int? = nil) -> String { if let length { return self[from.. Int? { if let range = range(of: search, options: [.literal, .backwards]) { return distance(from: startIndex, to: range.lowerBound) } return nil } func nsrangeToIndexRange(_ nsrange: NSRange) -> Range? { guard nsrange.location != NSNotFound else { return nil } let from16 = utf16.index(utf16.startIndex, offsetBy: nsrange.location, limitedBy: utf16.endIndex) ?? utf16.endIndex let to16 = utf16.index(from16, offsetBy: nsrange.length, limitedBy: utf16.endIndex) ?? utf16.endIndex guard let fromIndex = Index(from16, within: self), let toIndex = Index(to16, within: self) else { return nil } return fromIndex.. Important: This method might use an incorrect working directory internally. This can cause test failures /// in Bazel builds but does not seem to cause trouble in production. /// /// - returns: A new `String`. func absolutePathStandardized() -> String { URL(fileURLWithPath: bridge().standardizingPath.absolutePathRepresentation()).filepath } var isFile: Bool { if isEmpty { return false } var isDirectoryObjC: ObjCBool = false if FileManager.default.fileExists(atPath: self, isDirectory: &isDirectoryObjC) { return !isDirectoryObjC.boolValue } return false } /// Count the number of occurrences of the given character in `self` /// - Parameter character: Character to count /// - Returns: Number of times `character` occurs in `self` func countOccurrences(of character: Character) -> Int { reduce(0) { $1 == character ? $0 + 1 : $0 } } /// If self is a path, this method can be used to get a path expression relative to a root directory func path(relativeTo rootDirectory: String) -> String { let normalizedRootDir = rootDirectory.bridge().standardizingPath let normalizedSelf = bridge().standardizingPath if normalizedRootDir.isEmpty { return normalizedSelf } var rootDirComps = normalizedRootDir.components(separatedBy: "/") let rootDirCompsCount = rootDirComps.count while true { let sharedRootDir = rootDirComps.joined(separator: "/") if normalizedSelf == sharedRootDir || normalizedSelf.hasPrefix(sharedRootDir + "/") { let path = (0 ..< rootDirCompsCount - rootDirComps.count).map { _ in "/.." }.flatMap(\.self) + String(normalizedSelf.dropFirst(sharedRootDir.count)) return String(path.dropFirst()) // Remove leading '/' } rootDirComps = rootDirComps.dropLast() } } func deletingPrefix(_ prefix: String) -> String { guard hasPrefix(prefix) else { return self } return String(dropFirst(prefix.count)) } func indent(by spaces: Int, skipFirst: Bool = false, skipEmptyLines: Bool = true) -> String { let lines = components(separatedBy: "\n") if skipFirst, let firstLine = lines.first { return firstLine + "\n" + lines.dropFirst().indent(by: spaces, skipEmptyLines: skipEmptyLines) } return lines.indent(by: spaces, skipEmptyLines: skipEmptyLines) } func linesPrefixed(with prefix: Self) -> Self { split(separator: "\n").joined(separator: "\n\(prefix)") } func characterPosition(of utf8Offset: Int) -> Int? { guard utf8Offset != 0 else { return 0 } guard utf8Offset > 0, utf8Offset < lengthOfBytes(using: .utf8) else { return nil } for (offset, index) in indices.enumerated() where self[...index].lengthOfBytes(using: .utf8) == utf8Offset { return offset + 1 } return nil } } private extension Sequence where Element == String { func indent(by spaces: Int, skipEmptyLines: Bool = true) -> String { map { line in if skipEmptyLines, line.isEmpty { return line } return String(repeating: " ", count: spaces) + line } .joined(separator: "\n") } }