github.com/aretext/aretext@v1.3.0/locate/character.go (about)

     1  package locate
     2  
     3  import (
     4  	"io"
     5  
     6  	"github.com/aretext/aretext/cellwidth"
     7  	"github.com/aretext/aretext/text"
     8  	"github.com/aretext/aretext/text/segment"
     9  )
    10  
    11  // NextCharInLine locates the next grapheme cluster in the current line.
    12  func NextCharInLine(tree *text.Tree, count uint64, includeEndOfLineOrFile bool, pos uint64) uint64 {
    13  	reader := tree.ReaderAtPosition(pos)
    14  	segmentIter := segment.NewGraphemeClusterIter(reader)
    15  	seg := segment.Empty()
    16  	var endOfLineOrFile bool
    17  	var prevPrevOffset, prevOffset uint64
    18  	for i := uint64(0); i <= count; i++ {
    19  		err := segmentIter.NextSegment(seg)
    20  		if err == io.EOF {
    21  			endOfLineOrFile = true
    22  			break
    23  		} else if err != nil {
    24  			panic(err)
    25  		}
    26  
    27  		if seg.HasNewline() {
    28  			endOfLineOrFile = true
    29  			break
    30  		}
    31  		prevPrevOffset = prevOffset
    32  		prevOffset += seg.NumRunes()
    33  	}
    34  	if endOfLineOrFile && includeEndOfLineOrFile {
    35  		return pos + prevOffset
    36  	}
    37  	return pos + prevPrevOffset
    38  }
    39  
    40  // PrevCharInLine locates the previous grapheme cluster in the current line.
    41  func PrevCharInLine(tree *text.Tree, count uint64, includeEndOfLineOrFile bool, pos uint64) uint64 {
    42  	reader := tree.ReverseReaderAtPosition(pos)
    43  	segmentIter := segment.NewReverseGraphemeClusterIter(reader)
    44  	seg := segment.Empty()
    45  	var offset uint64
    46  	for i := uint64(0); i < count; i++ {
    47  		err := segmentIter.NextSegment(seg)
    48  		if err == io.EOF {
    49  			break
    50  		} else if err != nil {
    51  			panic(err)
    52  		}
    53  		if offset+seg.NumRunes() > pos {
    54  			return 0
    55  		}
    56  		if seg.HasNewline() {
    57  			if includeEndOfLineOrFile {
    58  				offset += seg.NumRunes()
    59  			}
    60  			break
    61  		}
    62  		offset += seg.NumRunes()
    63  	}
    64  	return pos - offset
    65  }
    66  
    67  // PrevChar locates the grapheme cluster before a position, which may be on a previous line.
    68  func PrevChar(tree *text.Tree, count uint64, pos uint64) uint64 {
    69  	reader := tree.ReverseReaderAtPosition(pos)
    70  	iter := segment.NewReverseGraphemeClusterIter(reader)
    71  	seg := segment.Empty()
    72  	for i := uint64(0); i < count; i++ {
    73  		err := iter.NextSegment(seg)
    74  		if err == io.EOF {
    75  			break
    76  		} else if err != nil {
    77  			panic(err)
    78  		}
    79  		pos -= seg.NumRunes()
    80  	}
    81  	return pos
    82  }
    83  
    84  // NextMatchingCharInLine locates the count'th next occurrence of a rune in the line.
    85  func NextMatchingCharInLine(tree *text.Tree, char rune, count uint64, includeChar bool, pos uint64) (bool, uint64) {
    86  	var matchCount uint64
    87  	var offset, prevOffset uint64
    88  	reader := tree.ReaderAtPosition(pos)
    89  	segmentIter := segment.NewGraphemeClusterIter(reader)
    90  	seg := segment.Empty()
    91  	for {
    92  		err := segmentIter.NextSegment(seg)
    93  		if err == io.EOF || (err == nil && seg.HasNewline()) {
    94  			// No match found before end of line or file.
    95  			return false, 0
    96  		} else if err != nil {
    97  			panic(err)
    98  		}
    99  
   100  		if offset > 0 {
   101  			for _, r := range seg.Runes() {
   102  				if r == char {
   103  					matchCount++
   104  					if matchCount == count {
   105  						if includeChar {
   106  							return true, pos + offset
   107  						} else {
   108  							return true, pos + prevOffset
   109  						}
   110  					}
   111  				}
   112  			}
   113  		}
   114  
   115  		prevOffset = offset
   116  		offset += seg.NumRunes()
   117  	}
   118  }
   119  
   120  // PrevMatchingCharInLine locates the count'th previous occurrence of a rune in the line.
   121  func PrevMatchingCharInLine(tree *text.Tree, char rune, count uint64, includeChar bool, pos uint64) (bool, uint64) {
   122  	var matchCount uint64
   123  	var offset, prevOffset uint64
   124  	reader := tree.ReverseReaderAtPosition(pos)
   125  	segmentIter := segment.NewReverseGraphemeClusterIter(reader)
   126  	seg := segment.Empty()
   127  	for {
   128  		err := segmentIter.NextSegment(seg)
   129  		if err == io.EOF || (err == nil && seg.HasNewline()) {
   130  			// No match found before end of line or file.
   131  			return false, 0
   132  		} else if err != nil {
   133  			panic(err)
   134  		}
   135  
   136  		prevOffset = offset
   137  		offset += seg.NumRunes()
   138  
   139  		for _, r := range seg.Runes() {
   140  			if r == char {
   141  				matchCount++
   142  				if matchCount == count {
   143  					if includeChar {
   144  						return true, pos - offset
   145  					} else {
   146  						return true, pos - prevOffset
   147  					}
   148  				}
   149  			}
   150  		}
   151  	}
   152  }
   153  
   154  // PrevAutoIndent locates the previous tab stop if autoIndent is enabled.
   155  // If autoIndent is disabled or the characters before the cursor are not spaces/tabs, it returns the original position.
   156  func PrevAutoIndent(tree *text.Tree, autoIndentEnabled bool, tabSize uint64, pos uint64) uint64 {
   157  	if !autoIndentEnabled {
   158  		return pos
   159  	}
   160  
   161  	prevTabAlignedPos := findPrevTabAlignedPos(tree, tabSize, pos)
   162  	prevWhitespaceStartPos := findPrevWhitespaceStartPos(tree, tabSize, pos)
   163  	if prevTabAlignedPos < prevWhitespaceStartPos {
   164  		return prevWhitespaceStartPos
   165  	} else {
   166  		return prevTabAlignedPos
   167  	}
   168  }
   169  
   170  func findPrevTabAlignedPos(tree *text.Tree, tabSize uint64, startPos uint64) uint64 {
   171  	pos := StartOfLineAtPos(tree, startPos)
   172  	reader := tree.ReaderAtPosition(pos)
   173  	iter := segment.NewGraphemeClusterIter(reader)
   174  	seg := segment.Empty()
   175  	var offset uint64
   176  	lastAlignedPos := pos
   177  	for pos < startPos {
   178  		if offset%tabSize == 0 {
   179  			lastAlignedPos = pos
   180  		}
   181  		err := iter.NextSegment(seg)
   182  		if err == io.EOF {
   183  			break
   184  		} else if err != nil {
   185  			panic(err)
   186  		}
   187  		offset += cellwidth.GraphemeClusterWidth(seg.Runes(), offset, tabSize)
   188  		pos += seg.NumRunes()
   189  	}
   190  	return lastAlignedPos
   191  }
   192  
   193  func findPrevWhitespaceStartPos(tree *text.Tree, tabSize uint64, pos uint64) uint64 {
   194  	reader := tree.ReverseReaderAtPosition(pos)
   195  	iter := segment.NewReverseGraphemeClusterIter(reader)
   196  	seg := segment.Empty()
   197  	for {
   198  		err := iter.NextSegment(seg)
   199  		if err == io.EOF {
   200  			break
   201  		} else if err != nil {
   202  			panic(err)
   203  		}
   204  
   205  		r := seg.Runes()[0]
   206  		if r != ' ' && r != '\t' {
   207  			break
   208  		}
   209  		pos -= seg.NumRunes()
   210  	}
   211  	return pos
   212  }
   213  
   214  // NonWhitespaceOrNewline locates the next non-whitespace character or newline on or after a position.
   215  func NextNonWhitespaceOrNewline(tree *text.Tree, pos uint64) uint64 {
   216  	reader := tree.ReaderAtPosition(pos)
   217  	segmentIter := segment.NewGraphemeClusterIter(reader)
   218  	seg := segment.Empty()
   219  	var offset uint64
   220  	for {
   221  		err := segmentIter.NextSegment(seg)
   222  		if err == io.EOF || (err == nil && (!seg.IsWhitespace() || seg.HasNewline())) {
   223  			break
   224  		} else if err != nil {
   225  			panic(err)
   226  		}
   227  		offset += seg.NumRunes()
   228  	}
   229  	return pos + offset
   230  }
   231  
   232  // NextNewline locates the next newline on or after the specified position.
   233  // It returns both the positon of the newline as well as its length in runes,
   234  // since the grapheme cluster could be either '\n' or '\r\n'.
   235  func NextNewline(tree *text.Tree, pos uint64) (uint64, uint64, bool) {
   236  	reader := tree.ReaderAtPosition(pos)
   237  	segmentIter := segment.NewGraphemeClusterIter(reader)
   238  	seg := segment.Empty()
   239  	var offset uint64
   240  	for {
   241  		err := segmentIter.NextSegment(seg)
   242  		if err == io.EOF {
   243  			return 0, 0, false
   244  		} else if err != nil {
   245  			panic(err)
   246  		} else if seg.HasNewline() {
   247  			return pos + offset, seg.NumRunes(), true
   248  		}
   249  		offset += seg.NumRunes()
   250  	}
   251  }
   252  
   253  // NumGraphemeClustersInRange counts the number of grapheme clusters from the start position (inclusive) to the end position (exclusive).
   254  func NumGraphemeClustersInRange(tree *text.Tree, startPos, endPos uint64) uint64 {
   255  	reader := tree.ReaderAtPosition(startPos)
   256  	segmentIter := segment.NewGraphemeClusterIter(reader)
   257  	seg := segment.Empty()
   258  	var offset, count uint64
   259  	for startPos+offset < endPos {
   260  		err := segmentIter.NextSegment(seg)
   261  		if err == io.EOF || (err == nil && seg.HasNewline()) {
   262  			break
   263  		} else if err != nil {
   264  			panic(err)
   265  		}
   266  		count++
   267  		offset += seg.NumRunes()
   268  	}
   269  	return count
   270  
   271  }