1package buffer 2 3import ( 4 "bufio" 5 "bytes" 6 "crypto/md5" 7 "errors" 8 "fmt" 9 "io" 10 "io/ioutil" 11 "os" 12 "path" 13 "path/filepath" 14 "strconv" 15 "strings" 16 "sync" 17 "sync/atomic" 18 "time" 19 20 luar "layeh.com/gopher-luar" 21 22 dmp "github.com/sergi/go-diff/diffmatchpatch" 23 "github.com/zyedidia/micro/v2/internal/config" 24 ulua "github.com/zyedidia/micro/v2/internal/lua" 25 "github.com/zyedidia/micro/v2/internal/screen" 26 "github.com/zyedidia/micro/v2/internal/util" 27 "github.com/zyedidia/micro/v2/pkg/highlight" 28 "golang.org/x/text/encoding/htmlindex" 29 "golang.org/x/text/encoding/unicode" 30 "golang.org/x/text/transform" 31) 32 33const backupTime = 8000 34 35var ( 36 // OpenBuffers is a list of the currently open buffers 37 OpenBuffers []*Buffer 38 // LogBuf is a reference to the log buffer which can be opened with the 39 // `> log` command 40 LogBuf *Buffer 41) 42 43// The BufType defines what kind of buffer this is 44type BufType struct { 45 Kind int 46 Readonly bool // The buffer cannot be edited 47 Scratch bool // The buffer cannot be saved 48 Syntax bool // Syntax highlighting is enabled 49} 50 51var ( 52 // BTDefault is a default buffer 53 BTDefault = BufType{0, false, false, true} 54 // BTHelp is a help buffer 55 BTHelp = BufType{1, true, true, true} 56 // BTLog is a log buffer 57 BTLog = BufType{2, true, true, false} 58 // BTScratch is a buffer that cannot be saved (for scratch work) 59 BTScratch = BufType{3, false, true, false} 60 // BTRaw is is a buffer that shows raw terminal events 61 BTRaw = BufType{4, false, true, false} 62 // BTInfo is a buffer for inputting information 63 BTInfo = BufType{5, false, true, false} 64 // BTStdout is a buffer that only writes to stdout 65 // when closed 66 BTStdout = BufType{6, false, true, true} 67 68 // ErrFileTooLarge is returned when the file is too large to hash 69 // (fastdirty is automatically enabled) 70 ErrFileTooLarge = errors.New("File is too large to hash") 71) 72 73// SharedBuffer is a struct containing info that is shared among buffers 74// that have the same file open 75type SharedBuffer struct { 76 *LineArray 77 // Stores the last modification time of the file the buffer is pointing to 78 ModTime time.Time 79 // Type of the buffer (e.g. help, raw, scratch etc..) 80 Type BufType 81 82 // Path to the file on disk 83 Path string 84 // Absolute path to the file on disk 85 AbsPath string 86 // Name of the buffer on the status line 87 name string 88 89 toStdout bool 90 91 // Settings customized by the user 92 Settings map[string]interface{} 93 94 Suggestions []string 95 Completions []string 96 CurSuggestion int 97 98 Messages []*Message 99 100 updateDiffTimer *time.Timer 101 diffBase []byte 102 diffBaseLineCount int 103 diffLock sync.RWMutex 104 diff map[int]DiffStatus 105 106 requestedBackup bool 107 108 // ReloadDisabled allows the user to disable reloads if they 109 // are viewing a file that is constantly changing 110 ReloadDisabled bool 111 112 isModified bool 113 // Whether or not suggestions can be autocompleted must be shared because 114 // it changes based on how the buffer has changed 115 HasSuggestions bool 116 117 // The Highlighter struct actually performs the highlighting 118 Highlighter *highlight.Highlighter 119 // SyntaxDef represents the syntax highlighting definition being used 120 // This stores the highlighting rules and filetype detection info 121 SyntaxDef *highlight.Def 122 123 ModifiedThisFrame bool 124 125 // Hash of the original buffer -- empty if fastdirty is on 126 origHash [md5.Size]byte 127} 128 129func (b *SharedBuffer) insert(pos Loc, value []byte) { 130 b.isModified = true 131 b.HasSuggestions = false 132 b.LineArray.insert(pos, value) 133 134 inslines := bytes.Count(value, []byte{'\n'}) 135 b.MarkModified(pos.Y, pos.Y+inslines) 136} 137func (b *SharedBuffer) remove(start, end Loc) []byte { 138 b.isModified = true 139 b.HasSuggestions = false 140 defer b.MarkModified(start.Y, end.Y) 141 return b.LineArray.remove(start, end) 142} 143 144// MarkModified marks the buffer as modified for this frame 145// and performs rehighlighting if syntax highlighting is enabled 146func (b *SharedBuffer) MarkModified(start, end int) { 147 b.ModifiedThisFrame = true 148 149 if !b.Settings["syntax"].(bool) || b.SyntaxDef == nil { 150 return 151 } 152 153 start = util.Clamp(start, 0, len(b.lines)-1) 154 end = util.Clamp(end, 0, len(b.lines)-1) 155 156 l := -1 157 for i := start; i <= end; i++ { 158 l = util.Max(b.Highlighter.ReHighlightStates(b, i), l) 159 } 160 b.Highlighter.HighlightMatches(b, start, l) 161} 162 163// DisableReload disables future reloads of this sharedbuffer 164func (b *SharedBuffer) DisableReload() { 165 b.ReloadDisabled = true 166} 167 168const ( 169 DSUnchanged = 0 170 DSAdded = 1 171 DSModified = 2 172 DSDeletedAbove = 3 173) 174 175type DiffStatus byte 176 177// Buffer stores the main information about a currently open file including 178// the actual text (in a LineArray), the undo/redo stack (in an EventHandler) 179// all the cursors, the syntax highlighting info, the settings for the buffer 180// and some misc info about modification time and path location. 181// The syntax highlighting info must be stored with the buffer because the syntax 182// highlighter attaches information to each line of the buffer for optimization 183// purposes so it doesn't have to rehighlight everything on every update. 184type Buffer struct { 185 *EventHandler 186 *SharedBuffer 187 188 fini int32 189 cursors []*Cursor 190 curCursor int 191 StartCursor Loc 192 193 // OptionCallback is called after a buffer option value is changed. 194 // The display module registers its OptionCallback to ensure the buffer window 195 // is properly updated when needed. This is a workaround for the fact that 196 // the buffer module cannot directly call the display's API (it would mean 197 // a circular dependency between packages). 198 OptionCallback func(option string, nativeValue interface{}) 199 200 // The display module registers its own GetVisualX function for getting 201 // the correct visual x location of a cursor when softwrap is used. 202 // This is hacky. Maybe it would be better to move all the visual x logic 203 // from buffer to display, but it would require rewriting a lot of code. 204 GetVisualX func(loc Loc) int 205} 206 207// NewBufferFromFileAtLoc opens a new buffer with a given cursor location 208// If cursorLoc is {-1, -1} the location does not overwrite what the cursor location 209// would otherwise be (start of file, or saved cursor position if `savecursor` is 210// enabled) 211func NewBufferFromFileAtLoc(path string, btype BufType, cursorLoc Loc) (*Buffer, error) { 212 var err error 213 filename := path 214 if config.GetGlobalOption("parsecursor").(bool) && cursorLoc.X == -1 && cursorLoc.Y == -1 { 215 var cursorPos []string 216 filename, cursorPos = util.GetPathAndCursorPosition(filename) 217 cursorLoc, err = ParseCursorLocation(cursorPos) 218 if err != nil { 219 cursorLoc = Loc{-1, -1} 220 } 221 } 222 223 filename, err = util.ReplaceHome(filename) 224 if err != nil { 225 return nil, err 226 } 227 228 f, err := os.OpenFile(filename, os.O_WRONLY, 0) 229 readonly := os.IsPermission(err) 230 f.Close() 231 232 fileInfo, serr := os.Stat(filename) 233 if serr != nil && !os.IsNotExist(serr) { 234 return nil, serr 235 } 236 if serr == nil && fileInfo.IsDir() { 237 return nil, errors.New("Error: " + filename + " is a directory and cannot be opened") 238 } 239 240 file, err := os.Open(filename) 241 if err == nil { 242 defer file.Close() 243 } 244 245 var buf *Buffer 246 if os.IsNotExist(err) { 247 // File does not exist -- create an empty buffer with that name 248 buf = NewBufferFromString("", filename, btype) 249 } else if err != nil { 250 return nil, err 251 } else { 252 buf = NewBuffer(file, util.FSize(file), filename, cursorLoc, btype) 253 if buf == nil { 254 return nil, errors.New("could not open file") 255 } 256 } 257 258 if readonly && prompt != nil { 259 prompt.Message("Warning: file is readonly - sudo will be attempted when saving") 260 // buf.SetOptionNative("readonly", true) 261 } 262 263 return buf, nil 264} 265 266// NewBufferFromFile opens a new buffer using the given path 267// It will also automatically handle `~`, and line/column with filename:l:c 268// It will return an empty buffer if the path does not exist 269// and an error if the file is a directory 270func NewBufferFromFile(path string, btype BufType) (*Buffer, error) { 271 return NewBufferFromFileAtLoc(path, btype, Loc{-1, -1}) 272} 273 274// NewBufferFromStringAtLoc creates a new buffer containing the given string with a cursor loc 275func NewBufferFromStringAtLoc(text, path string, btype BufType, cursorLoc Loc) *Buffer { 276 return NewBuffer(strings.NewReader(text), int64(len(text)), path, cursorLoc, btype) 277} 278 279// NewBufferFromString creates a new buffer containing the given string 280func NewBufferFromString(text, path string, btype BufType) *Buffer { 281 return NewBuffer(strings.NewReader(text), int64(len(text)), path, Loc{-1, -1}, btype) 282} 283 284// NewBuffer creates a new buffer from a given reader with a given path 285// Ensure that ReadSettings and InitGlobalSettings have been called before creating 286// a new buffer 287// Places the cursor at startcursor. If startcursor is -1, -1 places the 288// cursor at an autodetected location (based on savecursor or :LINE:COL) 289func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufType) *Buffer { 290 absPath, _ := filepath.Abs(path) 291 292 b := new(Buffer) 293 294 found := false 295 if len(path) > 0 { 296 for _, buf := range OpenBuffers { 297 if buf.AbsPath == absPath && buf.Type != BTInfo { 298 found = true 299 b.SharedBuffer = buf.SharedBuffer 300 b.EventHandler = buf.EventHandler 301 } 302 } 303 } 304 305 hasBackup := false 306 if !found { 307 b.SharedBuffer = new(SharedBuffer) 308 b.Type = btype 309 310 b.AbsPath = absPath 311 b.Path = path 312 313 // this is a little messy since we need to know some settings to read 314 // the file properly, but some settings depend on the filetype, which 315 // we don't know until reading the file. We first read the settings 316 // into a local variable and then use that to determine the encoding, 317 // readonly, and fileformat necessary for reading the file and 318 // assigning the filetype. 319 settings := config.DefaultCommonSettings() 320 b.Settings = config.DefaultCommonSettings() 321 for k, v := range config.GlobalSettings { 322 if _, ok := config.DefaultGlobalOnlySettings[k]; !ok { 323 // make sure setting is not global-only 324 settings[k] = v 325 b.Settings[k] = v 326 } 327 } 328 config.InitLocalSettings(settings, path) 329 b.Settings["readonly"] = settings["readonly"] 330 b.Settings["filetype"] = settings["filetype"] 331 b.Settings["syntax"] = settings["syntax"] 332 333 enc, err := htmlindex.Get(settings["encoding"].(string)) 334 if err != nil { 335 enc = unicode.UTF8 336 b.Settings["encoding"] = "utf-8" 337 } 338 339 var ok bool 340 hasBackup, ok = b.ApplyBackup(size) 341 342 if !ok { 343 return NewBufferFromString("", "", btype) 344 } 345 if !hasBackup { 346 reader := bufio.NewReader(transform.NewReader(r, enc.NewDecoder())) 347 348 var ff FileFormat = FFAuto 349 350 if size == 0 { 351 // for empty files, use the fileformat setting instead of 352 // autodetection 353 switch settings["fileformat"] { 354 case "unix": 355 ff = FFUnix 356 case "dos": 357 ff = FFDos 358 } 359 } 360 361 b.LineArray = NewLineArray(uint64(size), ff, reader) 362 } 363 b.EventHandler = NewEventHandler(b.SharedBuffer, b.cursors) 364 365 // The last time this file was modified 366 b.UpdateModTime() 367 } 368 369 if b.Settings["readonly"].(bool) && b.Type == BTDefault { 370 b.Type.Readonly = true 371 } 372 373 switch b.Endings { 374 case FFUnix: 375 b.Settings["fileformat"] = "unix" 376 case FFDos: 377 b.Settings["fileformat"] = "dos" 378 } 379 380 b.UpdateRules() 381 // init local settings again now that we know the filetype 382 config.InitLocalSettings(b.Settings, b.Path) 383 384 if _, err := os.Stat(filepath.Join(config.ConfigDir, "buffers")); os.IsNotExist(err) { 385 os.Mkdir(filepath.Join(config.ConfigDir, "buffers"), os.ModePerm) 386 } 387 388 if startcursor.X != -1 && startcursor.Y != -1 { 389 b.StartCursor = startcursor 390 } else if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) { 391 err := b.Unserialize() 392 if err != nil { 393 screen.TermMessage(err) 394 } 395 } 396 397 b.AddCursor(NewCursor(b, b.StartCursor)) 398 b.GetActiveCursor().Relocate() 399 400 if !b.Settings["fastdirty"].(bool) && !found { 401 if size > LargeFileThreshold { 402 // If the file is larger than LargeFileThreshold fastdirty needs to be on 403 b.Settings["fastdirty"] = true 404 } else if !hasBackup { 405 // since applying a backup does not save the applied backup to disk, we should 406 // not calculate the original hash based on the backup data 407 calcHash(b, &b.origHash) 408 } 409 } 410 411 err := config.RunPluginFn("onBufferOpen", luar.New(ulua.L, b)) 412 if err != nil { 413 screen.TermMessage(err) 414 } 415 416 OpenBuffers = append(OpenBuffers, b) 417 418 return b 419} 420 421// Close removes this buffer from the list of open buffers 422func (b *Buffer) Close() { 423 for i, buf := range OpenBuffers { 424 if b == buf { 425 b.Fini() 426 copy(OpenBuffers[i:], OpenBuffers[i+1:]) 427 OpenBuffers[len(OpenBuffers)-1] = nil 428 OpenBuffers = OpenBuffers[:len(OpenBuffers)-1] 429 return 430 } 431 } 432} 433 434// Fini should be called when a buffer is closed and performs 435// some cleanup 436func (b *Buffer) Fini() { 437 if !b.Modified() { 438 b.Serialize() 439 } 440 b.RemoveBackup() 441 442 if b.Type == BTStdout { 443 fmt.Fprint(util.Stdout, string(b.Bytes())) 444 } 445 446 atomic.StoreInt32(&(b.fini), int32(1)) 447} 448 449// GetName returns the name that should be displayed in the statusline 450// for this buffer 451func (b *Buffer) GetName() string { 452 name := b.name 453 if name == "" { 454 if b.Path == "" { 455 return "No name" 456 } 457 name = b.Path 458 } 459 if b.Settings["basename"].(bool) { 460 return path.Base(name) 461 } 462 return name 463} 464 465//SetName changes the name for this buffer 466func (b *Buffer) SetName(s string) { 467 b.name = s 468} 469 470// Insert inserts the given string of text at the start location 471func (b *Buffer) Insert(start Loc, text string) { 472 if !b.Type.Readonly { 473 b.EventHandler.cursors = b.cursors 474 b.EventHandler.active = b.curCursor 475 b.EventHandler.Insert(start, text) 476 477 b.RequestBackup() 478 } 479} 480 481// Remove removes the characters between the start and end locations 482func (b *Buffer) Remove(start, end Loc) { 483 if !b.Type.Readonly { 484 b.EventHandler.cursors = b.cursors 485 b.EventHandler.active = b.curCursor 486 b.EventHandler.Remove(start, end) 487 488 b.RequestBackup() 489 } 490} 491 492// FileType returns the buffer's filetype 493func (b *Buffer) FileType() string { 494 return b.Settings["filetype"].(string) 495} 496 497// ExternallyModified returns whether the file being edited has 498// been modified by some external process 499func (b *Buffer) ExternallyModified() bool { 500 modTime, err := util.GetModTime(b.Path) 501 if err == nil { 502 return modTime != b.ModTime 503 } 504 return false 505} 506 507// UpdateModTime updates the modtime of this file 508func (b *Buffer) UpdateModTime() (err error) { 509 b.ModTime, err = util.GetModTime(b.Path) 510 return 511} 512 513// ReOpen reloads the current buffer from disk 514func (b *Buffer) ReOpen() error { 515 file, err := os.Open(b.Path) 516 if err != nil { 517 return err 518 } 519 520 enc, err := htmlindex.Get(b.Settings["encoding"].(string)) 521 if err != nil { 522 return err 523 } 524 525 reader := bufio.NewReader(transform.NewReader(file, enc.NewDecoder())) 526 data, err := ioutil.ReadAll(reader) 527 txt := string(data) 528 529 if err != nil { 530 return err 531 } 532 b.EventHandler.ApplyDiff(txt) 533 534 err = b.UpdateModTime() 535 if !b.Settings["fastdirty"].(bool) { 536 calcHash(b, &b.origHash) 537 } 538 b.isModified = false 539 b.RelocateCursors() 540 return err 541} 542 543// RelocateCursors relocates all cursors (makes sure they are in the buffer) 544func (b *Buffer) RelocateCursors() { 545 for _, c := range b.cursors { 546 c.Relocate() 547 } 548} 549 550// RuneAt returns the rune at a given location in the buffer 551func (b *Buffer) RuneAt(loc Loc) rune { 552 line := b.LineBytes(loc.Y) 553 if len(line) > 0 { 554 i := 0 555 for len(line) > 0 { 556 r, _, size := util.DecodeCharacter(line) 557 line = line[size:] 558 559 if i == loc.X { 560 return r 561 } 562 563 i++ 564 } 565 } 566 return '\n' 567} 568 569// WordAt returns the word around a given location in the buffer 570func (b *Buffer) WordAt(loc Loc) []byte { 571 if len(b.LineBytes(loc.Y)) == 0 || !util.IsWordChar(b.RuneAt(loc)) { 572 return []byte{} 573 } 574 575 start := loc 576 end := loc.Move(1, b) 577 578 for start.X > 0 && util.IsWordChar(b.RuneAt(start.Move(-1, b))) { 579 start.X-- 580 } 581 582 lineLen := util.CharacterCount(b.LineBytes(loc.Y)) 583 for end.X < lineLen && util.IsWordChar(b.RuneAt(end)) { 584 end.X++ 585 } 586 587 return b.Substr(start, end) 588} 589 590// Modified returns if this buffer has been modified since 591// being opened 592func (b *Buffer) Modified() bool { 593 if b.Type.Scratch { 594 return false 595 } 596 597 if b.Settings["fastdirty"].(bool) { 598 return b.isModified 599 } 600 601 var buff [md5.Size]byte 602 603 calcHash(b, &buff) 604 return buff != b.origHash 605} 606 607// Size returns the number of bytes in the current buffer 608func (b *Buffer) Size() int { 609 nb := 0 610 for i := 0; i < b.LinesNum(); i++ { 611 nb += len(b.LineBytes(i)) 612 613 if i != b.LinesNum()-1 { 614 if b.Endings == FFDos { 615 nb++ // carriage return 616 } 617 nb++ // newline 618 } 619 } 620 return nb 621} 622 623// calcHash calculates md5 hash of all lines in the buffer 624func calcHash(b *Buffer, out *[md5.Size]byte) error { 625 h := md5.New() 626 627 size := 0 628 if len(b.lines) > 0 { 629 n, e := h.Write(b.lines[0].data) 630 if e != nil { 631 return e 632 } 633 size += n 634 635 for _, l := range b.lines[1:] { 636 n, e = h.Write([]byte{'\n'}) 637 if e != nil { 638 return e 639 } 640 size += n 641 n, e = h.Write(l.data) 642 if e != nil { 643 return e 644 } 645 size += n 646 } 647 } 648 649 if size > LargeFileThreshold { 650 return ErrFileTooLarge 651 } 652 653 h.Sum((*out)[:0]) 654 return nil 655} 656 657// UpdateRules updates the syntax rules and filetype for this buffer 658// This is called when the colorscheme changes 659func (b *Buffer) UpdateRules() { 660 if !b.Type.Syntax { 661 return 662 } 663 ft := b.Settings["filetype"].(string) 664 if ft == "off" { 665 return 666 } 667 syntaxFile := "" 668 foundDef := false 669 var header *highlight.Header 670 // search for the syntax file in the user's custom syntax files 671 for _, f := range config.ListRealRuntimeFiles(config.RTSyntax) { 672 data, err := f.Data() 673 if err != nil { 674 screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error()) 675 continue 676 } 677 678 header, err = highlight.MakeHeaderYaml(data) 679 if err != nil { 680 screen.TermMessage("Error parsing header for syntax file " + f.Name() + ": " + err.Error()) 681 } 682 file, err := highlight.ParseFile(data) 683 if err != nil { 684 screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error()) 685 continue 686 } 687 688 if ((ft == "unknown" || ft == "") && highlight.MatchFiletype(header.FtDetect, b.Path, b.lines[0].data)) || header.FileType == ft { 689 syndef, err := highlight.ParseDef(file, header) 690 if err != nil { 691 screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error()) 692 continue 693 } 694 b.SyntaxDef = syndef 695 syntaxFile = f.Name() 696 foundDef = true 697 break 698 } 699 } 700 701 // search in the default syntax files 702 for _, f := range config.ListRuntimeFiles(config.RTSyntaxHeader) { 703 data, err := f.Data() 704 if err != nil { 705 screen.TermMessage("Error loading syntax header file " + f.Name() + ": " + err.Error()) 706 continue 707 } 708 709 header, err = highlight.MakeHeader(data) 710 if err != nil { 711 screen.TermMessage("Error reading syntax header file", f.Name(), err) 712 continue 713 } 714 715 if ft == "unknown" || ft == "" { 716 if highlight.MatchFiletype(header.FtDetect, b.Path, b.lines[0].data) { 717 syntaxFile = f.Name() 718 break 719 } 720 } else if header.FileType == ft { 721 syntaxFile = f.Name() 722 break 723 } 724 } 725 726 if syntaxFile != "" && !foundDef { 727 // we found a syntax file using a syntax header file 728 for _, f := range config.ListRuntimeFiles(config.RTSyntax) { 729 if f.Name() == syntaxFile { 730 data, err := f.Data() 731 if err != nil { 732 screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error()) 733 continue 734 } 735 736 file, err := highlight.ParseFile(data) 737 if err != nil { 738 screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error()) 739 continue 740 } 741 742 syndef, err := highlight.ParseDef(file, header) 743 if err != nil { 744 screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error()) 745 continue 746 } 747 b.SyntaxDef = syndef 748 break 749 } 750 } 751 } 752 753 if b.SyntaxDef != nil && highlight.HasIncludes(b.SyntaxDef) { 754 includes := highlight.GetIncludes(b.SyntaxDef) 755 756 var files []*highlight.File 757 for _, f := range config.ListRuntimeFiles(config.RTSyntax) { 758 data, err := f.Data() 759 if err != nil { 760 screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error()) 761 continue 762 } 763 header, err := highlight.MakeHeaderYaml(data) 764 if err != nil { 765 screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error()) 766 continue 767 } 768 769 for _, i := range includes { 770 if header.FileType == i { 771 file, err := highlight.ParseFile(data) 772 if err != nil { 773 screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error()) 774 continue 775 } 776 files = append(files, file) 777 break 778 } 779 } 780 if len(files) >= len(includes) { 781 break 782 } 783 } 784 785 highlight.ResolveIncludes(b.SyntaxDef, files) 786 } 787 788 if b.Highlighter == nil || syntaxFile != "" { 789 if b.SyntaxDef != nil { 790 b.Settings["filetype"] = b.SyntaxDef.FileType 791 } 792 } else { 793 b.SyntaxDef = &highlight.EmptyDef 794 } 795 796 if b.SyntaxDef != nil { 797 b.Highlighter = highlight.NewHighlighter(b.SyntaxDef) 798 if b.Settings["syntax"].(bool) { 799 go func() { 800 b.Highlighter.HighlightStates(b) 801 b.Highlighter.HighlightMatches(b, 0, b.End().Y) 802 screen.Redraw() 803 }() 804 } 805 } 806} 807 808// ClearMatches clears all of the syntax highlighting for the buffer 809func (b *Buffer) ClearMatches() { 810 for i := range b.lines { 811 b.SetMatch(i, nil) 812 b.SetState(i, nil) 813 } 814} 815 816// IndentString returns this buffer's indent method (a tabstop or n spaces 817// depending on the settings) 818func (b *Buffer) IndentString(tabsize int) string { 819 if b.Settings["tabstospaces"].(bool) { 820 return util.Spaces(tabsize) 821 } 822 return "\t" 823} 824 825// SetCursors resets this buffer's cursors to a new list 826func (b *Buffer) SetCursors(c []*Cursor) { 827 b.cursors = c 828 b.EventHandler.cursors = b.cursors 829 b.EventHandler.active = b.curCursor 830} 831 832// AddCursor adds a new cursor to the list 833func (b *Buffer) AddCursor(c *Cursor) { 834 b.cursors = append(b.cursors, c) 835 b.EventHandler.cursors = b.cursors 836 b.EventHandler.active = b.curCursor 837 b.UpdateCursors() 838} 839 840// SetCurCursor sets the current cursor 841func (b *Buffer) SetCurCursor(n int) { 842 b.curCursor = n 843} 844 845// GetActiveCursor returns the main cursor in this buffer 846func (b *Buffer) GetActiveCursor() *Cursor { 847 return b.cursors[b.curCursor] 848} 849 850// GetCursor returns the nth cursor 851func (b *Buffer) GetCursor(n int) *Cursor { 852 return b.cursors[n] 853} 854 855// GetCursors returns the list of cursors in this buffer 856func (b *Buffer) GetCursors() []*Cursor { 857 return b.cursors 858} 859 860// NumCursors returns the number of cursors 861func (b *Buffer) NumCursors() int { 862 return len(b.cursors) 863} 864 865// MergeCursors merges any cursors that are at the same position 866// into one cursor 867func (b *Buffer) MergeCursors() { 868 var cursors []*Cursor 869 for i := 0; i < len(b.cursors); i++ { 870 c1 := b.cursors[i] 871 if c1 != nil { 872 for j := 0; j < len(b.cursors); j++ { 873 c2 := b.cursors[j] 874 if c2 != nil && i != j && c1.Loc == c2.Loc { 875 b.cursors[j] = nil 876 } 877 } 878 cursors = append(cursors, c1) 879 } 880 } 881 882 b.cursors = cursors 883 884 for i := range b.cursors { 885 b.cursors[i].Num = i 886 } 887 888 if b.curCursor >= len(b.cursors) { 889 b.curCursor = len(b.cursors) - 1 890 } 891 b.EventHandler.cursors = b.cursors 892 b.EventHandler.active = b.curCursor 893} 894 895// UpdateCursors updates all the cursors indicies 896func (b *Buffer) UpdateCursors() { 897 b.EventHandler.cursors = b.cursors 898 b.EventHandler.active = b.curCursor 899 for i, c := range b.cursors { 900 c.Num = i 901 } 902} 903 904func (b *Buffer) RemoveCursor(i int) { 905 copy(b.cursors[i:], b.cursors[i+1:]) 906 b.cursors[len(b.cursors)-1] = nil 907 b.cursors = b.cursors[:len(b.cursors)-1] 908 b.curCursor = util.Clamp(b.curCursor, 0, len(b.cursors)-1) 909 b.UpdateCursors() 910} 911 912// ClearCursors removes all extra cursors 913func (b *Buffer) ClearCursors() { 914 for i := 1; i < len(b.cursors); i++ { 915 b.cursors[i] = nil 916 } 917 b.cursors = b.cursors[:1] 918 b.UpdateCursors() 919 b.curCursor = 0 920 b.GetActiveCursor().ResetSelection() 921} 922 923// MoveLinesUp moves the range of lines up one row 924func (b *Buffer) MoveLinesUp(start int, end int) { 925 if start < 1 || start >= end || end > len(b.lines) { 926 return 927 } 928 l := string(b.LineBytes(start - 1)) 929 if end == len(b.lines) { 930 b.insert( 931 Loc{ 932 util.CharacterCount(b.lines[end-1].data), 933 end - 1, 934 }, 935 []byte{'\n'}, 936 ) 937 } 938 b.Insert( 939 Loc{0, end}, 940 l+"\n", 941 ) 942 b.Remove( 943 Loc{0, start - 1}, 944 Loc{0, start}, 945 ) 946} 947 948// MoveLinesDown moves the range of lines down one row 949func (b *Buffer) MoveLinesDown(start int, end int) { 950 if start < 0 || start >= end || end >= len(b.lines) { 951 return 952 } 953 l := string(b.LineBytes(end)) 954 b.Insert( 955 Loc{0, start}, 956 l+"\n", 957 ) 958 end++ 959 b.Remove( 960 Loc{0, end}, 961 Loc{0, end + 1}, 962 ) 963} 964 965var BracePairs = [][2]rune{ 966 {'(', ')'}, 967 {'{', '}'}, 968 {'[', ']'}, 969} 970 971// FindMatchingBrace returns the location in the buffer of the matching bracket 972// It is given a brace type containing the open and closing character, (for example 973// '{' and '}') as well as the location to match from 974// TODO: maybe can be more efficient with utf8 package 975// returns the location of the matching brace 976// if the boolean returned is true then the original matching brace is one character left 977// of the starting location 978func (b *Buffer) FindMatchingBrace(braceType [2]rune, start Loc) (Loc, bool, bool) { 979 curLine := []rune(string(b.LineBytes(start.Y))) 980 startChar := ' ' 981 if start.X >= 0 && start.X < len(curLine) { 982 startChar = curLine[start.X] 983 } 984 leftChar := ' ' 985 if start.X-1 >= 0 && start.X-1 < len(curLine) { 986 leftChar = curLine[start.X-1] 987 } 988 var i int 989 if startChar == braceType[0] || leftChar == braceType[0] { 990 for y := start.Y; y < b.LinesNum(); y++ { 991 l := []rune(string(b.LineBytes(y))) 992 xInit := 0 993 if y == start.Y { 994 if startChar == braceType[0] { 995 xInit = start.X 996 } else { 997 xInit = start.X - 1 998 } 999 } 1000 for x := xInit; x < len(l); x++ { 1001 r := l[x] 1002 if r == braceType[0] { 1003 i++ 1004 } else if r == braceType[1] { 1005 i-- 1006 if i == 0 { 1007 if startChar == braceType[0] { 1008 return Loc{x, y}, false, true 1009 } 1010 return Loc{x, y}, true, true 1011 } 1012 } 1013 } 1014 } 1015 } else if startChar == braceType[1] || leftChar == braceType[1] { 1016 for y := start.Y; y >= 0; y-- { 1017 l := []rune(string(b.lines[y].data)) 1018 xInit := len(l) - 1 1019 if y == start.Y { 1020 if leftChar == braceType[1] { 1021 xInit = start.X - 1 1022 } else { 1023 xInit = start.X 1024 } 1025 } 1026 for x := xInit; x >= 0; x-- { 1027 r := l[x] 1028 if r == braceType[0] { 1029 i-- 1030 if i == 0 { 1031 if leftChar == braceType[1] { 1032 return Loc{x, y}, true, true 1033 } 1034 return Loc{x, y}, false, true 1035 } 1036 } else if r == braceType[1] { 1037 i++ 1038 } 1039 } 1040 } 1041 } 1042 return start, true, false 1043} 1044 1045// Retab changes all tabs to spaces or vice versa 1046func (b *Buffer) Retab() { 1047 toSpaces := b.Settings["tabstospaces"].(bool) 1048 tabsize := util.IntOpt(b.Settings["tabsize"]) 1049 dirty := false 1050 1051 for i := 0; i < b.LinesNum(); i++ { 1052 l := b.LineBytes(i) 1053 1054 ws := util.GetLeadingWhitespace(l) 1055 if len(ws) != 0 { 1056 if toSpaces { 1057 ws = bytes.ReplaceAll(ws, []byte{'\t'}, bytes.Repeat([]byte{' '}, tabsize)) 1058 } else { 1059 ws = bytes.ReplaceAll(ws, bytes.Repeat([]byte{' '}, tabsize), []byte{'\t'}) 1060 } 1061 } 1062 1063 l = bytes.TrimLeft(l, " \t") 1064 b.lines[i].data = append(ws, l...) 1065 b.MarkModified(i, i) 1066 dirty = true 1067 } 1068 1069 b.isModified = dirty 1070} 1071 1072// ParseCursorLocation turns a cursor location like 10:5 (LINE:COL) 1073// into a loc 1074func ParseCursorLocation(cursorPositions []string) (Loc, error) { 1075 startpos := Loc{0, 0} 1076 var err error 1077 1078 // if no positions are available exit early 1079 if cursorPositions == nil { 1080 return startpos, errors.New("No cursor positions were provided.") 1081 } 1082 1083 startpos.Y, err = strconv.Atoi(cursorPositions[0]) 1084 startpos.Y-- 1085 if err == nil { 1086 if len(cursorPositions) > 1 { 1087 startpos.X, err = strconv.Atoi(cursorPositions[1]) 1088 if startpos.X > 0 { 1089 startpos.X-- 1090 } 1091 } 1092 } 1093 1094 return startpos, err 1095} 1096 1097// Line returns the string representation of the given line number 1098func (b *Buffer) Line(i int) string { 1099 return string(b.LineBytes(i)) 1100} 1101 1102func (b *Buffer) Write(bytes []byte) (n int, err error) { 1103 b.EventHandler.InsertBytes(b.End(), bytes) 1104 return len(bytes), nil 1105} 1106 1107func (b *Buffer) updateDiffSync() { 1108 b.diffLock.Lock() 1109 defer b.diffLock.Unlock() 1110 1111 b.diff = make(map[int]DiffStatus) 1112 1113 if b.diffBase == nil { 1114 return 1115 } 1116 1117 differ := dmp.New() 1118 baseRunes, bufferRunes, _ := differ.DiffLinesToRunes(string(b.diffBase), string(b.Bytes())) 1119 diffs := differ.DiffMainRunes(baseRunes, bufferRunes, false) 1120 lineN := 0 1121 1122 for _, diff := range diffs { 1123 lineCount := len([]rune(diff.Text)) 1124 1125 switch diff.Type { 1126 case dmp.DiffEqual: 1127 lineN += lineCount 1128 case dmp.DiffInsert: 1129 var status DiffStatus 1130 if b.diff[lineN] == DSDeletedAbove { 1131 status = DSModified 1132 } else { 1133 status = DSAdded 1134 } 1135 for i := 0; i < lineCount; i++ { 1136 b.diff[lineN] = status 1137 lineN++ 1138 } 1139 case dmp.DiffDelete: 1140 b.diff[lineN] = DSDeletedAbove 1141 } 1142 } 1143} 1144 1145// UpdateDiff computes the diff between the diff base and the buffer content. 1146// The update may be performed synchronously or asynchronously. 1147// UpdateDiff calls the supplied callback when the update is complete. 1148// The argument passed to the callback is set to true if and only if 1149// the update was performed synchronously. 1150// If an asynchronous update is already pending when UpdateDiff is called, 1151// UpdateDiff does not schedule another update, in which case the callback 1152// is not called. 1153func (b *Buffer) UpdateDiff(callback func(bool)) { 1154 if b.updateDiffTimer != nil { 1155 return 1156 } 1157 1158 lineCount := b.LinesNum() 1159 if b.diffBaseLineCount > lineCount { 1160 lineCount = b.diffBaseLineCount 1161 } 1162 1163 if lineCount < 1000 { 1164 b.updateDiffSync() 1165 callback(true) 1166 } else if lineCount < 30000 { 1167 b.updateDiffTimer = time.AfterFunc(500*time.Millisecond, func() { 1168 b.updateDiffTimer = nil 1169 b.updateDiffSync() 1170 callback(false) 1171 }) 1172 } else { 1173 // Don't compute diffs for very large files 1174 b.diffLock.Lock() 1175 b.diff = make(map[int]DiffStatus) 1176 b.diffLock.Unlock() 1177 callback(true) 1178 } 1179} 1180 1181// SetDiffBase sets the text that is used as the base for diffing the buffer content 1182func (b *Buffer) SetDiffBase(diffBase []byte) { 1183 b.diffBase = diffBase 1184 if diffBase == nil { 1185 b.diffBaseLineCount = 0 1186 } else { 1187 b.diffBaseLineCount = strings.Count(string(diffBase), "\n") 1188 } 1189 b.UpdateDiff(func(synchronous bool) { 1190 screen.Redraw() 1191 }) 1192} 1193 1194// DiffStatus returns the diff status for a line in the buffer 1195func (b *Buffer) DiffStatus(lineN int) DiffStatus { 1196 b.diffLock.RLock() 1197 defer b.diffLock.RUnlock() 1198 // Note that the zero value for DiffStatus is equal to DSUnchanged 1199 return b.diff[lineN] 1200} 1201 1202// WriteLog writes a string to the log buffer 1203func WriteLog(s string) { 1204 LogBuf.EventHandler.Insert(LogBuf.End(), s) 1205} 1206 1207// GetLogBuf returns the log buffer 1208func GetLogBuf() *Buffer { 1209 return LogBuf 1210} 1211