         
         guard let textStorage = self.textStorage else { return assertionFailure() }
         
+        let ranges = self.selectedRanges as! [NSRange]
+        
         // get line ranges to process
-        let lineRanges = self.selectedLineRanges
+        let lineRanges = (self.string as NSString).lineRanges(for: ranges)
         
         // cannot perform Move Line Up if one of the selections is already in the first line
         guard !lineRanges.isEmpty, lineRanges.first?.location != 0 else {

                lineString += "\n"
            }
            
            replacementRanges.append(replacementRange)
            replacementStrings.append(lineString)
        }
        
        self.replace(with: replacementStrings, ranges: replacementRanges, selectedRanges: nil,
                     actionName: "Duplicate Line".localized)
    }
    
    
    /// remove selected lines
    @IBAction func deleteLine(_ sender: Any?) {
        
        let replacementRanges = self.selectedLineRanges
        
        // on empty last line
        guard !replacementRanges.isEmpty else { return }
        
        let replacementStrings = [String](repeating: "", count: replacementRanges.count)
        
        self.replace(with: replacementStrings, ranges: replacementRanges, selectedRanges: nil,
                     actionName: "Delete Line".localized)
    }
    
    
    /// trim all trailing whitespace
    @IBAction func trimTrailingWhitespace(_ sender: Any?) {
        
        let trimsWhitespaceOnlyLines = UserDefaults.standard[.trimsWhitespaceOnlyLines]
        
        self.trimTrailingWhitespace(ignoresEmptyLines: !trimsWhitespaceOnlyLines)
    }
    
}


extension NSTextView {
    
    func sortLines(pattern: SortPattern, options: SortOptions) {
        
        let string = self.string as NSString
        
        // process whole document if no text selected
        if self.selectedRange.length == 0 {
            self.selectedRange = string.range
        }
        
        let lineRange = string.lineRange(for: self.selectedRange, excludingLastLineEnding: true)
        
        guard lineRange.length > 0 else { return }
        
        let newString = pattern.sort(string.substring(with: lineRange), options: options)
        
        self.replace(with: newString, range: lineRange, selectedRange: lineRange,
                     actionName: "Sort Lines".localized)
    }
    
}



// MARK: Private NSTextView Extension

private extension NSTextView {
    
    /// extract line by line line ranges which selected ranges include
    var selectedLineRanges: [NSRange] {
        
        return (self.string as NSString).lineRanges(for: self.selectedRanges as! [NSRange])
    }
    
}

            self.didChangeText()
            
            // move selected ranges in the line to move
            for selectedRange in ranges {
                if let intersectionRange = selectedRange.intersection(editRange) {
                    selectedRanges.append(NSRange(location: intersectionRange.location - upperLineRange.length,
                                                  length: intersectionRange.length))
                    
                } else if editRange.contains(selectedRange.location) || selectedRange.upperBound == editRange.upperBound {
                    selectedRanges.append(NSRange(location: selectedRange.location - upperLineRange.length,
                                                  length: selectedRange.length))
                }
            }
        }
        textStorage.endEditing()
        
        self.setSelectedRangesWithUndo(selectedRanges)
        
        self.undoManager?.setActionName("Move Line".localized)
    }
