okami.mom/ultraviolet
git clone https://git.nkp.pet/okami.mom/ultraviolet
6d7982b
upstream
1package uv 2 3import "hash/maphash" 4 5// hash returns the hash value of a [Line]. 6func hash (h * maphash.Hash ,l Line )uint64 { 7h .Reset () 8for _ ,c := range l { 9// maphash writes can not fail 10_ ,_ = h .WriteString (c .Content ) 11 } 12 13return h .Sum64 () 14} 15 16// hashmap represents a single [Line] hash. 17type hashmap struct { 18value uint64 19oldcount ,newcount int 20oldindex ,newindex int 21} 22 23// The value used to indicate lines created by insertions and scrolls. 24const newIndex = - 1 25 26// updateHashmap updates the hashmap with the new hash value. 27func (s * TerminalRenderer )updateHashmap (newbuf * RenderBuffer ) { 28height := newbuf .Height () 29 30if len (s .oldhash )>= height && len (s .newhash )>= height { 31// rehash changed lines 32for i := range height { 33if newbuf .Touched [i ]!= nil { 34// TODO: Investigate why this is needed. If we remove this 35// line, scroll optimization does not work correctly. This 36// should happen else where. 37s .oldhash [i ]= hash (& s .hasher ,s .curbuf .Line (i )) 38s .newhash [i ]= hash (& s .hasher ,newbuf .Line (i )) 39 } 40 } 41 }else { 42// rehash all 43if len (s .oldhash )!= height { 44s .oldhash = make ([]uint64 ,height ) 45 } 46if len (s .newhash )!= height { 47s .newhash = make ([]uint64 ,height ) 48 } 49for i := range height { 50s .oldhash [i ]= hash (& s .hasher ,s .curbuf .Line (i )) 51s .newhash [i ]= hash (& s .hasher ,newbuf .Line (i )) 52 } 53 } 54 55s .hashtab = make ([]hashmap , (height + 1 )* 2 ) 56for i := range height { 57hashval := s .oldhash [i ] 58 59// Find matching hash or empty slot 60var idx int 61for idx = 0 ;idx < len (s .hashtab )&& s .hashtab [idx ].value != 0 ;idx ++ { 62if s .hashtab [idx ].value == hashval { 63break 64 } 65 } 66 67s .hashtab [idx ].value = hashval // in case this is a new hash 68s .hashtab [idx ].oldcount ++ 69s .hashtab [idx ].oldindex = i 70 } 71for i := range height { 72hashval := s .newhash [i ] 73 74// Find matching hash or empty slot 75var idx int 76for idx = 0 ;idx < len (s .hashtab )&& s .hashtab [idx ].value != 0 ;idx ++ { 77if s .hashtab [idx ].value == hashval { 78break 79 } 80 } 81 82s .hashtab [idx ].value = hashval // in case this is a new hash 83s .hashtab [idx ].newcount ++ 84s .hashtab [idx ].newindex = i 85s .oldnum [i ]= newIndex // init old indices slice 86 } 87 88// Mark line pair corresponding to unique hash pairs. 89for i := 0 ;i < len (s .hashtab )&& s .hashtab [i ].value != 0 ;i ++ { 90hsp := & s .hashtab [i ] 91if hsp .oldcount == 1 && hsp .newcount == 1 && hsp .oldindex != hsp .newindex { 92s .oldnum [hsp .newindex ]= hsp .oldindex 93 } 94 } 95 96s .growHunks (newbuf ) 97 98// Eliminate bad or impossible shifts. This includes removing those hunks 99// which could not grow because of conflicts, as well those which are to be 100// moved too far, they are likely to destroy more than carry. 101for i := 0 ;i < height ; { 102var start ,shift ,size int 103for i < height && s .oldnum [i ]== newIndex { 104i ++ 105 } 106if i >= height { 107break 108 } 109start = i 110shift = s .oldnum [i ]- i 111i ++ 112for i < height && s .oldnum [i ]!= newIndex && s .oldnum [i ]- i == shift { 113i ++ 114 } 115size = i - start 116if size < 3 || size + min (size / 8 ,2 )< abs (shift ) { 117for start < i { 118s .oldnum [start ]= newIndex 119start ++ 120 } 121 } 122 } 123 124// After clearing invalid hunks, try grow the rest. 125s .growHunks (newbuf ) 126} 127 128// scrollOldhash scrolls the oldhash slice by 'n' lines between 'top' and 'bot'. 129func (s * TerminalRenderer )scrollOldhash (n ,top ,bot int ) { 130if len (s .oldhash )== 0 { 131return 132 } 133 134size := bot - top + 1 - abs (n ) 135if n > 0 { 136// Move existing hashes up 137copy (s .oldhash [top :],s .oldhash [top + n :top + n + size ]) 138// Recalculate hashes for newly shifted-in lines 139for i := bot ;i > bot - n ;i -- { 140s .oldhash [i ]= hash (& s .hasher ,s .curbuf .Line (i )) 141 } 142 }else { 143// Move existing hashes down 144copy (s .oldhash [top - n :],s .oldhash [top :top + size ]) 145// Recalculate hashes for newly shifted-in lines 146for i := top ;i < top - n ;i ++ { 147s .oldhash [i ]= hash (& s .hasher ,s .curbuf .Line (i )) 148 } 149 } 150} 151 152func (s * TerminalRenderer )growHunks (newbuf * RenderBuffer ) { 153var ( 154backLimit int // limits for cells to fill 155backRefLimit int // limit for references 156i int 157nextHunk int 158 ) 159 160height := newbuf .Height () 161for i < height && s .oldnum [i ]== newIndex { 162i ++ 163 } 164for ;i < height ;i = nextHunk { 165var ( 166forwardLimit int 167forwardRefLimit int 168end int 169start = i 170shift = s .oldnum [i ]- i 171 ) 172 173// get forward limit 174i = start + 1 175for i < height && 176s .oldnum [i ]!= newIndex && 177s .oldnum [i ]- i == shift { 178i ++ 179 } 180 181end = i 182for i < height && s .oldnum [i ]== newIndex { 183i ++ 184 } 185 186nextHunk = i 187forwardLimit = i 188if i >= height || s .oldnum [i ]>= i { 189forwardRefLimit = i 190 }else { 191forwardRefLimit = s .oldnum [i ] 192 } 193 194i = start - 1 195 196// grow back 197if shift < 0 { 198backLimit = backRefLimit + (- shift ) 199 } 200for i >= backLimit { 201if s .newhash [i ]== s .oldhash [i + shift ]|| 202s .costEffective (newbuf ,i + shift ,i ,shift < 0 ) { 203s .oldnum [i ]= i + shift 204 }else { 205break 206 } 207i -- 208 } 209 210i = end 211// grow forward 212if shift > 0 { 213forwardLimit = forwardRefLimit - shift 214 } 215for i < forwardLimit { 216if s .newhash [i ]== s .oldhash [i + shift ]|| 217s .costEffective (newbuf ,i + shift ,i ,shift > 0 ) { 218s .oldnum [i ]= i + shift 219 }else { 220break 221 } 222i ++ 223 } 224 225backLimit = i 226backRefLimit = backLimit 227if shift > 0 { 228backRefLimit += shift 229 } 230 } 231} 232 233// costEffective returns true if the cost of moving line 'from' to line 'to' seems to be 234// cost effective. 'blank' indicates whether the line 'to' would become blank. 235func (s * TerminalRenderer )costEffective (newbuf * RenderBuffer ,from ,to int ,blank bool )bool { 236if from == to { 237return false 238 } 239 240newFrom := s .oldnum [from ] 241if newFrom == newIndex { 242newFrom = from 243 } 244 245// On the left side of >= is the cost before moving. On the right side -- 246// cost after moving. 247 248// Calculate costs before moving. 249var costBeforeMove int 250if blank { 251// Cost of updating blank line at destination. 252costBeforeMove = s .updateCostBlank (newbuf ,newbuf .Line (to )) 253 }else { 254// Cost of updating exiting line at destination. 255costBeforeMove = s .updateCost (newbuf ,s .curbuf .Line (to ),newbuf .Line (to )) 256 } 257 258// Add cost of updating source line 259costBeforeMove += s .updateCost (newbuf ,s .curbuf .Line (newFrom ),newbuf .Line (from )) 260 261// Calculate costs after moving. 262var costAfterMove int 263if newFrom == from { 264// Source becomes blank after move 265costAfterMove = s .updateCostBlank (newbuf ,newbuf .Line (from )) 266 }else { 267// Source gets updated from another line 268costAfterMove = s .updateCost (newbuf ,s .curbuf .Line (newFrom ),newbuf .Line (from )) 269 } 270 271// Add cost of moving source line to destination 272costAfterMove += s .updateCost (newbuf ,s .curbuf .Line (from ),newbuf .Line (to )) 273 274// Return true if moving is cost effective (costs less or equal) 275return costBeforeMove >= costAfterMove 276} 277 278func (s * TerminalRenderer )updateCost (_ * RenderBuffer ,from ,to Line ) (cost int ) { 279var fidx ,tidx int 280for i := s .curbuf .Width ();i > 0 ;i ,fidx ,tidx = i - 1 ,fidx + 1 ,tidx + 1 { 281if ! cellEqual (from .At (fidx ),to .At (tidx )) { 282cost ++ 283 } 284 } 285return 286} 287 288func (s * TerminalRenderer )updateCostBlank (_ * RenderBuffer ,to Line ) (cost int ) { 289// This assumes bce capability. 290blank := s .clearBlank () 291var tidx int 292for i := s .curbuf .Width ();i > 0 ;i ,tidx = i - 1 ,tidx + 1 { 293if ! cellEqual (blank ,to .At (tidx )) { 294cost ++ 295 } 296 } 297return 298}