0

I want to implement the following feature: In my NSTextView subclass I listen on option + arrow up and option + arrow down. Whenever these shortcuts are triggered the textviews selection should expand/collapse in the following manner:

  • Cursor
  • Current Word under cursor
  • Text under cursor up to white space
  • Current Sentence under cursor
  • Current Paragraph under cursor
  • Whole TextView content

My implementation seems to work however whenever I expand the selection I "overwrite" the cursor position and therefore I can't collapse the selection correctly again. Here is an example of what I want to achieve: https://caret.io/feat/feat-extend.gif

My current solution relies on finding the next bigger selection from the current selection and looks like this:

private func selectNextTextBlock() {
        let location = insertionLocation ?? selectedRange().location
        let wr = wordRange(at: location)
        let wgr = wordGroupRange(at: location)
        let sr = sentenceRange(at: location)
        let pr = paragraphRange(at: location)

        if self.selectedRange().length == 0 {
            self.setSelectedRangesWithUndo([wr])
        } else if self.selectedRange().length >= wr.length && self.selectedRange().length < wgr.length {
            self.setSelectedRangesWithUndo([wgr])
        } else if self.selectedRange().length >= wgr.length && self.selectedRange().length < sr.length {
            self.setSelectedRangesWithUndo([sr])
        } else if self.selectedRange().length >= sr.length && self.selectedRange().length < pr.length {
            setSelectedRangesWithUndo([pr])
        } else {
            setSelectedRangesWithUndo([NSRange(location: 0, length: string.count)])
        }
    }

    private func selectPreviousTextBlock() {
        #warning("implement?")
    }

    private func wordRange(at location: Int) -> NSRange {
        let proposedWordRange = super.selectionRange(forProposedRange: NSRange(location: location, length: 0), granularity: .selectByWord)
        guard proposedWordRange.contains(location) else { return proposedWordRange }
        // treat `.` and `:` as word delimiter
        return (self.string as NSString).rangeOfCharacter(until: CharacterSet(charactersIn: ".:"), at: location, range: proposedWordRange)
    }

    private func wordGroupRange(at location: Int) -> NSRange {
        let proposedWordRange = super.selectionRange(forProposedRange: NSRange(location: location, length: 0), granularity: .selectByParagraph)
        guard proposedWordRange.contains(location) else { return proposedWordRange }
        // treat `whitespaces and newlines` as word delimiter
        return (self.string as NSString).rangeOfCharacter(until: CharacterSet.whitespacesAndNewlines, at: location, range: proposedWordRange)
    }

    private func sentenceRange(at location: Int) -> NSRange {
        let proposedWordRange = super.selectionRange(forProposedRange: NSRange(location: location, length: 0), granularity: .selectByParagraph)
        guard proposedWordRange.contains(location) else { return proposedWordRange }
        // treat `.` and `:` as paragraph delimiter
        var range = (self.string as NSString).rangeOfCharacter(until: CharacterSet(charactersIn: ".:"), at: location, range: proposedWordRange)
        range.length += 1
        return range
    }

    private func paragraphRange(at location: Int) -> NSRange {
        return super.selectionRange(forProposedRange: NSRange(location: location, length: 0), granularity: .selectByParagraph)
    }

Any hints for me on how to implement selectPreviousTextBlock? Since the user can change the cursor position manually I can't guarantee to save the correct latest cursor position in selectNextTextBlock

dehlen
  • 7,325
  • 4
  • 43
  • 71
  • 1
    Save the previous selection ranges for insertion point, word, sentence etc. and discard them when the user changes the selection. – Willeke Apr 17 '20 at 12:52
  • Yes I thought of this initially but could not get It to work since the user could preselect a certain selection and therefore there is no insertion point. However I just use the selectedRange location as the insertionPoint instead in this case and this seems to work fine for my use case. Thanks for your help. – dehlen Apr 17 '20 at 14:09

0 Answers0