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