mirror of
https://github.com/zyedidia/micro.git
synced 2026-03-23 17:27:14 +09:00
Change project layout and use go.mod
This commit is contained in:
128
internal/buffer/autocomplete.go
Normal file
128
internal/buffer/autocomplete.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/zyedidia/micro/internal/util"
|
||||
)
|
||||
|
||||
// A Completer is a function that takes a buffer and returns info
|
||||
// describing what autocompletions should be inserted at the current
|
||||
// cursor location
|
||||
// It returns a list of string suggestions which will be inserted at
|
||||
// the current cursor location if selected as well as a list of
|
||||
// suggestion names which can be displayed in a autocomplete box or
|
||||
// other UI element
|
||||
type Completer func(*Buffer) ([]string, []string)
|
||||
|
||||
func (b *Buffer) GetSuggestions() {
|
||||
|
||||
}
|
||||
|
||||
func (b *Buffer) Autocomplete(c Completer) {
|
||||
b.Completions, b.Suggestions = c(b)
|
||||
if len(b.Completions) != len(b.Suggestions) || len(b.Completions) == 0 {
|
||||
return
|
||||
}
|
||||
b.CurSuggestion = -1
|
||||
b.CycleAutocomplete(true)
|
||||
}
|
||||
|
||||
func (b *Buffer) CycleAutocomplete(forward bool) {
|
||||
prevSuggestion := b.CurSuggestion
|
||||
|
||||
if forward {
|
||||
b.CurSuggestion++
|
||||
} else {
|
||||
b.CurSuggestion--
|
||||
}
|
||||
if b.CurSuggestion >= len(b.Suggestions) {
|
||||
b.CurSuggestion = 0
|
||||
} else if b.CurSuggestion < 0 {
|
||||
b.CurSuggestion = len(b.Suggestions) - 1
|
||||
}
|
||||
|
||||
c := b.GetActiveCursor()
|
||||
start := c.Loc
|
||||
end := c.Loc
|
||||
if prevSuggestion < len(b.Suggestions) && prevSuggestion >= 0 {
|
||||
start = end.Move(-utf8.RuneCountInString(b.Completions[prevSuggestion]), b)
|
||||
} else {
|
||||
end = start.Move(1, b)
|
||||
}
|
||||
|
||||
b.Replace(start, end, b.Completions[b.CurSuggestion])
|
||||
b.HasSuggestions = true
|
||||
}
|
||||
|
||||
func GetArg(b *Buffer) (string, int) {
|
||||
c := b.GetActiveCursor()
|
||||
l := b.LineBytes(c.Y)
|
||||
l = util.SliceStart(l, c.X)
|
||||
|
||||
args := bytes.Split(l, []byte{' '})
|
||||
input := string(args[len(args)-1])
|
||||
argstart := 0
|
||||
for i, a := range args {
|
||||
if i == len(args)-1 {
|
||||
break
|
||||
}
|
||||
argstart += utf8.RuneCount(a) + 1
|
||||
}
|
||||
|
||||
return input, argstart
|
||||
}
|
||||
|
||||
// FileComplete autocompletes filenames
|
||||
func FileComplete(b *Buffer) ([]string, []string) {
|
||||
c := b.GetActiveCursor()
|
||||
input, argstart := GetArg(b)
|
||||
|
||||
sep := string(os.PathSeparator)
|
||||
dirs := strings.Split(input, sep)
|
||||
|
||||
var files []os.FileInfo
|
||||
var err error
|
||||
if len(dirs) > 1 {
|
||||
directories := strings.Join(dirs[:len(dirs)-1], sep) + sep
|
||||
|
||||
directories, _ = util.ReplaceHome(directories)
|
||||
files, err = ioutil.ReadDir(directories)
|
||||
} else {
|
||||
files, err = ioutil.ReadDir(".")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var suggestions []string
|
||||
for _, f := range files {
|
||||
name := f.Name()
|
||||
if f.IsDir() {
|
||||
name += sep
|
||||
}
|
||||
if strings.HasPrefix(name, dirs[len(dirs)-1]) {
|
||||
suggestions = append(suggestions, name)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(suggestions)
|
||||
completions := make([]string, len(suggestions))
|
||||
for i := range suggestions {
|
||||
var complete string
|
||||
if len(dirs) > 1 {
|
||||
complete = strings.Join(dirs[:len(dirs)-1], sep) + sep + suggestions[i]
|
||||
} else {
|
||||
complete = suggestions[i]
|
||||
}
|
||||
completions[i] = util.SliceEndStr(complete, c.X-argstart)
|
||||
}
|
||||
|
||||
return completions, suggestions
|
||||
}
|
||||
695
internal/buffer/buffer.go
Normal file
695
internal/buffer/buffer.go
Normal file
@@ -0,0 +1,695 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/zyedidia/micro/internal/config"
|
||||
"github.com/zyedidia/micro/pkg/highlight"
|
||||
"github.com/zyedidia/micro/internal/screen"
|
||||
. "github.com/zyedidia/micro/internal/util"
|
||||
"golang.org/x/text/encoding/htmlindex"
|
||||
"golang.org/x/text/encoding/unicode"
|
||||
"golang.org/x/text/transform"
|
||||
)
|
||||
|
||||
var (
|
||||
OpenBuffers []*Buffer
|
||||
)
|
||||
|
||||
// The BufType defines what kind of buffer this is
|
||||
type BufType struct {
|
||||
Kind int
|
||||
Readonly bool // The buffer cannot be edited
|
||||
Scratch bool // The buffer cannot be saved
|
||||
Syntax bool // Syntax highlighting is enabled
|
||||
}
|
||||
|
||||
var (
|
||||
BTDefault = BufType{0, false, false, true}
|
||||
BTHelp = BufType{1, true, true, true}
|
||||
BTLog = BufType{2, true, true, false}
|
||||
BTScratch = BufType{3, false, true, false}
|
||||
BTRaw = BufType{4, false, true, false}
|
||||
BTInfo = BufType{5, false, true, false}
|
||||
|
||||
ErrFileTooLarge = errors.New("File is too large to hash")
|
||||
)
|
||||
|
||||
type SharedBuffer struct {
|
||||
*LineArray
|
||||
// Stores the last modification time of the file the buffer is pointing to
|
||||
ModTime time.Time
|
||||
// Type of the buffer (e.g. help, raw, scratch etc..)
|
||||
Type BufType
|
||||
|
||||
isModified bool
|
||||
// Whether or not suggestions can be autocompleted must be shared because
|
||||
// it changes based on how the buffer has changed
|
||||
HasSuggestions bool
|
||||
}
|
||||
|
||||
func (b *SharedBuffer) insert(pos Loc, value []byte) {
|
||||
b.isModified = true
|
||||
b.HasSuggestions = false
|
||||
b.LineArray.insert(pos, value)
|
||||
}
|
||||
func (b *SharedBuffer) remove(start, end Loc) []byte {
|
||||
b.isModified = true
|
||||
b.HasSuggestions = false
|
||||
return b.LineArray.remove(start, end)
|
||||
}
|
||||
|
||||
// Buffer stores the main information about a currently open file including
|
||||
// the actual text (in a LineArray), the undo/redo stack (in an EventHandler)
|
||||
// all the cursors, the syntax highlighting info, the settings for the buffer
|
||||
// and some misc info about modification time and path location.
|
||||
// The syntax highlighting info must be stored with the buffer because the syntax
|
||||
// highlighter attaches information to each line of the buffer for optimization
|
||||
// purposes so it doesn't have to rehighlight everything on every update.
|
||||
type Buffer struct {
|
||||
*EventHandler
|
||||
*SharedBuffer
|
||||
|
||||
cursors []*Cursor
|
||||
curCursor int
|
||||
StartCursor Loc
|
||||
|
||||
// Path to the file on disk
|
||||
Path string
|
||||
// Absolute path to the file on disk
|
||||
AbsPath string
|
||||
// Name of the buffer on the status line
|
||||
name string
|
||||
|
||||
SyntaxDef *highlight.Def
|
||||
Highlighter *highlight.Highlighter
|
||||
|
||||
// Hash of the original buffer -- empty if fastdirty is on
|
||||
origHash [md5.Size]byte
|
||||
|
||||
// Settings customized by the user
|
||||
Settings map[string]interface{}
|
||||
|
||||
Suggestions []string
|
||||
Completions []string
|
||||
CurSuggestion int
|
||||
|
||||
Messages []*Message
|
||||
}
|
||||
|
||||
// NewBufferFromFile opens a new buffer using the given path
|
||||
// It will also automatically handle `~`, and line/column with filename:l:c
|
||||
// It will return an empty buffer if the path does not exist
|
||||
// and an error if the file is a directory
|
||||
func NewBufferFromFile(path string, btype BufType) (*Buffer, error) {
|
||||
var err error
|
||||
filename, cursorPosition := GetPathAndCursorPosition(path)
|
||||
filename, err = ReplaceHome(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
file, err := os.Open(filename)
|
||||
fileInfo, _ := os.Stat(filename)
|
||||
|
||||
if err == nil && fileInfo.IsDir() {
|
||||
return nil, errors.New(filename + " is a directory")
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
var buf *Buffer
|
||||
if err != nil {
|
||||
// File does not exist -- create an empty buffer with that name
|
||||
buf = NewBufferFromString("", filename, btype)
|
||||
} else {
|
||||
buf = NewBuffer(file, FSize(file), filename, cursorPosition, btype)
|
||||
}
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
// NewBufferFromString creates a new buffer containing the given string
|
||||
func NewBufferFromString(text, path string, btype BufType) *Buffer {
|
||||
return NewBuffer(strings.NewReader(text), int64(len(text)), path, nil, btype)
|
||||
}
|
||||
|
||||
// NewBuffer creates a new buffer from a given reader with a given path
|
||||
// Ensure that ReadSettings and InitGlobalSettings have been called before creating
|
||||
// a new buffer
|
||||
func NewBuffer(r io.Reader, size int64, path string, cursorPosition []string, btype BufType) *Buffer {
|
||||
absPath, _ := filepath.Abs(path)
|
||||
|
||||
b := new(Buffer)
|
||||
|
||||
b.Settings = config.DefaultLocalSettings()
|
||||
for k, v := range config.GlobalSettings {
|
||||
if _, ok := b.Settings[k]; ok {
|
||||
b.Settings[k] = v
|
||||
}
|
||||
}
|
||||
config.InitLocalSettings(b.Settings, b.Path)
|
||||
|
||||
enc, err := htmlindex.Get(b.Settings["encoding"].(string))
|
||||
if err != nil {
|
||||
enc = unicode.UTF8
|
||||
b.Settings["encoding"] = "utf-8"
|
||||
}
|
||||
|
||||
reader := transform.NewReader(r, enc.NewDecoder())
|
||||
|
||||
found := false
|
||||
if len(path) > 0 {
|
||||
for _, buf := range OpenBuffers {
|
||||
if buf.AbsPath == absPath {
|
||||
found = true
|
||||
b.SharedBuffer = buf.SharedBuffer
|
||||
b.EventHandler = buf.EventHandler
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
b.SharedBuffer = new(SharedBuffer)
|
||||
b.Type = btype
|
||||
b.LineArray = NewLineArray(uint64(size), FFAuto, reader)
|
||||
b.EventHandler = NewEventHandler(b.SharedBuffer, b.cursors)
|
||||
}
|
||||
|
||||
switch b.Endings {
|
||||
case FFUnix:
|
||||
b.Settings["fileformat"] = "unix"
|
||||
case FFDos:
|
||||
b.Settings["fileformat"] = "dos"
|
||||
}
|
||||
|
||||
b.Path = path
|
||||
b.AbsPath = absPath
|
||||
|
||||
// The last time this file was modified
|
||||
b.ModTime, _ = GetModTime(b.Path)
|
||||
|
||||
b.UpdateRules()
|
||||
|
||||
if _, err := os.Stat(config.ConfigDir + "/buffers/"); os.IsNotExist(err) {
|
||||
os.Mkdir(config.ConfigDir+"/buffers/", os.ModePerm)
|
||||
}
|
||||
|
||||
// cursorLocation, err := GetBufferCursorLocation(cursorPosition, b)
|
||||
// b.startcursor = Cursor{
|
||||
// Loc: cursorLocation,
|
||||
// buf: b,
|
||||
// }
|
||||
// TODO flagstartpos
|
||||
if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
|
||||
err := b.Unserialize()
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
}
|
||||
|
||||
b.AddCursor(NewCursor(b, b.StartCursor))
|
||||
|
||||
if !b.Settings["fastdirty"].(bool) {
|
||||
if size > LargeFileThreshold {
|
||||
// If the file is larger than LargeFileThreshold fastdirty needs to be on
|
||||
b.Settings["fastdirty"] = true
|
||||
} else {
|
||||
calcHash(b, &b.origHash)
|
||||
}
|
||||
}
|
||||
|
||||
OpenBuffers = append(OpenBuffers, b)
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// Close removes this buffer from the list of open buffers
|
||||
func (b *Buffer) Close() {
|
||||
for i, buf := range OpenBuffers {
|
||||
if b == buf {
|
||||
copy(OpenBuffers[i:], OpenBuffers[i+1:])
|
||||
OpenBuffers[len(OpenBuffers)-1] = nil
|
||||
OpenBuffers = OpenBuffers[:len(OpenBuffers)-1]
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetName returns the name that should be displayed in the statusline
|
||||
// for this buffer
|
||||
func (b *Buffer) GetName() string {
|
||||
if b.name == "" {
|
||||
if b.Path == "" {
|
||||
return "No name"
|
||||
}
|
||||
return b.Path
|
||||
}
|
||||
return b.name
|
||||
}
|
||||
|
||||
//SetName changes the name for this buffer
|
||||
func (b *Buffer) SetName(s string) {
|
||||
b.name = s
|
||||
}
|
||||
|
||||
func (b *Buffer) Insert(start Loc, text string) {
|
||||
if !b.Type.Readonly {
|
||||
b.EventHandler.cursors = b.cursors
|
||||
b.EventHandler.active = b.curCursor
|
||||
b.EventHandler.Insert(start, text)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Buffer) Remove(start, end Loc) {
|
||||
if !b.Type.Readonly {
|
||||
b.EventHandler.cursors = b.cursors
|
||||
b.EventHandler.active = b.curCursor
|
||||
b.EventHandler.Remove(start, end)
|
||||
}
|
||||
}
|
||||
|
||||
// FileType returns the buffer's filetype
|
||||
func (b *Buffer) FileType() string {
|
||||
return b.Settings["filetype"].(string)
|
||||
}
|
||||
|
||||
// ReOpen reloads the current buffer from disk
|
||||
func (b *Buffer) ReOpen() error {
|
||||
file, err := os.Open(b.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
enc, err := htmlindex.Get(b.Settings["encoding"].(string))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reader := transform.NewReader(file, enc.NewDecoder())
|
||||
data, err := ioutil.ReadAll(reader)
|
||||
txt := string(data)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.EventHandler.ApplyDiff(txt)
|
||||
|
||||
b.ModTime, err = GetModTime(b.Path)
|
||||
b.isModified = false
|
||||
b.RelocateCursors()
|
||||
return err
|
||||
}
|
||||
|
||||
func (b *Buffer) RelocateCursors() {
|
||||
for _, c := range b.cursors {
|
||||
c.Relocate()
|
||||
}
|
||||
}
|
||||
|
||||
// RuneAt returns the rune at a given location in the buffer
|
||||
func (b *Buffer) RuneAt(loc Loc) rune {
|
||||
line := b.LineBytes(loc.Y)
|
||||
if len(line) > 0 {
|
||||
i := 0
|
||||
for len(line) > 0 {
|
||||
r, size := utf8.DecodeRune(line)
|
||||
line = line[size:]
|
||||
i++
|
||||
|
||||
if i == loc.X {
|
||||
return r
|
||||
}
|
||||
}
|
||||
}
|
||||
return '\n'
|
||||
}
|
||||
|
||||
// Modified returns if this buffer has been modified since
|
||||
// being opened
|
||||
func (b *Buffer) Modified() bool {
|
||||
if b.Type.Scratch {
|
||||
return false
|
||||
}
|
||||
|
||||
if b.Settings["fastdirty"].(bool) {
|
||||
return b.isModified
|
||||
}
|
||||
|
||||
var buff [md5.Size]byte
|
||||
|
||||
calcHash(b, &buff)
|
||||
return buff != b.origHash
|
||||
}
|
||||
|
||||
// calcHash calculates md5 hash of all lines in the buffer
|
||||
func calcHash(b *Buffer, out *[md5.Size]byte) error {
|
||||
h := md5.New()
|
||||
|
||||
size := 0
|
||||
if len(b.lines) > 0 {
|
||||
n, e := h.Write(b.lines[0].data)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
size += n
|
||||
|
||||
for _, l := range b.lines[1:] {
|
||||
n, e = h.Write([]byte{'\n'})
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
size += n
|
||||
n, e = h.Write(l.data)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
size += n
|
||||
}
|
||||
}
|
||||
|
||||
if size > LargeFileThreshold {
|
||||
return ErrFileTooLarge
|
||||
}
|
||||
|
||||
h.Sum((*out)[:0])
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateRules updates the syntax rules and filetype for this buffer
|
||||
// This is called when the colorscheme changes
|
||||
func (b *Buffer) UpdateRules() {
|
||||
if !b.Type.Syntax {
|
||||
return
|
||||
}
|
||||
rehighlight := false
|
||||
var files []*highlight.File
|
||||
for _, f := range config.ListRuntimeFiles(config.RTSyntax) {
|
||||
data, err := f.Data()
|
||||
if err != nil {
|
||||
screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
|
||||
} else {
|
||||
file, err := highlight.ParseFile(data)
|
||||
if err != nil {
|
||||
screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
|
||||
continue
|
||||
}
|
||||
ftdetect, err := highlight.ParseFtDetect(file)
|
||||
if err != nil {
|
||||
screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
ft := b.Settings["filetype"].(string)
|
||||
if (ft == "Unknown" || ft == "") && !rehighlight {
|
||||
if highlight.MatchFiletype(ftdetect, b.Path, b.lines[0].data) {
|
||||
header := new(highlight.Header)
|
||||
header.FileType = file.FileType
|
||||
header.FtDetect = ftdetect
|
||||
b.SyntaxDef, err = highlight.ParseDef(file, header)
|
||||
if err != nil {
|
||||
screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
|
||||
continue
|
||||
}
|
||||
rehighlight = true
|
||||
}
|
||||
} else {
|
||||
if file.FileType == ft && !rehighlight {
|
||||
header := new(highlight.Header)
|
||||
header.FileType = file.FileType
|
||||
header.FtDetect = ftdetect
|
||||
b.SyntaxDef, err = highlight.ParseDef(file, header)
|
||||
if err != nil {
|
||||
screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
|
||||
continue
|
||||
}
|
||||
rehighlight = true
|
||||
}
|
||||
}
|
||||
files = append(files, file)
|
||||
}
|
||||
}
|
||||
|
||||
if b.SyntaxDef != nil {
|
||||
highlight.ResolveIncludes(b.SyntaxDef, files)
|
||||
}
|
||||
|
||||
if b.Highlighter == nil || rehighlight {
|
||||
if b.SyntaxDef != nil {
|
||||
b.Settings["filetype"] = b.SyntaxDef.FileType
|
||||
b.Highlighter = highlight.NewHighlighter(b.SyntaxDef)
|
||||
if b.Settings["syntax"].(bool) {
|
||||
b.Highlighter.HighlightStates(b)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ClearMatches clears all of the syntax highlighting for the buffer
|
||||
func (b *Buffer) ClearMatches() {
|
||||
for i := range b.lines {
|
||||
b.SetMatch(i, nil)
|
||||
b.SetState(i, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// IndentString returns this buffer's indent method (a tabstop or n spaces
|
||||
// depending on the settings)
|
||||
func (b *Buffer) IndentString(tabsize int) string {
|
||||
if b.Settings["tabstospaces"].(bool) {
|
||||
return Spaces(tabsize)
|
||||
}
|
||||
return "\t"
|
||||
}
|
||||
|
||||
// SetCursors resets this buffer's cursors to a new list
|
||||
func (b *Buffer) SetCursors(c []*Cursor) {
|
||||
b.cursors = c
|
||||
b.EventHandler.cursors = b.cursors
|
||||
b.EventHandler.active = b.curCursor
|
||||
}
|
||||
|
||||
// AddCursor adds a new cursor to the list
|
||||
func (b *Buffer) AddCursor(c *Cursor) {
|
||||
b.cursors = append(b.cursors, c)
|
||||
b.EventHandler.cursors = b.cursors
|
||||
b.EventHandler.active = b.curCursor
|
||||
b.UpdateCursors()
|
||||
}
|
||||
|
||||
// SetCurCursor sets the current cursor
|
||||
func (b *Buffer) SetCurCursor(n int) {
|
||||
b.curCursor = n
|
||||
}
|
||||
|
||||
// GetActiveCursor returns the main cursor in this buffer
|
||||
func (b *Buffer) GetActiveCursor() *Cursor {
|
||||
return b.cursors[b.curCursor]
|
||||
}
|
||||
|
||||
// GetCursor returns the nth cursor
|
||||
func (b *Buffer) GetCursor(n int) *Cursor {
|
||||
return b.cursors[n]
|
||||
}
|
||||
|
||||
// GetCursors returns the list of cursors in this buffer
|
||||
func (b *Buffer) GetCursors() []*Cursor {
|
||||
return b.cursors
|
||||
}
|
||||
|
||||
// NumCursors returns the number of cursors
|
||||
func (b *Buffer) NumCursors() int {
|
||||
return len(b.cursors)
|
||||
}
|
||||
|
||||
// MergeCursors merges any cursors that are at the same position
|
||||
// into one cursor
|
||||
func (b *Buffer) MergeCursors() {
|
||||
var cursors []*Cursor
|
||||
for i := 0; i < len(b.cursors); i++ {
|
||||
c1 := b.cursors[i]
|
||||
if c1 != nil {
|
||||
for j := 0; j < len(b.cursors); j++ {
|
||||
c2 := b.cursors[j]
|
||||
if c2 != nil && i != j && c1.Loc == c2.Loc {
|
||||
b.cursors[j] = nil
|
||||
}
|
||||
}
|
||||
cursors = append(cursors, c1)
|
||||
}
|
||||
}
|
||||
|
||||
b.cursors = cursors
|
||||
|
||||
for i := range b.cursors {
|
||||
b.cursors[i].Num = i
|
||||
}
|
||||
|
||||
if b.curCursor >= len(b.cursors) {
|
||||
b.curCursor = len(b.cursors) - 1
|
||||
}
|
||||
b.EventHandler.cursors = b.cursors
|
||||
b.EventHandler.active = b.curCursor
|
||||
}
|
||||
|
||||
// UpdateCursors updates all the cursors indicies
|
||||
func (b *Buffer) UpdateCursors() {
|
||||
b.EventHandler.cursors = b.cursors
|
||||
b.EventHandler.active = b.curCursor
|
||||
for i, c := range b.cursors {
|
||||
c.Num = i
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Buffer) RemoveCursor(i int) {
|
||||
copy(b.cursors[i:], b.cursors[i+1:])
|
||||
b.cursors[len(b.cursors)-1] = nil
|
||||
b.cursors = b.cursors[:len(b.cursors)-1]
|
||||
b.curCursor = Clamp(b.curCursor, 0, len(b.cursors)-1)
|
||||
b.UpdateCursors()
|
||||
}
|
||||
|
||||
// ClearCursors removes all extra cursors
|
||||
func (b *Buffer) ClearCursors() {
|
||||
for i := 1; i < len(b.cursors); i++ {
|
||||
b.cursors[i] = nil
|
||||
}
|
||||
b.cursors = b.cursors[:1]
|
||||
b.UpdateCursors()
|
||||
b.curCursor = 0
|
||||
b.GetActiveCursor().ResetSelection()
|
||||
}
|
||||
|
||||
// MoveLinesUp moves the range of lines up one row
|
||||
func (b *Buffer) MoveLinesUp(start int, end int) {
|
||||
if start < 1 || start >= end || end > len(b.lines) {
|
||||
return
|
||||
}
|
||||
l := string(b.LineBytes(start - 1))
|
||||
if end == len(b.lines) {
|
||||
b.Insert(
|
||||
Loc{
|
||||
utf8.RuneCount(b.lines[end-1].data),
|
||||
end - 1,
|
||||
},
|
||||
"\n"+l,
|
||||
)
|
||||
} else {
|
||||
b.Insert(
|
||||
Loc{0, end},
|
||||
l+"\n",
|
||||
)
|
||||
}
|
||||
b.Remove(
|
||||
Loc{0, start - 1},
|
||||
Loc{0, start},
|
||||
)
|
||||
}
|
||||
|
||||
// MoveLinesDown moves the range of lines down one row
|
||||
func (b *Buffer) MoveLinesDown(start int, end int) {
|
||||
if start < 0 || start >= end || end >= len(b.lines)-1 {
|
||||
return
|
||||
}
|
||||
l := string(b.LineBytes(end))
|
||||
b.Insert(
|
||||
Loc{0, start},
|
||||
l+"\n",
|
||||
)
|
||||
end++
|
||||
b.Remove(
|
||||
Loc{0, end},
|
||||
Loc{0, end + 1},
|
||||
)
|
||||
}
|
||||
|
||||
var BracePairs = [][2]rune{
|
||||
{'(', ')'},
|
||||
{'{', '}'},
|
||||
{'[', ']'},
|
||||
}
|
||||
|
||||
// FindMatchingBrace returns the location in the buffer of the matching bracket
|
||||
// It is given a brace type containing the open and closing character, (for example
|
||||
// '{' and '}') as well as the location to match from
|
||||
// TODO: maybe can be more efficient with utf8 package
|
||||
func (b *Buffer) FindMatchingBrace(braceType [2]rune, start Loc) Loc {
|
||||
curLine := []rune(string(b.LineBytes(start.Y)))
|
||||
startChar := curLine[start.X]
|
||||
var i int
|
||||
if startChar == braceType[0] {
|
||||
for y := start.Y; y < b.LinesNum(); y++ {
|
||||
l := []rune(string(b.LineBytes(y)))
|
||||
xInit := 0
|
||||
if y == start.Y {
|
||||
xInit = start.X
|
||||
}
|
||||
for x := xInit; x < len(l); x++ {
|
||||
r := l[x]
|
||||
if r == braceType[0] {
|
||||
i++
|
||||
} else if r == braceType[1] {
|
||||
i--
|
||||
if i == 0 {
|
||||
return Loc{x, y}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if startChar == braceType[1] {
|
||||
for y := start.Y; y >= 0; y-- {
|
||||
l := []rune(string(b.lines[y].data))
|
||||
xInit := len(l) - 1
|
||||
if y == start.Y {
|
||||
xInit = start.X
|
||||
}
|
||||
for x := xInit; x >= 0; x-- {
|
||||
r := l[x]
|
||||
if r == braceType[0] {
|
||||
i--
|
||||
if i == 0 {
|
||||
return Loc{x, y}
|
||||
}
|
||||
} else if r == braceType[1] {
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return start
|
||||
}
|
||||
|
||||
// Retab changes all tabs to spaces or vice versa
|
||||
func (b *Buffer) Retab() {
|
||||
toSpaces := b.Settings["tabstospaces"].(bool)
|
||||
tabsize := IntOpt(b.Settings["tabsize"])
|
||||
dirty := false
|
||||
|
||||
for i := 0; i < b.LinesNum(); i++ {
|
||||
l := b.LineBytes(i)
|
||||
|
||||
ws := GetLeadingWhitespace(l)
|
||||
if len(ws) != 0 {
|
||||
if toSpaces {
|
||||
ws = bytes.Replace(ws, []byte{'\t'}, bytes.Repeat([]byte{' '}, tabsize), -1)
|
||||
} else {
|
||||
ws = bytes.Replace(ws, bytes.Repeat([]byte{' '}, tabsize), []byte{'\t'}, -1)
|
||||
}
|
||||
}
|
||||
|
||||
l = bytes.TrimLeft(l, " \t")
|
||||
b.lines[i].data = append(ws, l...)
|
||||
dirty = true
|
||||
}
|
||||
|
||||
b.isModified = dirty
|
||||
}
|
||||
440
internal/buffer/cursor.go
Normal file
440
internal/buffer/cursor.go
Normal file
@@ -0,0 +1,440 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/zyedidia/clipboard"
|
||||
"github.com/zyedidia/micro/internal/util"
|
||||
)
|
||||
|
||||
// InBounds returns whether the given location is a valid character position in the given buffer
|
||||
func InBounds(pos Loc, buf *Buffer) bool {
|
||||
if pos.Y < 0 || pos.Y >= len(buf.lines) || pos.X < 0 || pos.X > utf8.RuneCount(buf.LineBytes(pos.Y)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// The Cursor struct stores the location of the cursor in the buffer
|
||||
// as well as the selection
|
||||
type Cursor struct {
|
||||
buf *Buffer
|
||||
Loc
|
||||
|
||||
// Last cursor x position
|
||||
LastVisualX int
|
||||
|
||||
// The current selection as a range of character numbers (inclusive)
|
||||
CurSelection [2]Loc
|
||||
// The original selection as a range of character numbers
|
||||
// This is used for line and word selection where it is necessary
|
||||
// to know what the original selection was
|
||||
OrigSelection [2]Loc
|
||||
|
||||
// Which cursor index is this (for multiple cursors)
|
||||
Num int
|
||||
}
|
||||
|
||||
func NewCursor(b *Buffer, l Loc) *Cursor {
|
||||
c := &Cursor{
|
||||
buf: b,
|
||||
Loc: l,
|
||||
}
|
||||
c.StoreVisualX()
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Cursor) SetBuf(b *Buffer) {
|
||||
c.buf = b
|
||||
}
|
||||
|
||||
func (c *Cursor) Buf() *Buffer {
|
||||
return c.buf
|
||||
}
|
||||
|
||||
// Goto puts the cursor at the given cursor's location and gives
|
||||
// the current cursor its selection too
|
||||
func (c *Cursor) Goto(b Cursor) {
|
||||
c.X, c.Y, c.LastVisualX = b.X, b.Y, b.LastVisualX
|
||||
c.OrigSelection, c.CurSelection = b.OrigSelection, b.CurSelection
|
||||
}
|
||||
|
||||
// GotoLoc puts the cursor at the given cursor's location and gives
|
||||
// the current cursor its selection too
|
||||
func (c *Cursor) GotoLoc(l Loc) {
|
||||
c.X, c.Y = l.X, l.Y
|
||||
c.StoreVisualX()
|
||||
}
|
||||
|
||||
// GetVisualX returns the x value of the cursor in visual spaces
|
||||
func (c *Cursor) GetVisualX() int {
|
||||
if c.X <= 0 {
|
||||
c.X = 0
|
||||
return 0
|
||||
}
|
||||
|
||||
bytes := c.buf.LineBytes(c.Y)
|
||||
tabsize := int(c.buf.Settings["tabsize"].(float64))
|
||||
if c.X > utf8.RuneCount(bytes) {
|
||||
c.X = utf8.RuneCount(bytes) - 1
|
||||
}
|
||||
|
||||
return util.StringWidth(bytes, c.X, tabsize)
|
||||
}
|
||||
|
||||
// GetCharPosInLine gets the char position of a visual x y
|
||||
// coordinate (this is necessary because tabs are 1 char but
|
||||
// 4 visual spaces)
|
||||
func (c *Cursor) GetCharPosInLine(b []byte, visualPos int) int {
|
||||
tabsize := int(c.buf.Settings["tabsize"].(float64))
|
||||
return util.GetCharPosInLine(b, visualPos, tabsize)
|
||||
}
|
||||
|
||||
// Start moves the cursor to the start of the line it is on
|
||||
func (c *Cursor) Start() {
|
||||
c.X = 0
|
||||
c.LastVisualX = c.GetVisualX()
|
||||
}
|
||||
|
||||
// StartOfText moves the cursor to the first non-whitespace rune of
|
||||
// the line it is on
|
||||
func (c *Cursor) StartOfText() {
|
||||
c.Start()
|
||||
for util.IsWhitespace(c.RuneUnder(c.X)) {
|
||||
if c.X == utf8.RuneCount(c.buf.LineBytes(c.Y)) {
|
||||
break
|
||||
}
|
||||
c.Right()
|
||||
}
|
||||
}
|
||||
|
||||
// End moves the cursor to the end of the line it is on
|
||||
func (c *Cursor) End() {
|
||||
c.X = utf8.RuneCount(c.buf.LineBytes(c.Y))
|
||||
c.LastVisualX = c.GetVisualX()
|
||||
}
|
||||
|
||||
// CopySelection copies the user's selection to either "primary"
|
||||
// or "clipboard"
|
||||
func (c *Cursor) CopySelection(target string) {
|
||||
if c.HasSelection() {
|
||||
if target != "primary" || c.buf.Settings["useprimary"].(bool) {
|
||||
clipboard.WriteAll(string(c.GetSelection()), target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ResetSelection resets the user's selection
|
||||
func (c *Cursor) ResetSelection() {
|
||||
c.CurSelection[0] = c.buf.Start()
|
||||
c.CurSelection[1] = c.buf.Start()
|
||||
}
|
||||
|
||||
// SetSelectionStart sets the start of the selection
|
||||
func (c *Cursor) SetSelectionStart(pos Loc) {
|
||||
c.CurSelection[0] = pos
|
||||
}
|
||||
|
||||
// SetSelectionEnd sets the end of the selection
|
||||
func (c *Cursor) SetSelectionEnd(pos Loc) {
|
||||
c.CurSelection[1] = pos
|
||||
}
|
||||
|
||||
// HasSelection returns whether or not the user has selected anything
|
||||
func (c *Cursor) HasSelection() bool {
|
||||
return c.CurSelection[0] != c.CurSelection[1]
|
||||
}
|
||||
|
||||
// DeleteSelection deletes the currently selected text
|
||||
func (c *Cursor) DeleteSelection() {
|
||||
if c.CurSelection[0].GreaterThan(c.CurSelection[1]) {
|
||||
c.buf.Remove(c.CurSelection[1], c.CurSelection[0])
|
||||
c.Loc = c.CurSelection[1]
|
||||
} else if !c.HasSelection() {
|
||||
return
|
||||
} else {
|
||||
c.buf.Remove(c.CurSelection[0], c.CurSelection[1])
|
||||
c.Loc = c.CurSelection[0]
|
||||
}
|
||||
}
|
||||
|
||||
// Deselect closes the cursor's current selection
|
||||
// Start indicates whether the cursor should be placed
|
||||
// at the start or end of the selection
|
||||
func (c *Cursor) Deselect(start bool) {
|
||||
if c.HasSelection() {
|
||||
if start {
|
||||
c.Loc = c.CurSelection[0]
|
||||
} else {
|
||||
c.Loc = c.CurSelection[1].Move(-1, c.buf)
|
||||
}
|
||||
c.ResetSelection()
|
||||
c.StoreVisualX()
|
||||
}
|
||||
}
|
||||
|
||||
// GetSelection returns the cursor's selection
|
||||
func (c *Cursor) GetSelection() []byte {
|
||||
if InBounds(c.CurSelection[0], c.buf) && InBounds(c.CurSelection[1], c.buf) {
|
||||
if c.CurSelection[0].GreaterThan(c.CurSelection[1]) {
|
||||
return c.buf.Substr(c.CurSelection[1], c.CurSelection[0])
|
||||
}
|
||||
return c.buf.Substr(c.CurSelection[0], c.CurSelection[1])
|
||||
}
|
||||
return []byte{}
|
||||
}
|
||||
|
||||
// SelectLine selects the current line
|
||||
func (c *Cursor) SelectLine() {
|
||||
c.Start()
|
||||
c.SetSelectionStart(c.Loc)
|
||||
c.End()
|
||||
if len(c.buf.lines)-1 > c.Y {
|
||||
c.SetSelectionEnd(c.Loc.Move(1, c.buf))
|
||||
} else {
|
||||
c.SetSelectionEnd(c.Loc)
|
||||
}
|
||||
|
||||
c.OrigSelection = c.CurSelection
|
||||
}
|
||||
|
||||
// AddLineToSelection adds the current line to the selection
|
||||
func (c *Cursor) AddLineToSelection() {
|
||||
if c.Loc.LessThan(c.OrigSelection[0]) {
|
||||
c.Start()
|
||||
c.SetSelectionStart(c.Loc)
|
||||
c.SetSelectionEnd(c.OrigSelection[1])
|
||||
}
|
||||
if c.Loc.GreaterThan(c.OrigSelection[1]) {
|
||||
c.End()
|
||||
c.SetSelectionEnd(c.Loc.Move(1, c.buf))
|
||||
c.SetSelectionStart(c.OrigSelection[0])
|
||||
}
|
||||
|
||||
if c.Loc.LessThan(c.OrigSelection[1]) && c.Loc.GreaterThan(c.OrigSelection[0]) {
|
||||
c.CurSelection = c.OrigSelection
|
||||
}
|
||||
}
|
||||
|
||||
// UpN moves the cursor up N lines (if possible)
|
||||
func (c *Cursor) UpN(amount int) {
|
||||
proposedY := c.Y - amount
|
||||
if proposedY < 0 {
|
||||
proposedY = 0
|
||||
} else if proposedY >= len(c.buf.lines) {
|
||||
proposedY = len(c.buf.lines) - 1
|
||||
}
|
||||
|
||||
bytes := c.buf.LineBytes(proposedY)
|
||||
c.X = c.GetCharPosInLine(bytes, c.LastVisualX)
|
||||
|
||||
if c.X > utf8.RuneCount(bytes) || (amount < 0 && proposedY == c.Y) {
|
||||
c.X = utf8.RuneCount(bytes)
|
||||
}
|
||||
|
||||
c.Y = proposedY
|
||||
}
|
||||
|
||||
// DownN moves the cursor down N lines (if possible)
|
||||
func (c *Cursor) DownN(amount int) {
|
||||
c.UpN(-amount)
|
||||
}
|
||||
|
||||
// Up moves the cursor up one line (if possible)
|
||||
func (c *Cursor) Up() {
|
||||
c.UpN(1)
|
||||
}
|
||||
|
||||
// Down moves the cursor down one line (if possible)
|
||||
func (c *Cursor) Down() {
|
||||
c.DownN(1)
|
||||
}
|
||||
|
||||
// Left moves the cursor left one cell (if possible) or to
|
||||
// the previous line if it is at the beginning
|
||||
func (c *Cursor) Left() {
|
||||
if c.Loc == c.buf.Start() {
|
||||
return
|
||||
}
|
||||
if c.X > 0 {
|
||||
c.X--
|
||||
} else {
|
||||
c.Up()
|
||||
c.End()
|
||||
}
|
||||
c.StoreVisualX()
|
||||
}
|
||||
|
||||
// Right moves the cursor right one cell (if possible) or
|
||||
// to the next line if it is at the end
|
||||
func (c *Cursor) Right() {
|
||||
if c.Loc == c.buf.End() {
|
||||
return
|
||||
}
|
||||
if c.X < utf8.RuneCount(c.buf.LineBytes(c.Y)) {
|
||||
c.X++
|
||||
} else {
|
||||
c.Down()
|
||||
c.Start()
|
||||
}
|
||||
c.StoreVisualX()
|
||||
}
|
||||
|
||||
// Relocate makes sure that the cursor is inside the bounds
|
||||
// of the buffer If it isn't, it moves it to be within the
|
||||
// buffer's lines
|
||||
func (c *Cursor) Relocate() {
|
||||
if c.Y < 0 {
|
||||
c.Y = 0
|
||||
} else if c.Y >= len(c.buf.lines) {
|
||||
c.Y = len(c.buf.lines) - 1
|
||||
}
|
||||
|
||||
if c.X < 0 {
|
||||
c.X = 0
|
||||
} else if c.X > utf8.RuneCount(c.buf.LineBytes(c.Y)) {
|
||||
c.X = utf8.RuneCount(c.buf.LineBytes(c.Y))
|
||||
}
|
||||
}
|
||||
|
||||
// SelectWord selects the word the cursor is currently on
|
||||
func (c *Cursor) SelectWord() {
|
||||
if len(c.buf.LineBytes(c.Y)) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if !util.IsWordChar(c.RuneUnder(c.X)) {
|
||||
c.SetSelectionStart(c.Loc)
|
||||
c.SetSelectionEnd(c.Loc.Move(1, c.buf))
|
||||
c.OrigSelection = c.CurSelection
|
||||
return
|
||||
}
|
||||
|
||||
forward, backward := c.X, c.X
|
||||
|
||||
for backward > 0 && util.IsWordChar(c.RuneUnder(backward-1)) {
|
||||
backward--
|
||||
}
|
||||
|
||||
c.SetSelectionStart(Loc{backward, c.Y})
|
||||
c.OrigSelection[0] = c.CurSelection[0]
|
||||
|
||||
lineLen := utf8.RuneCount(c.buf.LineBytes(c.Y)) - 1
|
||||
for forward < lineLen && util.IsWordChar(c.RuneUnder(forward+1)) {
|
||||
forward++
|
||||
}
|
||||
|
||||
c.SetSelectionEnd(Loc{forward, c.Y}.Move(1, c.buf))
|
||||
c.OrigSelection[1] = c.CurSelection[1]
|
||||
c.Loc = c.CurSelection[1]
|
||||
}
|
||||
|
||||
// AddWordToSelection adds the word the cursor is currently on
|
||||
// to the selection
|
||||
func (c *Cursor) AddWordToSelection() {
|
||||
if c.Loc.GreaterThan(c.OrigSelection[0]) && c.Loc.LessThan(c.OrigSelection[1]) {
|
||||
c.CurSelection = c.OrigSelection
|
||||
return
|
||||
}
|
||||
|
||||
if c.Loc.LessThan(c.OrigSelection[0]) {
|
||||
backward := c.X
|
||||
|
||||
for backward > 0 && util.IsWordChar(c.RuneUnder(backward-1)) {
|
||||
backward--
|
||||
}
|
||||
|
||||
c.SetSelectionStart(Loc{backward, c.Y})
|
||||
c.SetSelectionEnd(c.OrigSelection[1])
|
||||
}
|
||||
|
||||
if c.Loc.GreaterThan(c.OrigSelection[1]) {
|
||||
forward := c.X
|
||||
|
||||
lineLen := utf8.RuneCount(c.buf.LineBytes(c.Y)) - 1
|
||||
for forward < lineLen && util.IsWordChar(c.RuneUnder(forward+1)) {
|
||||
forward++
|
||||
}
|
||||
|
||||
c.SetSelectionEnd(Loc{forward, c.Y}.Move(1, c.buf))
|
||||
c.SetSelectionStart(c.OrigSelection[0])
|
||||
}
|
||||
|
||||
c.Loc = c.CurSelection[1]
|
||||
}
|
||||
|
||||
// SelectTo selects from the current cursor location to the given
|
||||
// location
|
||||
func (c *Cursor) SelectTo(loc Loc) {
|
||||
if loc.GreaterThan(c.OrigSelection[0]) {
|
||||
c.SetSelectionStart(c.OrigSelection[0])
|
||||
c.SetSelectionEnd(loc)
|
||||
} else {
|
||||
c.SetSelectionStart(loc)
|
||||
c.SetSelectionEnd(c.OrigSelection[0])
|
||||
}
|
||||
}
|
||||
|
||||
// WordRight moves the cursor one word to the right
|
||||
func (c *Cursor) WordRight() {
|
||||
for util.IsWhitespace(c.RuneUnder(c.X)) {
|
||||
if c.X == utf8.RuneCount(c.buf.LineBytes(c.Y)) {
|
||||
c.Right()
|
||||
return
|
||||
}
|
||||
c.Right()
|
||||
}
|
||||
c.Right()
|
||||
for util.IsWordChar(c.RuneUnder(c.X)) {
|
||||
if c.X == utf8.RuneCount(c.buf.LineBytes(c.Y)) {
|
||||
return
|
||||
}
|
||||
c.Right()
|
||||
}
|
||||
}
|
||||
|
||||
// WordLeft moves the cursor one word to the left
|
||||
func (c *Cursor) WordLeft() {
|
||||
c.Left()
|
||||
for util.IsWhitespace(c.RuneUnder(c.X)) {
|
||||
if c.X == 0 {
|
||||
return
|
||||
}
|
||||
c.Left()
|
||||
}
|
||||
c.Left()
|
||||
for util.IsWordChar(c.RuneUnder(c.X)) {
|
||||
if c.X == 0 {
|
||||
return
|
||||
}
|
||||
c.Left()
|
||||
}
|
||||
c.Right()
|
||||
}
|
||||
|
||||
// RuneUnder returns the rune under the given x position
|
||||
func (c *Cursor) RuneUnder(x int) rune {
|
||||
line := c.buf.LineBytes(c.Y)
|
||||
if len(line) == 0 || x >= utf8.RuneCount(line) {
|
||||
return '\n'
|
||||
} else if x < 0 {
|
||||
x = 0
|
||||
}
|
||||
i := 0
|
||||
for len(line) > 0 {
|
||||
r, size := utf8.DecodeRune(line)
|
||||
line = line[size:]
|
||||
|
||||
if i == x {
|
||||
return r
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
return '\n'
|
||||
}
|
||||
|
||||
func (c *Cursor) StoreVisualX() {
|
||||
c.LastVisualX = c.GetVisualX()
|
||||
}
|
||||
300
internal/buffer/eventhandler.go
Normal file
300
internal/buffer/eventhandler.go
Normal file
@@ -0,0 +1,300 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
dmp "github.com/sergi/go-diff/diffmatchpatch"
|
||||
)
|
||||
|
||||
const (
|
||||
// Opposite and undoing events must have opposite values
|
||||
|
||||
// TextEventInsert represents an insertion event
|
||||
TextEventInsert = 1
|
||||
// TextEventRemove represents a deletion event
|
||||
TextEventRemove = -1
|
||||
// TextEventReplace represents a replace event
|
||||
TextEventReplace = 0
|
||||
|
||||
undoThreshold = 500 // If two events are less than n milliseconds apart, undo both of them
|
||||
)
|
||||
|
||||
// TextEvent holds data for a manipulation on some text that can be undone
|
||||
type TextEvent struct {
|
||||
C Cursor
|
||||
|
||||
EventType int
|
||||
Deltas []Delta
|
||||
Time time.Time
|
||||
}
|
||||
|
||||
// A Delta is a change to the buffer
|
||||
type Delta struct {
|
||||
Text []byte
|
||||
Start Loc
|
||||
End Loc
|
||||
}
|
||||
|
||||
// ExecuteTextEvent runs a text event
|
||||
func ExecuteTextEvent(t *TextEvent, buf *SharedBuffer) {
|
||||
if t.EventType == TextEventInsert {
|
||||
for _, d := range t.Deltas {
|
||||
buf.insert(d.Start, d.Text)
|
||||
}
|
||||
} else if t.EventType == TextEventRemove {
|
||||
for i, d := range t.Deltas {
|
||||
t.Deltas[i].Text = buf.remove(d.Start, d.End)
|
||||
}
|
||||
} else if t.EventType == TextEventReplace {
|
||||
for i, d := range t.Deltas {
|
||||
t.Deltas[i].Text = buf.remove(d.Start, d.End)
|
||||
buf.insert(d.Start, d.Text)
|
||||
t.Deltas[i].Start = d.Start
|
||||
t.Deltas[i].End = Loc{d.Start.X + utf8.RuneCount(d.Text), d.Start.Y}
|
||||
}
|
||||
for i, j := 0, len(t.Deltas)-1; i < j; i, j = i+1, j-1 {
|
||||
t.Deltas[i], t.Deltas[j] = t.Deltas[j], t.Deltas[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UndoTextEvent undoes a text event
|
||||
func UndoTextEvent(t *TextEvent, buf *SharedBuffer) {
|
||||
t.EventType = -t.EventType
|
||||
ExecuteTextEvent(t, buf)
|
||||
}
|
||||
|
||||
// EventHandler executes text manipulations and allows undoing and redoing
|
||||
type EventHandler struct {
|
||||
buf *SharedBuffer
|
||||
cursors []*Cursor
|
||||
active int
|
||||
UndoStack *TEStack
|
||||
RedoStack *TEStack
|
||||
}
|
||||
|
||||
// NewEventHandler returns a new EventHandler
|
||||
func NewEventHandler(buf *SharedBuffer, cursors []*Cursor) *EventHandler {
|
||||
eh := new(EventHandler)
|
||||
eh.UndoStack = new(TEStack)
|
||||
eh.RedoStack = new(TEStack)
|
||||
eh.buf = buf
|
||||
eh.cursors = cursors
|
||||
return eh
|
||||
}
|
||||
|
||||
// ApplyDiff takes a string and runs the necessary insertion and deletion events to make
|
||||
// the buffer equal to that string
|
||||
// This means that we can transform the buffer into any string and still preserve undo/redo
|
||||
// through insert and delete events
|
||||
func (eh *EventHandler) ApplyDiff(new string) {
|
||||
differ := dmp.New()
|
||||
diff := differ.DiffMain(string(eh.buf.Bytes()), new, false)
|
||||
loc := eh.buf.Start()
|
||||
for _, d := range diff {
|
||||
if d.Type == dmp.DiffDelete {
|
||||
eh.Remove(loc, loc.MoveLA(utf8.RuneCountInString(d.Text), eh.buf.LineArray))
|
||||
} else {
|
||||
if d.Type == dmp.DiffInsert {
|
||||
eh.Insert(loc, d.Text)
|
||||
}
|
||||
loc = loc.MoveLA(utf8.RuneCountInString(d.Text), eh.buf.LineArray)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Insert creates an insert text event and executes it
|
||||
func (eh *EventHandler) Insert(start Loc, textStr string) {
|
||||
text := []byte(textStr)
|
||||
e := &TextEvent{
|
||||
C: *eh.cursors[eh.active],
|
||||
EventType: TextEventInsert,
|
||||
Deltas: []Delta{{text, start, Loc{0, 0}}},
|
||||
Time: time.Now(),
|
||||
}
|
||||
eh.Execute(e)
|
||||
e.Deltas[0].End = start.MoveLA(utf8.RuneCount(text), eh.buf.LineArray)
|
||||
end := e.Deltas[0].End
|
||||
|
||||
for _, c := range eh.cursors {
|
||||
move := func(loc Loc) Loc {
|
||||
if start.Y != end.Y && loc.GreaterThan(start) {
|
||||
loc.Y += end.Y - start.Y
|
||||
} else if loc.Y == start.Y && loc.GreaterEqual(start) {
|
||||
loc = loc.MoveLA(utf8.RuneCount(text), eh.buf.LineArray)
|
||||
}
|
||||
return loc
|
||||
}
|
||||
c.Loc = move(c.Loc)
|
||||
c.CurSelection[0] = move(c.CurSelection[0])
|
||||
c.CurSelection[1] = move(c.CurSelection[1])
|
||||
c.OrigSelection[0] = move(c.OrigSelection[0])
|
||||
c.OrigSelection[1] = move(c.OrigSelection[1])
|
||||
c.LastVisualX = c.GetVisualX()
|
||||
}
|
||||
}
|
||||
|
||||
// Remove creates a remove text event and executes it
|
||||
func (eh *EventHandler) Remove(start, end Loc) {
|
||||
e := &TextEvent{
|
||||
C: *eh.cursors[eh.active],
|
||||
EventType: TextEventRemove,
|
||||
Deltas: []Delta{{[]byte{}, start, end}},
|
||||
Time: time.Now(),
|
||||
}
|
||||
eh.Execute(e)
|
||||
|
||||
for _, c := range eh.cursors {
|
||||
move := func(loc Loc) Loc {
|
||||
if start.Y != end.Y && loc.GreaterThan(end) {
|
||||
loc.Y -= end.Y - start.Y
|
||||
} else if loc.Y == end.Y && loc.GreaterEqual(end) {
|
||||
loc = loc.MoveLA(-DiffLA(start, end, eh.buf.LineArray), eh.buf.LineArray)
|
||||
}
|
||||
return loc
|
||||
}
|
||||
c.Loc = move(c.Loc)
|
||||
c.CurSelection[0] = move(c.CurSelection[0])
|
||||
c.CurSelection[1] = move(c.CurSelection[1])
|
||||
c.OrigSelection[0] = move(c.OrigSelection[0])
|
||||
c.OrigSelection[1] = move(c.OrigSelection[1])
|
||||
c.LastVisualX = c.GetVisualX()
|
||||
}
|
||||
}
|
||||
|
||||
// MultipleReplace creates an multiple insertions executes them
|
||||
func (eh *EventHandler) MultipleReplace(deltas []Delta) {
|
||||
e := &TextEvent{
|
||||
C: *eh.cursors[eh.active],
|
||||
EventType: TextEventReplace,
|
||||
Deltas: deltas,
|
||||
Time: time.Now(),
|
||||
}
|
||||
eh.Execute(e)
|
||||
}
|
||||
|
||||
// Replace deletes from start to end and replaces it with the given string
|
||||
func (eh *EventHandler) Replace(start, end Loc, replace string) {
|
||||
eh.Remove(start, end)
|
||||
eh.Insert(start, replace)
|
||||
}
|
||||
|
||||
// Execute a textevent and add it to the undo stack
|
||||
func (eh *EventHandler) Execute(t *TextEvent) {
|
||||
if eh.RedoStack.Len() > 0 {
|
||||
eh.RedoStack = new(TEStack)
|
||||
}
|
||||
eh.UndoStack.Push(t)
|
||||
|
||||
// TODO: Call plugins on text events
|
||||
// for pl := range loadedPlugins {
|
||||
// ret, err := Call(pl+".onBeforeTextEvent", t)
|
||||
// if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") {
|
||||
// screen.TermMessage(err)
|
||||
// }
|
||||
// if val, ok := ret.(lua.LBool); ok && val == lua.LFalse {
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
|
||||
ExecuteTextEvent(t, eh.buf)
|
||||
}
|
||||
|
||||
// Undo the first event in the undo stack
|
||||
func (eh *EventHandler) Undo() {
|
||||
t := eh.UndoStack.Peek()
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
|
||||
startTime := t.Time.UnixNano() / int64(time.Millisecond)
|
||||
|
||||
eh.UndoOneEvent()
|
||||
|
||||
for {
|
||||
t = eh.UndoStack.Peek()
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if startTime-(t.Time.UnixNano()/int64(time.Millisecond)) > undoThreshold {
|
||||
return
|
||||
}
|
||||
startTime = t.Time.UnixNano() / int64(time.Millisecond)
|
||||
|
||||
eh.UndoOneEvent()
|
||||
}
|
||||
}
|
||||
|
||||
// UndoOneEvent undoes one event
|
||||
func (eh *EventHandler) UndoOneEvent() {
|
||||
// This event should be undone
|
||||
// Pop it off the stack
|
||||
t := eh.UndoStack.Pop()
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Undo it
|
||||
// Modifies the text event
|
||||
UndoTextEvent(t, eh.buf)
|
||||
|
||||
// Set the cursor in the right place
|
||||
teCursor := t.C
|
||||
if teCursor.Num >= 0 && teCursor.Num < len(eh.cursors) {
|
||||
t.C = *eh.cursors[teCursor.Num]
|
||||
eh.cursors[teCursor.Num].Goto(teCursor)
|
||||
} else {
|
||||
teCursor.Num = -1
|
||||
}
|
||||
|
||||
// Push it to the redo stack
|
||||
eh.RedoStack.Push(t)
|
||||
}
|
||||
|
||||
// Redo the first event in the redo stack
|
||||
func (eh *EventHandler) Redo() {
|
||||
t := eh.RedoStack.Peek()
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
|
||||
startTime := t.Time.UnixNano() / int64(time.Millisecond)
|
||||
|
||||
eh.RedoOneEvent()
|
||||
|
||||
for {
|
||||
t = eh.RedoStack.Peek()
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if (t.Time.UnixNano()/int64(time.Millisecond))-startTime > undoThreshold {
|
||||
return
|
||||
}
|
||||
|
||||
eh.RedoOneEvent()
|
||||
}
|
||||
}
|
||||
|
||||
// RedoOneEvent redoes one event
|
||||
func (eh *EventHandler) RedoOneEvent() {
|
||||
t := eh.RedoStack.Pop()
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Modifies the text event
|
||||
UndoTextEvent(t, eh.buf)
|
||||
|
||||
teCursor := t.C
|
||||
if teCursor.Num >= 0 && teCursor.Num < len(eh.cursors) {
|
||||
t.C = *eh.cursors[teCursor.Num]
|
||||
eh.cursors[teCursor.Num].Goto(teCursor)
|
||||
} else {
|
||||
teCursor.Num = -1
|
||||
}
|
||||
|
||||
eh.UndoStack.Push(t)
|
||||
}
|
||||
312
internal/buffer/line_array.go
Normal file
312
internal/buffer/line_array.go
Normal file
@@ -0,0 +1,312 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/zyedidia/micro/pkg/highlight"
|
||||
)
|
||||
|
||||
// Finds the byte index of the nth rune in a byte slice
|
||||
func runeToByteIndex(n int, txt []byte) int {
|
||||
if n == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
count := 0
|
||||
i := 0
|
||||
for len(txt) > 0 {
|
||||
_, size := utf8.DecodeRune(txt)
|
||||
|
||||
txt = txt[size:]
|
||||
count += size
|
||||
i++
|
||||
|
||||
if i == n {
|
||||
break
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// A Line contains the data in bytes as well as a highlight state, match
|
||||
// and a flag for whether the highlighting needs to be updated
|
||||
type Line struct {
|
||||
data []byte
|
||||
|
||||
state highlight.State
|
||||
match highlight.LineMatch
|
||||
rehighlight bool
|
||||
}
|
||||
|
||||
const (
|
||||
// Line ending file formats
|
||||
FFAuto = 0 // Autodetect format
|
||||
FFUnix = 1 // LF line endings (unix style '\n')
|
||||
FFDos = 2 // CRLF line endings (dos style '\r\n')
|
||||
)
|
||||
|
||||
type FileFormat byte
|
||||
|
||||
// A LineArray simply stores and array of lines and makes it easy to insert
|
||||
// and delete in it
|
||||
type LineArray struct {
|
||||
lines []Line
|
||||
Endings FileFormat
|
||||
initsize uint64
|
||||
}
|
||||
|
||||
// Append efficiently appends lines together
|
||||
// It allocates an additional 10000 lines if the original estimate
|
||||
// is incorrect
|
||||
func Append(slice []Line, data ...Line) []Line {
|
||||
l := len(slice)
|
||||
if l+len(data) > cap(slice) { // reallocate
|
||||
newSlice := make([]Line, (l+len(data))+10000)
|
||||
copy(newSlice, slice)
|
||||
slice = newSlice
|
||||
}
|
||||
slice = slice[0 : l+len(data)]
|
||||
for i, c := range data {
|
||||
slice[l+i] = c
|
||||
}
|
||||
return slice
|
||||
}
|
||||
|
||||
// NewLineArray returns a new line array from an array of bytes
|
||||
func NewLineArray(size uint64, endings FileFormat, reader io.Reader) *LineArray {
|
||||
la := new(LineArray)
|
||||
|
||||
la.lines = make([]Line, 0, 1000)
|
||||
la.initsize = size
|
||||
|
||||
br := bufio.NewReader(reader)
|
||||
var loaded int
|
||||
|
||||
n := 0
|
||||
for {
|
||||
data, err := br.ReadBytes('\n')
|
||||
// Detect the line ending by checking to see if there is a '\r' char
|
||||
// before the '\n'
|
||||
// Even if the file format is set to DOS, the '\r' is removed so
|
||||
// that all lines end with '\n'
|
||||
dlen := len(data)
|
||||
if dlen > 1 && data[dlen-2] == '\r' {
|
||||
data = append(data[:dlen-2], '\n')
|
||||
if endings == FFAuto {
|
||||
la.Endings = FFDos
|
||||
}
|
||||
dlen = len(data)
|
||||
} else if dlen > 0 {
|
||||
if endings == FFAuto {
|
||||
la.Endings = FFUnix
|
||||
}
|
||||
}
|
||||
|
||||
// If we are loading a large file (greater than 1000) we use the file
|
||||
// size and the length of the first 1000 lines to try to estimate
|
||||
// how many lines will need to be allocated for the rest of the file
|
||||
// We add an extra 10000 to the original estimate to be safe and give
|
||||
// plenty of room for expansion
|
||||
if n >= 1000 && loaded >= 0 {
|
||||
totalLinesNum := int(float64(size) * (float64(n) / float64(loaded)))
|
||||
newSlice := make([]Line, len(la.lines), totalLinesNum+10000)
|
||||
copy(newSlice, la.lines)
|
||||
la.lines = newSlice
|
||||
loaded = -1
|
||||
}
|
||||
|
||||
// Counter for the number of bytes in the first 1000 lines
|
||||
if loaded >= 0 {
|
||||
loaded += dlen
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
la.lines = Append(la.lines, Line{data[:], nil, nil, false})
|
||||
}
|
||||
// Last line was read
|
||||
break
|
||||
} else {
|
||||
la.lines = Append(la.lines, Line{data[:dlen-1], nil, nil, false})
|
||||
}
|
||||
n++
|
||||
}
|
||||
|
||||
return la
|
||||
}
|
||||
|
||||
// Bytes returns the string that should be written to disk when
|
||||
// the line array is saved
|
||||
func (la *LineArray) Bytes() []byte {
|
||||
str := make([]byte, 0, la.initsize+1000) // initsize should provide a good estimate
|
||||
for i, l := range la.lines {
|
||||
str = append(str, l.data...)
|
||||
if i != len(la.lines)-1 {
|
||||
if la.Endings == FFDos {
|
||||
str = append(str, '\r')
|
||||
}
|
||||
str = append(str, '\n')
|
||||
}
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
// newlineBelow adds a newline below the given line number
|
||||
func (la *LineArray) newlineBelow(y int) {
|
||||
la.lines = append(la.lines, Line{[]byte{' '}, nil, nil, false})
|
||||
copy(la.lines[y+2:], la.lines[y+1:])
|
||||
la.lines[y+1] = Line{[]byte{}, la.lines[y].state, nil, false}
|
||||
}
|
||||
|
||||
// Inserts a byte array at a given location
|
||||
func (la *LineArray) insert(pos Loc, value []byte) {
|
||||
x, y := runeToByteIndex(pos.X, la.lines[pos.Y].data), pos.Y
|
||||
for i := 0; i < len(value); i++ {
|
||||
if value[i] == '\n' {
|
||||
la.split(Loc{x, y})
|
||||
x = 0
|
||||
y++
|
||||
continue
|
||||
}
|
||||
la.insertByte(Loc{x, y}, value[i])
|
||||
x++
|
||||
}
|
||||
}
|
||||
|
||||
// InsertByte inserts a byte at a given location
|
||||
func (la *LineArray) insertByte(pos Loc, value byte) {
|
||||
la.lines[pos.Y].data = append(la.lines[pos.Y].data, 0)
|
||||
copy(la.lines[pos.Y].data[pos.X+1:], la.lines[pos.Y].data[pos.X:])
|
||||
la.lines[pos.Y].data[pos.X] = value
|
||||
}
|
||||
|
||||
// joinLines joins the two lines a and b
|
||||
func (la *LineArray) joinLines(a, b int) {
|
||||
la.insert(Loc{len(la.lines[a].data), a}, la.lines[b].data)
|
||||
la.deleteLine(b)
|
||||
}
|
||||
|
||||
// split splits a line at a given position
|
||||
func (la *LineArray) split(pos Loc) {
|
||||
la.newlineBelow(pos.Y)
|
||||
la.insert(Loc{0, pos.Y + 1}, la.lines[pos.Y].data[pos.X:])
|
||||
la.lines[pos.Y+1].state = la.lines[pos.Y].state
|
||||
la.lines[pos.Y].state = nil
|
||||
la.lines[pos.Y].match = nil
|
||||
la.lines[pos.Y+1].match = nil
|
||||
la.lines[pos.Y].rehighlight = true
|
||||
la.deleteToEnd(Loc{pos.X, pos.Y})
|
||||
}
|
||||
|
||||
// removes from start to end
|
||||
func (la *LineArray) remove(start, end Loc) []byte {
|
||||
sub := la.Substr(start, end)
|
||||
startX := runeToByteIndex(start.X, la.lines[start.Y].data)
|
||||
endX := runeToByteIndex(end.X, la.lines[end.Y].data)
|
||||
if start.Y == end.Y {
|
||||
la.lines[start.Y].data = append(la.lines[start.Y].data[:startX], la.lines[start.Y].data[endX:]...)
|
||||
} else {
|
||||
for i := start.Y + 1; i <= end.Y-1; i++ {
|
||||
la.deleteLine(start.Y + 1)
|
||||
}
|
||||
la.deleteToEnd(Loc{startX, start.Y})
|
||||
la.deleteFromStart(Loc{endX - 1, start.Y + 1})
|
||||
la.joinLines(start.Y, start.Y+1)
|
||||
}
|
||||
return sub
|
||||
}
|
||||
|
||||
// deleteToEnd deletes from the end of a line to the position
|
||||
func (la *LineArray) deleteToEnd(pos Loc) {
|
||||
la.lines[pos.Y].data = la.lines[pos.Y].data[:pos.X]
|
||||
}
|
||||
|
||||
// deleteFromStart deletes from the start of a line to the position
|
||||
func (la *LineArray) deleteFromStart(pos Loc) {
|
||||
la.lines[pos.Y].data = la.lines[pos.Y].data[pos.X+1:]
|
||||
}
|
||||
|
||||
// deleteLine deletes the line number
|
||||
func (la *LineArray) deleteLine(y int) {
|
||||
la.lines = la.lines[:y+copy(la.lines[y:], la.lines[y+1:])]
|
||||
}
|
||||
|
||||
// DeleteByte deletes the byte at a position
|
||||
func (la *LineArray) deleteByte(pos Loc) {
|
||||
la.lines[pos.Y].data = la.lines[pos.Y].data[:pos.X+copy(la.lines[pos.Y].data[pos.X:], la.lines[pos.Y].data[pos.X+1:])]
|
||||
}
|
||||
|
||||
// Substr returns the string representation between two locations
|
||||
func (la *LineArray) Substr(start, end Loc) []byte {
|
||||
startX := runeToByteIndex(start.X, la.lines[start.Y].data)
|
||||
endX := runeToByteIndex(end.X, la.lines[end.Y].data)
|
||||
if start.Y == end.Y {
|
||||
src := la.lines[start.Y].data[startX:endX]
|
||||
dest := make([]byte, len(src))
|
||||
copy(dest, src)
|
||||
return dest
|
||||
}
|
||||
str := make([]byte, 0, len(la.lines[start.Y+1].data)*(end.Y-start.Y))
|
||||
str = append(str, la.lines[start.Y].data[startX:]...)
|
||||
str = append(str, '\n')
|
||||
for i := start.Y + 1; i <= end.Y-1; i++ {
|
||||
str = append(str, la.lines[i].data...)
|
||||
str = append(str, '\n')
|
||||
}
|
||||
str = append(str, la.lines[end.Y].data[:endX]...)
|
||||
return str
|
||||
}
|
||||
|
||||
// LinesNum returns the number of lines in the buffer
|
||||
func (la *LineArray) LinesNum() int {
|
||||
return len(la.lines)
|
||||
}
|
||||
|
||||
// Start returns the start of the buffer
|
||||
func (la *LineArray) Start() Loc {
|
||||
return Loc{0, 0}
|
||||
}
|
||||
|
||||
// End returns the location of the last character in the buffer
|
||||
func (la *LineArray) End() Loc {
|
||||
numlines := len(la.lines)
|
||||
return Loc{utf8.RuneCount(la.lines[numlines-1].data), numlines - 1}
|
||||
}
|
||||
|
||||
// LineBytes returns line n as an array of bytes
|
||||
func (la *LineArray) LineBytes(n int) []byte {
|
||||
if n >= len(la.lines) || n < 0 {
|
||||
return []byte{}
|
||||
}
|
||||
return la.lines[n].data
|
||||
}
|
||||
|
||||
// State gets the highlight state for the given line number
|
||||
func (la *LineArray) State(lineN int) highlight.State {
|
||||
return la.lines[lineN].state
|
||||
}
|
||||
|
||||
// SetState sets the highlight state at the given line number
|
||||
func (la *LineArray) SetState(lineN int, s highlight.State) {
|
||||
la.lines[lineN].state = s
|
||||
}
|
||||
|
||||
// SetMatch sets the match at the given line number
|
||||
func (la *LineArray) SetMatch(lineN int, m highlight.LineMatch) {
|
||||
la.lines[lineN].match = m
|
||||
}
|
||||
|
||||
// Match retrieves the match for the given line number
|
||||
func (la *LineArray) Match(lineN int) highlight.LineMatch {
|
||||
return la.lines[lineN].match
|
||||
}
|
||||
|
||||
func (la *LineArray) Rehighlight(lineN int) bool {
|
||||
return la.lines[lineN].rehighlight
|
||||
}
|
||||
|
||||
func (la *LineArray) SetRehighlight(lineN int, on bool) {
|
||||
la.lines[lineN].rehighlight = on
|
||||
}
|
||||
60
internal/buffer/line_array_test.go
Normal file
60
internal/buffer/line_array_test.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var unicode_txt = `An preost wes on leoden, Laȝamon was ihoten
|
||||
He wes Leovenaðes sone -- liðe him be Drihten.
|
||||
He wonede at Ernleȝe at æðelen are chirechen,
|
||||
Uppen Sevarne staþe, sel þar him þuhte,
|
||||
Onfest Radestone, þer he bock radde.`
|
||||
|
||||
var la *LineArray
|
||||
|
||||
func init() {
|
||||
reader := strings.NewReader(unicode_txt)
|
||||
la = NewLineArray(uint64(len(unicode_txt)), FFAuto, reader)
|
||||
}
|
||||
|
||||
func TestSplit(t *testing.T) {
|
||||
la.insert(Loc{17, 1}, []byte{'\n'})
|
||||
assert.Equal(t, len(la.lines), 6)
|
||||
sub1 := la.Substr(Loc{0, 1}, Loc{17, 1})
|
||||
sub2 := la.Substr(Loc{0, 2}, Loc{30, 2})
|
||||
|
||||
assert.Equal(t, []byte("He wes Leovenaðes"), sub1)
|
||||
assert.Equal(t, []byte(" sone -- liðe him be Drihten."), sub2)
|
||||
}
|
||||
|
||||
func TestJoin(t *testing.T) {
|
||||
la.remove(Loc{47, 1}, Loc{0, 2})
|
||||
assert.Equal(t, len(la.lines), 5)
|
||||
sub := la.Substr(Loc{0, 1}, Loc{47, 1})
|
||||
bytes := la.Bytes()
|
||||
|
||||
assert.Equal(t, []byte("He wes Leovenaðes sone -- liðe him be Drihten."), sub)
|
||||
assert.Equal(t, unicode_txt, string(bytes))
|
||||
}
|
||||
|
||||
func TestInsert(t *testing.T) {
|
||||
la.insert(Loc{20, 3}, []byte(" foobar"))
|
||||
sub1 := la.Substr(Loc{0, 3}, Loc{50, 3})
|
||||
|
||||
assert.Equal(t, []byte("Uppen Sevarne staþe, foobar sel þar him þuhte,"), sub1)
|
||||
|
||||
la.insert(Loc{25, 2}, []byte("ಮಣ್ಣಾಗಿ"))
|
||||
sub2 := la.Substr(Loc{0, 2}, Loc{60, 2})
|
||||
assert.Equal(t, []byte("He wonede at Ernleȝe at æಮಣ್ಣಾಗಿðelen are chirechen,"), sub2)
|
||||
}
|
||||
|
||||
func TestRemove(t *testing.T) {
|
||||
la.remove(Loc{20, 3}, Loc{27, 3})
|
||||
la.remove(Loc{25, 2}, Loc{32, 2})
|
||||
|
||||
bytes := la.Bytes()
|
||||
assert.Equal(t, unicode_txt, string(bytes))
|
||||
}
|
||||
125
internal/buffer/loc.go
Normal file
125
internal/buffer/loc.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/zyedidia/micro/internal/util"
|
||||
)
|
||||
|
||||
// Loc stores a location
|
||||
type Loc struct {
|
||||
X, Y int
|
||||
}
|
||||
|
||||
// LessThan returns true if b is smaller
|
||||
func (l Loc) LessThan(b Loc) bool {
|
||||
if l.Y < b.Y {
|
||||
return true
|
||||
}
|
||||
return l.Y == b.Y && l.X < b.X
|
||||
}
|
||||
|
||||
// GreaterThan returns true if b is bigger
|
||||
func (l Loc) GreaterThan(b Loc) bool {
|
||||
if l.Y > b.Y {
|
||||
return true
|
||||
}
|
||||
return l.Y == b.Y && l.X > b.X
|
||||
}
|
||||
|
||||
// GreaterEqual returns true if b is greater than or equal to b
|
||||
func (l Loc) GreaterEqual(b Loc) bool {
|
||||
if l.Y > b.Y {
|
||||
return true
|
||||
}
|
||||
if l.Y == b.Y && l.X > b.X {
|
||||
return true
|
||||
}
|
||||
return l == b
|
||||
}
|
||||
|
||||
// LessEqual returns true if b is less than or equal to b
|
||||
func (l Loc) LessEqual(b Loc) bool {
|
||||
if l.Y < b.Y {
|
||||
return true
|
||||
}
|
||||
if l.Y == b.Y && l.X < b.X {
|
||||
return true
|
||||
}
|
||||
return l == b
|
||||
}
|
||||
|
||||
// The following functions require a buffer to know where newlines are
|
||||
|
||||
// Diff returns the distance between two locations
|
||||
func DiffLA(a, b Loc, buf *LineArray) int {
|
||||
if a.Y == b.Y {
|
||||
if a.X > b.X {
|
||||
return a.X - b.X
|
||||
}
|
||||
return b.X - a.X
|
||||
}
|
||||
|
||||
// Make sure a is guaranteed to be less than b
|
||||
if b.LessThan(a) {
|
||||
a, b = b, a
|
||||
}
|
||||
|
||||
loc := 0
|
||||
for i := a.Y + 1; i < b.Y; i++ {
|
||||
// + 1 for the newline
|
||||
loc += utf8.RuneCount(buf.LineBytes(i)) + 1
|
||||
}
|
||||
loc += utf8.RuneCount(buf.LineBytes(a.Y)) - a.X + b.X + 1
|
||||
return loc
|
||||
}
|
||||
|
||||
// This moves the location one character to the right
|
||||
func (l Loc) right(buf *LineArray) Loc {
|
||||
if l == buf.End() {
|
||||
return Loc{l.X + 1, l.Y}
|
||||
}
|
||||
var res Loc
|
||||
if l.X < utf8.RuneCount(buf.LineBytes(l.Y)) {
|
||||
res = Loc{l.X + 1, l.Y}
|
||||
} else {
|
||||
res = Loc{0, l.Y + 1}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// This moves the given location one character to the left
|
||||
func (l Loc) left(buf *LineArray) Loc {
|
||||
if l == buf.Start() {
|
||||
return Loc{l.X - 1, l.Y}
|
||||
}
|
||||
var res Loc
|
||||
if l.X > 0 {
|
||||
res = Loc{l.X - 1, l.Y}
|
||||
} else {
|
||||
res = Loc{utf8.RuneCount(buf.LineBytes(l.Y - 1)), l.Y - 1}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Move moves the cursor n characters to the left or right
|
||||
// It moves the cursor left if n is negative
|
||||
func (l Loc) MoveLA(n int, buf *LineArray) Loc {
|
||||
if n > 0 {
|
||||
for i := 0; i < n; i++ {
|
||||
l = l.right(buf)
|
||||
}
|
||||
return l
|
||||
}
|
||||
for i := 0; i < util.Abs(n); i++ {
|
||||
l = l.left(buf)
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
func (l Loc) Diff(a, b Loc, buf *Buffer) int {
|
||||
return DiffLA(a, b, buf.LineArray)
|
||||
}
|
||||
func (l Loc) Move(n int, buf *Buffer) Loc {
|
||||
return l.MoveLA(n, buf.LineArray)
|
||||
}
|
||||
77
internal/buffer/message.go
Normal file
77
internal/buffer/message.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"github.com/zyedidia/micro/internal/config"
|
||||
"github.com/zyedidia/tcell"
|
||||
)
|
||||
|
||||
type MsgType int
|
||||
|
||||
const (
|
||||
MTInfo = iota
|
||||
MTWarning
|
||||
MTError
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
Msg string
|
||||
Start, End Loc
|
||||
Kind MsgType
|
||||
Owner int
|
||||
}
|
||||
|
||||
func NewMessage(owner int, msg string, start, end Loc, kind MsgType) *Message {
|
||||
return &Message{
|
||||
Msg: msg,
|
||||
Start: start,
|
||||
End: end,
|
||||
Kind: kind,
|
||||
Owner: owner,
|
||||
}
|
||||
}
|
||||
|
||||
func NewMessageAtLine(owner int, msg string, line int, kind MsgType) *Message {
|
||||
start := Loc{-1, line}
|
||||
end := start
|
||||
return NewMessage(owner, msg, start, end, kind)
|
||||
}
|
||||
|
||||
func (m *Message) Style() tcell.Style {
|
||||
switch m.Kind {
|
||||
case MTInfo:
|
||||
if style, ok := config.Colorscheme["gutter-info"]; ok {
|
||||
return style
|
||||
}
|
||||
case MTWarning:
|
||||
if style, ok := config.Colorscheme["gutter-warning"]; ok {
|
||||
return style
|
||||
}
|
||||
case MTError:
|
||||
if style, ok := config.Colorscheme["gutter-error"]; ok {
|
||||
return style
|
||||
}
|
||||
}
|
||||
return config.DefStyle
|
||||
}
|
||||
|
||||
func (b *Buffer) AddMessage(m *Message) {
|
||||
b.Messages = append(b.Messages, m)
|
||||
}
|
||||
|
||||
func (b *Buffer) removeMsg(i int) {
|
||||
copy(b.Messages[i:], b.Messages[i+1:])
|
||||
b.Messages[len(b.Messages)-1] = nil
|
||||
b.Messages = b.Messages[:len(b.Messages)-1]
|
||||
}
|
||||
|
||||
func (b *Buffer) ClearMessages(owner int) {
|
||||
for i := len(b.Messages) - 1; i >= 0; i-- {
|
||||
if b.Messages[i].Owner == owner {
|
||||
b.removeMsg(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Buffer) ClearAllMessages() {
|
||||
b.Messages = make([]*Message, 0)
|
||||
}
|
||||
207
internal/buffer/save.go
Normal file
207
internal/buffer/save.go
Normal file
@@ -0,0 +1,207 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/zyedidia/micro/internal/config"
|
||||
. "github.com/zyedidia/micro/internal/util"
|
||||
"golang.org/x/text/encoding"
|
||||
"golang.org/x/text/encoding/htmlindex"
|
||||
"golang.org/x/text/transform"
|
||||
)
|
||||
|
||||
// LargeFileThreshold is the number of bytes when fastdirty is forced
|
||||
// because hashing is too slow
|
||||
const LargeFileThreshold = 50000
|
||||
|
||||
// overwriteFile opens the given file for writing, truncating if one exists, and then calls
|
||||
// the supplied function with the file as io.Writer object, also making sure the file is
|
||||
// closed afterwards.
|
||||
func overwriteFile(name string, enc encoding.Encoding, fn func(io.Writer) error) (err error) {
|
||||
var file *os.File
|
||||
|
||||
if file, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if e := file.Close(); e != nil && err == nil {
|
||||
err = e
|
||||
}
|
||||
}()
|
||||
|
||||
w := transform.NewWriter(file, enc.NewEncoder())
|
||||
// w := bufio.NewWriter(file)
|
||||
|
||||
if err = fn(w); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// err = w.Flush()
|
||||
return
|
||||
}
|
||||
|
||||
// Save saves the buffer to its default path
|
||||
func (b *Buffer) Save() error {
|
||||
return b.SaveAs(b.Path)
|
||||
}
|
||||
|
||||
// SaveAs saves the buffer to a specified path (filename), creating the file if it does not exist
|
||||
func (b *Buffer) SaveAs(filename string) error {
|
||||
if b.Type.Scratch {
|
||||
return errors.New("Cannot save scratch buffer")
|
||||
}
|
||||
|
||||
b.UpdateRules()
|
||||
if b.Settings["rmtrailingws"].(bool) {
|
||||
for i, l := range b.lines {
|
||||
leftover := utf8.RuneCount(bytes.TrimRightFunc(l.data, unicode.IsSpace))
|
||||
|
||||
linelen := utf8.RuneCount(l.data)
|
||||
b.Remove(Loc{leftover, i}, Loc{linelen, i})
|
||||
}
|
||||
|
||||
b.RelocateCursors()
|
||||
}
|
||||
|
||||
if b.Settings["eofnewline"].(bool) {
|
||||
end := b.End()
|
||||
if b.RuneAt(Loc{end.X - 1, end.Y}) != '\n' {
|
||||
b.Insert(end, "\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Update the last time this file was updated after saving
|
||||
defer func() {
|
||||
b.ModTime, _ = GetModTime(filename)
|
||||
}()
|
||||
|
||||
// Removes any tilde and replaces with the absolute path to home
|
||||
absFilename, _ := ReplaceHome(filename)
|
||||
|
||||
// TODO: save creates parent dirs
|
||||
// // Get the leading path to the file | "." is returned if there's no leading path provided
|
||||
// if dirname := filepath.Dir(absFilename); dirname != "." {
|
||||
// // Check if the parent dirs don't exist
|
||||
// if _, statErr := os.Stat(dirname); os.IsNotExist(statErr) {
|
||||
// // Prompt to make sure they want to create the dirs that are missing
|
||||
// if yes, canceled := messenger.YesNoPrompt("Parent folders \"" + dirname + "\" do not exist. Create them? (y,n)"); yes && !canceled {
|
||||
// // Create all leading dir(s) since they don't exist
|
||||
// if mkdirallErr := os.MkdirAll(dirname, os.ModePerm); mkdirallErr != nil {
|
||||
// // If there was an error creating the dirs
|
||||
// return mkdirallErr
|
||||
// }
|
||||
// } else {
|
||||
// // If they canceled the creation of leading dirs
|
||||
// return errors.New("Save aborted")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
var fileSize int
|
||||
|
||||
enc, err := htmlindex.Get(b.Settings["encoding"].(string))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = overwriteFile(absFilename, enc, func(file io.Writer) (e error) {
|
||||
if len(b.lines) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// end of line
|
||||
var eol []byte
|
||||
if b.Endings == FFDos {
|
||||
eol = []byte{'\r', '\n'}
|
||||
} else {
|
||||
eol = []byte{'\n'}
|
||||
}
|
||||
|
||||
// write lines
|
||||
if fileSize, e = file.Write(b.lines[0].data); e != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, l := range b.lines[1:] {
|
||||
if _, e = file.Write(eol); e != nil {
|
||||
return
|
||||
}
|
||||
if _, e = file.Write(l.data); e != nil {
|
||||
return
|
||||
}
|
||||
fileSize += len(eol) + len(l.data)
|
||||
}
|
||||
return
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !b.Settings["fastdirty"].(bool) {
|
||||
if fileSize > LargeFileThreshold {
|
||||
// For large files 'fastdirty' needs to be on
|
||||
b.Settings["fastdirty"] = true
|
||||
} else {
|
||||
calcHash(b, &b.origHash)
|
||||
}
|
||||
}
|
||||
|
||||
b.Path = filename
|
||||
absPath, _ := filepath.Abs(filename)
|
||||
b.AbsPath = absPath
|
||||
b.isModified = false
|
||||
return b.Serialize()
|
||||
}
|
||||
|
||||
// SaveWithSudo saves the buffer to the default path with sudo
|
||||
func (b *Buffer) SaveWithSudo() error {
|
||||
return b.SaveAsWithSudo(b.Path)
|
||||
}
|
||||
|
||||
// SaveAsWithSudo is the same as SaveAs except it uses a neat trick
|
||||
// with tee to use sudo so the user doesn't have to reopen micro with sudo
|
||||
func (b *Buffer) SaveAsWithSudo(filename string) error {
|
||||
if b.Type.Scratch {
|
||||
return errors.New("Cannot save scratch buffer")
|
||||
}
|
||||
|
||||
b.UpdateRules()
|
||||
b.Path = filename
|
||||
absPath, _ := filepath.Abs(filename)
|
||||
b.AbsPath = absPath
|
||||
|
||||
// Set up everything for the command
|
||||
cmd := exec.Command(config.GlobalSettings["sucmd"].(string), "tee", filename)
|
||||
cmd.Stdin = bytes.NewBuffer(b.Bytes())
|
||||
|
||||
// This is a trap for Ctrl-C so that it doesn't kill micro
|
||||
// Instead we trap Ctrl-C to kill the program we're running
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt)
|
||||
go func() {
|
||||
for range c {
|
||||
cmd.Process.Kill()
|
||||
}
|
||||
}()
|
||||
|
||||
// Start the command
|
||||
cmd.Start()
|
||||
err := cmd.Wait()
|
||||
|
||||
if err == nil {
|
||||
b.isModified = false
|
||||
b.ModTime, _ = GetModTime(filename)
|
||||
return b.Serialize()
|
||||
}
|
||||
return err
|
||||
}
|
||||
170
internal/buffer/search.go
Normal file
170
internal/buffer/search.go
Normal file
@@ -0,0 +1,170 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/zyedidia/micro/internal/util"
|
||||
)
|
||||
|
||||
func (b *Buffer) findDown(r *regexp.Regexp, start, end Loc) ([2]Loc, bool) {
|
||||
start.Y = util.Clamp(start.Y, 0, b.LinesNum()-1)
|
||||
end.Y = util.Clamp(end.Y, 0, b.LinesNum()-1)
|
||||
|
||||
if start.GreaterThan(end) {
|
||||
start, end = end, start
|
||||
}
|
||||
|
||||
for i := start.Y; i <= end.Y; i++ {
|
||||
l := b.LineBytes(i)
|
||||
charpos := 0
|
||||
|
||||
if i == start.Y && start.Y == end.Y {
|
||||
nchars := utf8.RuneCount(l)
|
||||
start.X = util.Clamp(start.X, 0, nchars-1)
|
||||
end.X = util.Clamp(end.X, 0, nchars-1)
|
||||
l = util.SliceStart(l, end.X)
|
||||
l = util.SliceEnd(l, start.X)
|
||||
charpos = start.X
|
||||
} else if i == start.Y {
|
||||
nchars := utf8.RuneCount(l)
|
||||
start.X = util.Clamp(start.X, 0, nchars-1)
|
||||
l = util.SliceEnd(l, start.X)
|
||||
charpos = start.X
|
||||
} else if i == end.Y {
|
||||
nchars := utf8.RuneCount(l)
|
||||
end.X = util.Clamp(end.X, 0, nchars-1)
|
||||
l = util.SliceStart(l, end.X)
|
||||
}
|
||||
|
||||
match := r.FindIndex(l)
|
||||
|
||||
if match != nil {
|
||||
start := Loc{charpos + util.RunePos(l, match[0]), i}
|
||||
end := Loc{charpos + util.RunePos(l, match[1]), i}
|
||||
return [2]Loc{start, end}, true
|
||||
}
|
||||
}
|
||||
return [2]Loc{}, false
|
||||
}
|
||||
|
||||
func (b *Buffer) findUp(r *regexp.Regexp, start, end Loc) ([2]Loc, bool) {
|
||||
start.Y = util.Clamp(start.Y, 0, b.LinesNum()-1)
|
||||
end.Y = util.Clamp(end.Y, 0, b.LinesNum()-1)
|
||||
|
||||
if start.GreaterThan(end) {
|
||||
start, end = end, start
|
||||
}
|
||||
|
||||
for i := end.Y; i >= start.Y; i-- {
|
||||
l := b.LineBytes(i)
|
||||
charpos := 0
|
||||
|
||||
if i == start.Y && start.Y == end.Y {
|
||||
nchars := utf8.RuneCount(l)
|
||||
start.X = util.Clamp(start.X, 0, nchars-1)
|
||||
end.X = util.Clamp(end.X, 0, nchars-1)
|
||||
l = util.SliceStart(l, end.X)
|
||||
l = util.SliceEnd(l, start.X)
|
||||
charpos = start.X
|
||||
} else if i == start.Y {
|
||||
nchars := utf8.RuneCount(l)
|
||||
start.X = util.Clamp(start.X, 0, nchars-1)
|
||||
l = util.SliceEnd(l, start.X)
|
||||
charpos = start.X
|
||||
} else if i == end.Y {
|
||||
nchars := utf8.RuneCount(l)
|
||||
end.X = util.Clamp(end.X, 0, nchars-1)
|
||||
l = util.SliceStart(l, end.X)
|
||||
}
|
||||
|
||||
match := r.FindIndex(l)
|
||||
|
||||
if match != nil {
|
||||
start := Loc{charpos + util.RunePos(l, match[0]), i}
|
||||
end := Loc{charpos + util.RunePos(l, match[1]), i}
|
||||
return [2]Loc{start, end}, true
|
||||
}
|
||||
}
|
||||
return [2]Loc{}, false
|
||||
}
|
||||
|
||||
// FindNext finds the next occurrence of a given string in the buffer
|
||||
// It returns the start and end location of the match (if found) and
|
||||
// a boolean indicating if it was found
|
||||
// May also return an error if the search regex is invalid
|
||||
func (b *Buffer) FindNext(s string, start, end, from Loc, down bool, useRegex bool) ([2]Loc, bool, error) {
|
||||
if s == "" {
|
||||
return [2]Loc{}, false, nil
|
||||
}
|
||||
|
||||
var r *regexp.Regexp
|
||||
var err error
|
||||
|
||||
if !useRegex {
|
||||
s = regexp.QuoteMeta(s)
|
||||
}
|
||||
|
||||
if b.Settings["ignorecase"].(bool) {
|
||||
r, err = regexp.Compile("(?i)" + s)
|
||||
} else {
|
||||
r, err = regexp.Compile(s)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return [2]Loc{}, false, err
|
||||
}
|
||||
|
||||
found := false
|
||||
var l [2]Loc
|
||||
if down {
|
||||
l, found = b.findDown(r, from, end)
|
||||
if !found {
|
||||
l, found = b.findDown(r, start, from)
|
||||
}
|
||||
} else {
|
||||
l, found = b.findUp(r, from, start)
|
||||
if !found {
|
||||
l, found = b.findUp(r, end, from)
|
||||
}
|
||||
}
|
||||
return l, found, nil
|
||||
}
|
||||
|
||||
// ReplaceRegex replaces all occurrences of 'search' with 'replace' in the given area
|
||||
// and returns the number of replacements made
|
||||
func (b *Buffer) ReplaceRegex(start, end Loc, search *regexp.Regexp, replace []byte) int {
|
||||
if start.GreaterThan(end) {
|
||||
start, end = end, start
|
||||
}
|
||||
|
||||
found := 0
|
||||
var deltas []Delta
|
||||
for i := start.Y; i <= end.Y; i++ {
|
||||
l := b.lines[i].data
|
||||
charpos := 0
|
||||
|
||||
if start.Y == end.Y && i == start.Y {
|
||||
l = util.SliceStart(l, end.X)
|
||||
l = util.SliceEnd(l, start.X)
|
||||
charpos = start.X
|
||||
} else if i == start.Y {
|
||||
l = util.SliceEnd(l, start.X)
|
||||
charpos = start.X
|
||||
} else if i == end.Y {
|
||||
l = util.SliceStart(l, end.X)
|
||||
}
|
||||
newText := search.ReplaceAllFunc(l, func(in []byte) []byte {
|
||||
found++
|
||||
return replace
|
||||
})
|
||||
|
||||
from := Loc{charpos, i}
|
||||
to := Loc{charpos + utf8.RuneCount(l), i}
|
||||
|
||||
deltas = append(deltas, Delta{newText, from, to})
|
||||
}
|
||||
b.MultipleReplace(deltas)
|
||||
|
||||
return found
|
||||
}
|
||||
73
internal/buffer/serialize.go
Normal file
73
internal/buffer/serialize.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/zyedidia/micro/internal/config"
|
||||
. "github.com/zyedidia/micro/internal/util"
|
||||
"golang.org/x/text/encoding/unicode"
|
||||
)
|
||||
|
||||
// The SerializedBuffer holds the types that get serialized when a buffer is saved
|
||||
// These are used for the savecursor and saveundo options
|
||||
type SerializedBuffer struct {
|
||||
EventHandler *EventHandler
|
||||
Cursor Loc
|
||||
ModTime time.Time
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register(TextEvent{})
|
||||
gob.Register(SerializedBuffer{})
|
||||
}
|
||||
|
||||
// Serialize serializes the buffer to config.ConfigDir/buffers
|
||||
func (b *Buffer) Serialize() error {
|
||||
if !b.Settings["savecursor"].(bool) && !b.Settings["saveundo"].(bool) {
|
||||
return nil
|
||||
}
|
||||
|
||||
name := config.ConfigDir + "/buffers/" + EscapePath(b.AbsPath)
|
||||
|
||||
return overwriteFile(name, unicode.UTF8, func(file io.Writer) error {
|
||||
err := gob.NewEncoder(file).Encode(SerializedBuffer{
|
||||
b.EventHandler,
|
||||
b.GetActiveCursor().Loc,
|
||||
b.ModTime,
|
||||
})
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (b *Buffer) Unserialize() error {
|
||||
// If either savecursor or saveundo is turned on, we need to load the serialized information
|
||||
// from ~/.config/micro/buffers
|
||||
file, err := os.Open(config.ConfigDir + "/buffers/" + EscapePath(b.AbsPath))
|
||||
defer file.Close()
|
||||
if err == nil {
|
||||
var buffer SerializedBuffer
|
||||
decoder := gob.NewDecoder(file)
|
||||
gob.Register(TextEvent{})
|
||||
err = decoder.Decode(&buffer)
|
||||
if err != nil {
|
||||
return errors.New(err.Error() + "\nYou may want to remove the files in ~/.config/micro/buffers (these files store the information for the 'saveundo' and 'savecursor' options) if this problem persists.")
|
||||
}
|
||||
if b.Settings["savecursor"].(bool) {
|
||||
b.StartCursor = buffer.Cursor
|
||||
}
|
||||
|
||||
if b.Settings["saveundo"].(bool) {
|
||||
// We should only use last time's eventhandler if the file wasn't modified by someone else in the meantime
|
||||
if b.ModTime == buffer.ModTime {
|
||||
b.EventHandler = buffer.EventHandler
|
||||
b.EventHandler.cursors = b.cursors
|
||||
b.EventHandler.buf = b.SharedBuffer
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
51
internal/buffer/settings.go
Normal file
51
internal/buffer/settings.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"github.com/zyedidia/micro/internal/config"
|
||||
"github.com/zyedidia/micro/internal/screen"
|
||||
)
|
||||
|
||||
// SetOption sets a given option to a value just for this buffer
|
||||
func (b *Buffer) SetOption(option, value string) error {
|
||||
if _, ok := b.Settings[option]; !ok {
|
||||
return config.ErrInvalidOption
|
||||
}
|
||||
|
||||
nativeValue, err := config.GetNativeValue(option, b.Settings[option], value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.Settings[option] = nativeValue
|
||||
|
||||
if option == "fastdirty" {
|
||||
if !nativeValue.(bool) {
|
||||
e := calcHash(b, &b.origHash)
|
||||
if e == ErrFileTooLarge {
|
||||
b.Settings["fastdirty"] = false
|
||||
}
|
||||
}
|
||||
} else if option == "statusline" {
|
||||
screen.Redraw()
|
||||
} else if option == "filetype" {
|
||||
b.UpdateRules()
|
||||
} else if option == "fileformat" {
|
||||
switch b.Settings["fileformat"].(string) {
|
||||
case "unix":
|
||||
b.Endings = FFUnix
|
||||
case "dos":
|
||||
b.Endings = FFDos
|
||||
}
|
||||
b.isModified = true
|
||||
} else if option == "syntax" {
|
||||
if !nativeValue.(bool) {
|
||||
b.ClearMatches()
|
||||
} else {
|
||||
b.UpdateRules()
|
||||
}
|
||||
} else if option == "encoding" {
|
||||
b.isModified = true
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
43
internal/buffer/stack.go
Normal file
43
internal/buffer/stack.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package buffer
|
||||
|
||||
// TEStack is a simple implementation of a LIFO stack for text events
|
||||
type TEStack struct {
|
||||
Top *Element
|
||||
Size int
|
||||
}
|
||||
|
||||
// An Element which is stored in the Stack
|
||||
type Element struct {
|
||||
Value *TextEvent
|
||||
Next *Element
|
||||
}
|
||||
|
||||
// Len returns the stack's length
|
||||
func (s *TEStack) Len() int {
|
||||
return s.Size
|
||||
}
|
||||
|
||||
// Push a new element onto the stack
|
||||
func (s *TEStack) Push(value *TextEvent) {
|
||||
s.Top = &Element{value, s.Top}
|
||||
s.Size++
|
||||
}
|
||||
|
||||
// Pop removes the top element from the stack and returns its value
|
||||
// If the stack is empty, return nil
|
||||
func (s *TEStack) Pop() (value *TextEvent) {
|
||||
if s.Size > 0 {
|
||||
value, s.Top = s.Top.Value, s.Top.Next
|
||||
s.Size--
|
||||
return
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Peek returns the top element of the stack without removing it
|
||||
func (s *TEStack) Peek() *TextEvent {
|
||||
if s.Size > 0 {
|
||||
return s.Top.Value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
35
internal/buffer/stack_test.go
Normal file
35
internal/buffer/stack_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestStack(t *testing.T) {
|
||||
s := new(TEStack)
|
||||
e1 := &TextEvent{
|
||||
EventType: TextEventReplace,
|
||||
Time: time.Now(),
|
||||
}
|
||||
e2 := &TextEvent{
|
||||
EventType: TextEventInsert,
|
||||
Time: time.Now(),
|
||||
}
|
||||
s.Push(e1)
|
||||
s.Push(e2)
|
||||
|
||||
p := s.Peek()
|
||||
assert.Equal(t, p.EventType, TextEventInsert)
|
||||
p = s.Pop()
|
||||
assert.Equal(t, p.EventType, TextEventInsert)
|
||||
p = s.Peek()
|
||||
assert.Equal(t, p.EventType, TextEventReplace)
|
||||
p = s.Pop()
|
||||
assert.Equal(t, p.EventType, TextEventReplace)
|
||||
p = s.Pop()
|
||||
assert.Nil(t, p)
|
||||
p = s.Peek()
|
||||
assert.Nil(t, p)
|
||||
}
|
||||
Reference in New Issue
Block a user