forked from toolshed/abra
		
	We were running behind and there were quite some deprecations to update. This was mostly in the upstream copy/pasta package but seems quite minimal.
		
			
				
	
	
		
			302 lines
		
	
	
		
			6.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			302 lines
		
	
	
		
			6.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package cellbuf
 | |
| 
 | |
| import (
 | |
| 	"github.com/charmbracelet/x/ansi"
 | |
| )
 | |
| 
 | |
| // hash returns the hash value of a [Line].
 | |
| func hash(l Line) (h uint64) {
 | |
| 	for _, c := range l {
 | |
| 		var r rune
 | |
| 		if c == nil {
 | |
| 			r = ansi.SP
 | |
| 		} else {
 | |
| 			r = c.Rune
 | |
| 		}
 | |
| 		h += (h << 5) + uint64(r)
 | |
| 	}
 | |
| 	return
 | |
| }
 | |
| 
 | |
| // hashmap represents a single [Line] hash.
 | |
| type hashmap struct {
 | |
| 	value              uint64
 | |
| 	oldcount, newcount int
 | |
| 	oldindex, newindex int
 | |
| }
 | |
| 
 | |
| // The value used to indicate lines created by insertions and scrolls.
 | |
| const newIndex = -1
 | |
| 
 | |
| // updateHashmap updates the hashmap with the new hash value.
 | |
| func (s *Screen) updateHashmap() {
 | |
| 	height := s.newbuf.Height()
 | |
| 	if len(s.oldhash) >= height && len(s.newhash) >= height {
 | |
| 		// rehash changed lines
 | |
| 		for i := 0; i < height; i++ {
 | |
| 			_, ok := s.touch[i]
 | |
| 			if ok {
 | |
| 				s.oldhash[i] = hash(s.curbuf.Line(i))
 | |
| 				s.newhash[i] = hash(s.newbuf.Line(i))
 | |
| 			}
 | |
| 		}
 | |
| 	} else {
 | |
| 		// rehash all
 | |
| 		if len(s.oldhash) != height {
 | |
| 			s.oldhash = make([]uint64, height)
 | |
| 		}
 | |
| 		if len(s.newhash) != height {
 | |
| 			s.newhash = make([]uint64, height)
 | |
| 		}
 | |
| 		for i := 0; i < height; i++ {
 | |
| 			s.oldhash[i] = hash(s.curbuf.Line(i))
 | |
| 			s.newhash[i] = hash(s.newbuf.Line(i))
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	s.hashtab = make([]hashmap, height*2)
 | |
| 	for i := 0; i < height; i++ {
 | |
| 		hashval := s.oldhash[i]
 | |
| 
 | |
| 		// Find matching hash or empty slot
 | |
| 		idx := 0
 | |
| 		for idx < len(s.hashtab) && s.hashtab[idx].value != 0 {
 | |
| 			if s.hashtab[idx].value == hashval {
 | |
| 				break
 | |
| 			}
 | |
| 			idx++
 | |
| 		}
 | |
| 
 | |
| 		s.hashtab[idx].value = hashval // in case this is a new hash
 | |
| 		s.hashtab[idx].oldcount++
 | |
| 		s.hashtab[idx].oldindex = i
 | |
| 	}
 | |
| 	for i := 0; i < height; i++ {
 | |
| 		hashval := s.newhash[i]
 | |
| 
 | |
| 		// Find matching hash or empty slot
 | |
| 		idx := 0
 | |
| 		for idx < len(s.hashtab) && s.hashtab[idx].value != 0 {
 | |
| 			if s.hashtab[idx].value == hashval {
 | |
| 				break
 | |
| 			}
 | |
| 			idx++
 | |
| 		}
 | |
| 
 | |
| 		s.hashtab[idx].value = hashval // in case this is a new hash
 | |
| 		s.hashtab[idx].newcount++
 | |
| 		s.hashtab[idx].newindex = i
 | |
| 
 | |
| 		s.oldnum[i] = newIndex // init old indices slice
 | |
| 	}
 | |
| 
 | |
| 	// Mark line pair corresponding to unique hash pairs.
 | |
| 	for i := 0; i < len(s.hashtab) && s.hashtab[i].value != 0; i++ {
 | |
| 		hsp := &s.hashtab[i]
 | |
| 		if hsp.oldcount == 1 && hsp.newcount == 1 && hsp.oldindex != hsp.newindex {
 | |
| 			s.oldnum[hsp.newindex] = hsp.oldindex
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	s.growHunks()
 | |
| 
 | |
| 	// Eliminate bad or impossible shifts. This includes removing those hunks
 | |
| 	// which could not grow because of conflicts, as well those which are to be
 | |
| 	// moved too far, they are likely to destroy more than carry.
 | |
| 	for i := 0; i < height; {
 | |
| 		var start, shift, size int
 | |
| 		for i < height && s.oldnum[i] == newIndex {
 | |
| 			i++
 | |
| 		}
 | |
| 		if i >= height {
 | |
| 			break
 | |
| 		}
 | |
| 		start = i
 | |
| 		shift = s.oldnum[i] - i
 | |
| 		i++
 | |
| 		for i < height && s.oldnum[i] != newIndex && s.oldnum[i]-i == shift {
 | |
| 			i++
 | |
| 		}
 | |
| 		size = i - start
 | |
| 		if size < 3 || size+min(size/8, 2) < abs(shift) {
 | |
| 			for start < i {
 | |
| 				s.oldnum[start] = newIndex
 | |
| 				start++
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// After clearing invalid hunks, try grow the rest.
 | |
| 	s.growHunks()
 | |
| }
 | |
| 
 | |
| // scrollOldhash
 | |
| func (s *Screen) scrollOldhash(n, top, bot int) {
 | |
| 	if len(s.oldhash) == 0 {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	size := bot - top + 1 - abs(n)
 | |
| 	if n > 0 {
 | |
| 		// Move existing hashes up
 | |
| 		copy(s.oldhash[top:], s.oldhash[top+n:top+n+size])
 | |
| 		// Recalculate hashes for newly shifted-in lines
 | |
| 		for i := bot; i > bot-n; i-- {
 | |
| 			s.oldhash[i] = hash(s.curbuf.Line(i))
 | |
| 		}
 | |
| 	} else {
 | |
| 		// Move existing hashes down
 | |
| 		copy(s.oldhash[top-n:], s.oldhash[top:top+size])
 | |
| 		// Recalculate hashes for newly shifted-in lines
 | |
| 		for i := top; i < top-n; i++ {
 | |
| 			s.oldhash[i] = hash(s.curbuf.Line(i))
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (s *Screen) growHunks() {
 | |
| 	var (
 | |
| 		backLimit    int // limits for cells to fill
 | |
| 		backRefLimit int // limit for references
 | |
| 		i            int
 | |
| 		nextHunk     int
 | |
| 	)
 | |
| 
 | |
| 	height := s.newbuf.Height()
 | |
| 	for i < height && s.oldnum[i] == newIndex {
 | |
| 		i++
 | |
| 	}
 | |
| 	for ; i < height; i = nextHunk {
 | |
| 		var (
 | |
| 			forwardLimit    int
 | |
| 			forwardRefLimit int
 | |
| 			end             int
 | |
| 			start           = i
 | |
| 			shift           = s.oldnum[i] - i
 | |
| 		)
 | |
| 
 | |
| 		// get forward limit
 | |
| 		i = start + 1
 | |
| 		for i < height &&
 | |
| 			s.oldnum[i] != newIndex &&
 | |
| 			s.oldnum[i]-i == shift {
 | |
| 			i++
 | |
| 		}
 | |
| 
 | |
| 		end = i
 | |
| 		for i < height && s.oldnum[i] == newIndex {
 | |
| 			i++
 | |
| 		}
 | |
| 
 | |
| 		nextHunk = i
 | |
| 		forwardLimit = i
 | |
| 		if i >= height || s.oldnum[i] >= i {
 | |
| 			forwardRefLimit = i
 | |
| 		} else {
 | |
| 			forwardRefLimit = s.oldnum[i]
 | |
| 		}
 | |
| 
 | |
| 		i = start - 1
 | |
| 
 | |
| 		// grow back
 | |
| 		if shift < 0 {
 | |
| 			backLimit = backRefLimit + (-shift)
 | |
| 		}
 | |
| 		for i >= backLimit {
 | |
| 			if s.newhash[i] == s.oldhash[i+shift] ||
 | |
| 				s.costEffective(i+shift, i, shift < 0) {
 | |
| 				s.oldnum[i] = i + shift
 | |
| 			} else {
 | |
| 				break
 | |
| 			}
 | |
| 			i--
 | |
| 		}
 | |
| 
 | |
| 		i = end
 | |
| 		// grow forward
 | |
| 		if shift > 0 {
 | |
| 			forwardLimit = forwardRefLimit - shift
 | |
| 		}
 | |
| 		for i < forwardLimit {
 | |
| 			if s.newhash[i] == s.oldhash[i+shift] ||
 | |
| 				s.costEffective(i+shift, i, shift > 0) {
 | |
| 				s.oldnum[i] = i + shift
 | |
| 			} else {
 | |
| 				break
 | |
| 			}
 | |
| 			i++
 | |
| 		}
 | |
| 
 | |
| 		backLimit = i
 | |
| 		backRefLimit = backLimit
 | |
| 		if shift > 0 {
 | |
| 			backRefLimit += shift
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // costEffective returns true if the cost of moving line 'from' to line 'to' seems to be
 | |
| // cost effective. 'blank' indicates whether the line 'to' would become blank.
 | |
| func (s *Screen) costEffective(from, to int, blank bool) bool {
 | |
| 	if from == to {
 | |
| 		return false
 | |
| 	}
 | |
| 
 | |
| 	newFrom := s.oldnum[from]
 | |
| 	if newFrom == newIndex {
 | |
| 		newFrom = from
 | |
| 	}
 | |
| 
 | |
| 	// On the left side of >= is the cost before moving. On the right side --
 | |
| 	// cost after moving.
 | |
| 
 | |
| 	// Calculate costs before moving.
 | |
| 	var costBeforeMove int
 | |
| 	if blank {
 | |
| 		// Cost of updating blank line at destination.
 | |
| 		costBeforeMove = s.updateCostBlank(s.newbuf.Line(to))
 | |
| 	} else {
 | |
| 		// Cost of updating exiting line at destination.
 | |
| 		costBeforeMove = s.updateCost(s.curbuf.Line(to), s.newbuf.Line(to))
 | |
| 	}
 | |
| 
 | |
| 	// Add cost of updating source line
 | |
| 	costBeforeMove += s.updateCost(s.curbuf.Line(newFrom), s.newbuf.Line(from))
 | |
| 
 | |
| 	// Calculate costs after moving.
 | |
| 	var costAfterMove int
 | |
| 	if newFrom == from {
 | |
| 		// Source becomes blank after move
 | |
| 		costAfterMove = s.updateCostBlank(s.newbuf.Line(from))
 | |
| 	} else {
 | |
| 		// Source gets updated from another line
 | |
| 		costAfterMove = s.updateCost(s.curbuf.Line(newFrom), s.newbuf.Line(from))
 | |
| 	}
 | |
| 
 | |
| 	// Add cost of moving source line to destination
 | |
| 	costAfterMove += s.updateCost(s.curbuf.Line(from), s.newbuf.Line(to))
 | |
| 
 | |
| 	// Return true if moving is cost effective (costs less or equal)
 | |
| 	return costBeforeMove >= costAfterMove
 | |
| }
 | |
| 
 | |
| func (s *Screen) updateCost(from, to Line) (cost int) {
 | |
| 	var fidx, tidx int
 | |
| 	for i := s.newbuf.Width() - 1; i > 0; i, fidx, tidx = i-1, fidx+1, tidx+1 {
 | |
| 		if !cellEqual(from.At(fidx), to.At(tidx)) {
 | |
| 			cost++
 | |
| 		}
 | |
| 	}
 | |
| 	return
 | |
| }
 | |
| 
 | |
| func (s *Screen) updateCostBlank(to Line) (cost int) {
 | |
| 	var tidx int
 | |
| 	for i := s.newbuf.Width() - 1; i > 0; i, tidx = i-1, tidx+1 {
 | |
| 		if !cellEqual(nil, to.At(tidx)) {
 | |
| 			cost++
 | |
| 		}
 | |
| 	}
 | |
| 	return
 | |
| }
 |