Add some more comments

This commit is contained in:
Zachary Yedidia
2016-07-10 13:26:05 -04:00
parent 6489f4b6e8
commit 8c0983a36c
7 changed files with 105 additions and 19 deletions

View File

@@ -18,6 +18,7 @@ import (
type Buffer struct { type Buffer struct {
// The eventhandler for undo/redo // The eventhandler for undo/redo
*EventHandler *EventHandler
// This stores all the text in the buffer as an array of lines
*LineArray *LineArray
Cursor Cursor Cursor Cursor
@@ -27,6 +28,7 @@ type Buffer struct {
// Name of the buffer on the status line // Name of the buffer on the status line
Name string Name string
// Whether or not the buffer has been modified since it was opened
IsModified bool IsModified bool
// Stores the last modification time of the file the buffer is pointing to // Stores the last modification time of the file the buffer is pointing to
@@ -41,6 +43,7 @@ type Buffer struct {
} }
// The SerializedBuffer holds the types that get serialized when a buffer is saved // 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 { type SerializedBuffer struct {
EventHandler *EventHandler EventHandler *EventHandler
Cursor Cursor Cursor Cursor
@@ -55,10 +58,12 @@ func NewBuffer(txt []byte, path string) *Buffer {
b.Path = path b.Path = path
b.Name = path b.Name = path
// If the file doesn't have a path to disk then we give it no name
if path == "" { if path == "" {
b.Name = "No name" b.Name = "No name"
} }
// The last time this file was modified
b.ModTime, _ = GetModTime(b.Path) b.ModTime, _ = GetModTime(b.Path)
b.EventHandler = NewEventHandler(b) b.EventHandler = NewEventHandler(b)
@@ -80,6 +85,8 @@ func NewBuffer(txt []byte, path string) *Buffer {
} }
if settings["savecursor"].(bool) || settings["saveundo"].(bool) { if settings["savecursor"].(bool) || settings["saveundo"].(bool) {
// If either savecursor or saveundo is turned on, we need to load the serialized information
// from ~/.config/micro/buffers
absPath, _ := filepath.Abs(b.Path) absPath, _ := filepath.Abs(b.Path)
file, err := os.Open(configDir + "/buffers/" + EscapePath(absPath)) file, err := os.Open(configDir + "/buffers/" + EscapePath(absPath))
if err == nil { if err == nil {

View File

@@ -7,12 +7,24 @@ import (
"strings" "strings"
) )
// Jobs are the way plugins can run processes in the background
// A job is simply a process that gets executed asynchronously
// There are callbacks for when the job exits, when the job creates stdout
// and when the job creates stderr
// These jobs run in a separate goroutine but the lua callbacks need to be
// executed in the main thread (where the Lua VM is running) so they are
// put into the jobs channel which gets read by the main loop
// JobFunction is a representation of a job (this data structure is what is loaded
// into the jobs channel)
type JobFunction struct { type JobFunction struct {
function func(string, ...string) function func(string, ...string)
output string output string
args []string args []string
} }
// A CallbackFile is the data structure that makes it possible to catch stderr and stdout write events
type CallbackFile struct { type CallbackFile struct {
io.Writer io.Writer
@@ -21,16 +33,21 @@ type CallbackFile struct {
} }
func (f *CallbackFile) Write(data []byte) (int, error) { func (f *CallbackFile) Write(data []byte) (int, error) {
// This is either stderr or stdout
// In either case we create a new job function callback and put it in the jobs channel
jobFunc := JobFunction{f.callback, string(data), f.args} jobFunc := JobFunction{f.callback, string(data), f.args}
jobs <- jobFunc jobs <- jobFunc
return f.Writer.Write(data) return f.Writer.Write(data)
} }
// JobStart starts a process in the background with the given callbacks
// It returns an *exec.Cmd as the job id
func JobStart(cmd string, onStdout, onStderr, onExit string, userargs ...string) *exec.Cmd { func JobStart(cmd string, onStdout, onStderr, onExit string, userargs ...string) *exec.Cmd {
split := strings.Split(cmd, " ") split := strings.Split(cmd, " ")
args := split[1:] args := split[1:]
cmdName := split[0] cmdName := split[0]
// Set up everything correctly if the functions have been provided
proc := exec.Command(cmdName, args...) proc := exec.Command(cmdName, args...)
var outbuf bytes.Buffer var outbuf bytes.Buffer
if onStdout != "" { if onStdout != "" {
@@ -45,6 +62,7 @@ func JobStart(cmd string, onStdout, onStderr, onExit string, userargs ...string)
} }
go func() { go func() {
// Run the process in the background and create the onExit callback
proc.Run() proc.Run()
jobFunc := JobFunction{LuaFunctionJob(onExit), string(outbuf.Bytes()), userargs} jobFunc := JobFunction{LuaFunctionJob(onExit), string(outbuf.Bytes()), userargs}
jobs <- jobFunc jobs <- jobFunc
@@ -53,10 +71,12 @@ func JobStart(cmd string, onStdout, onStderr, onExit string, userargs ...string)
return proc return proc
} }
// JobStop kills a job
func JobStop(cmd *exec.Cmd) { func JobStop(cmd *exec.Cmd) {
cmd.Process.Kill() cmd.Process.Kill()
} }
// JobSend sends the given data into the job's stdin stream
func JobSend(cmd *exec.Cmd, data string) { func JobSend(cmd *exec.Cmd, data string) {
stdin, err := cmd.StdinPipe() stdin, err := cmd.StdinPipe()
if err != nil { if err != nil {

View File

@@ -26,12 +26,16 @@ func runeToByteIndex(n int, txt []byte) int {
return count return count
} }
// A LineArray simply stores and array of lines and makes it easy to insert
// and delete in it
type LineArray struct { type LineArray struct {
lines [][]byte lines [][]byte
} }
// NewLineArray returns a new line array from an array of bytes
func NewLineArray(text []byte) *LineArray { func NewLineArray(text []byte) *LineArray {
la := new(LineArray) la := new(LineArray)
// Split the bytes into lines
split := bytes.Split(text, []byte("\n")) split := bytes.Split(text, []byte("\n"))
la.lines = make([][]byte, len(split)) la.lines = make([][]byte, len(split))
for i := range split { for i := range split {
@@ -42,16 +46,19 @@ func NewLineArray(text []byte) *LineArray {
return la return la
} }
// Returns the String representation of the LineArray
func (la *LineArray) String() string { func (la *LineArray) String() string {
return string(bytes.Join(la.lines, []byte("\n"))) return string(bytes.Join(la.lines, []byte("\n")))
} }
// NewlineBelow adds a newline below the given line number
func (la *LineArray) NewlineBelow(y int) { func (la *LineArray) NewlineBelow(y int) {
la.lines = append(la.lines, []byte(" ")) la.lines = append(la.lines, []byte(" "))
copy(la.lines[y+2:], la.lines[y+1:]) copy(la.lines[y+2:], la.lines[y+1:])
la.lines[y+1] = []byte("") la.lines[y+1] = []byte("")
} }
// inserts a byte array at a given location
func (la *LineArray) insert(pos Loc, value []byte) { func (la *LineArray) insert(pos Loc, value []byte) {
x, y := runeToByteIndex(pos.X, la.lines[pos.Y]), pos.Y x, y := runeToByteIndex(pos.X, la.lines[pos.Y]), pos.Y
// x, y := pos.x, pos.y // x, y := pos.x, pos.y
@@ -67,23 +74,27 @@ func (la *LineArray) insert(pos Loc, value []byte) {
} }
} }
// inserts a byte at a given location
func (la *LineArray) insertByte(pos Loc, value byte) { func (la *LineArray) insertByte(pos Loc, value byte) {
la.lines[pos.Y] = append(la.lines[pos.Y], 0) la.lines[pos.Y] = append(la.lines[pos.Y], 0)
copy(la.lines[pos.Y][pos.X+1:], la.lines[pos.Y][pos.X:]) copy(la.lines[pos.Y][pos.X+1:], la.lines[pos.Y][pos.X:])
la.lines[pos.Y][pos.X] = value la.lines[pos.Y][pos.X] = value
} }
// JoinLines joins the two lines a and b
func (la *LineArray) JoinLines(a, b int) { func (la *LineArray) JoinLines(a, b int) {
la.insert(Loc{len(la.lines[a]), a}, la.lines[b]) la.insert(Loc{len(la.lines[a]), a}, la.lines[b])
la.DeleteLine(b) la.DeleteLine(b)
} }
// Split splits a line at a given position
func (la *LineArray) Split(pos Loc) { func (la *LineArray) Split(pos Loc) {
la.NewlineBelow(pos.Y) la.NewlineBelow(pos.Y)
la.insert(Loc{0, pos.Y + 1}, la.lines[pos.Y][pos.X:]) la.insert(Loc{0, pos.Y + 1}, la.lines[pos.Y][pos.X:])
la.DeleteToEnd(Loc{pos.X, pos.Y}) la.DeleteToEnd(Loc{pos.X, pos.Y})
} }
// removes from start to end
func (la *LineArray) remove(start, end Loc) string { func (la *LineArray) remove(start, end Loc) string {
sub := la.Substr(start, end) sub := la.Substr(start, end)
startX := runeToByteIndex(start.X, la.lines[start.Y]) startX := runeToByteIndex(start.X, la.lines[start.Y])
@@ -101,22 +112,27 @@ func (la *LineArray) remove(start, end Loc) string {
return sub return sub
} }
// DeleteToEnd deletes from the end of a line to the position
func (la *LineArray) DeleteToEnd(pos Loc) { func (la *LineArray) DeleteToEnd(pos Loc) {
la.lines[pos.Y] = la.lines[pos.Y][:pos.X] la.lines[pos.Y] = la.lines[pos.Y][:pos.X]
} }
// DeleteFromStart deletes from the start of a line to the position
func (la *LineArray) DeleteFromStart(pos Loc) { func (la *LineArray) DeleteFromStart(pos Loc) {
la.lines[pos.Y] = la.lines[pos.Y][pos.X+1:] la.lines[pos.Y] = la.lines[pos.Y][pos.X+1:]
} }
// DeleteLine deletes the line number
func (la *LineArray) DeleteLine(y int) { func (la *LineArray) DeleteLine(y int) {
la.lines = la.lines[:y+copy(la.lines[y:], la.lines[y+1:])] 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) { func (la *LineArray) DeleteByte(pos Loc) {
la.lines[pos.Y] = la.lines[pos.Y][:pos.X+copy(la.lines[pos.Y][pos.X:], la.lines[pos.Y][pos.X+1:])] la.lines[pos.Y] = la.lines[pos.Y][:pos.X+copy(la.lines[pos.Y][pos.X:], la.lines[pos.Y][pos.X+1:])]
} }
// Substr returns the string representation between two locations
func (la *LineArray) Substr(start, end Loc) string { func (la *LineArray) Substr(start, end Loc) string {
startX := runeToByteIndex(start.X, la.lines[start.Y]) startX := runeToByteIndex(start.X, la.lines[start.Y])
endX := runeToByteIndex(end.X, la.lines[end.Y]) endX := runeToByteIndex(end.X, la.lines[end.Y])

View File

@@ -83,6 +83,7 @@ func (l Loc) LessEqual(b Loc) bool {
return false return false
} }
// This moves the location one character to the right
func (l Loc) right(buf *Buffer) Loc { func (l Loc) right(buf *Buffer) Loc {
if l == buf.End() { if l == buf.End() {
return Loc{l.X + 1, l.Y} return Loc{l.X + 1, l.Y}
@@ -95,6 +96,8 @@ func (l Loc) right(buf *Buffer) Loc {
} }
return res return res
} }
// This moves the given location one character to the left
func (l Loc) left(buf *Buffer) Loc { func (l Loc) left(buf *Buffer) Loc {
if l == buf.Start() { if l == buf.Start() {
return Loc{l.X - 1, l.Y} return Loc{l.X - 1, l.Y}
@@ -108,6 +111,8 @@ func (l Loc) left(buf *Buffer) Loc {
return res 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) Move(n int, buf *Buffer) Loc { func (l Loc) Move(n int, buf *Buffer) Loc {
if n > 0 { if n > 0 {
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
@@ -120,7 +125,3 @@ func (l Loc) Move(n int, buf *Buffer) Loc {
} }
return l return l
} }
// func (l Loc) DistanceTo(b Loc, buf *Buffer) int {
//
// }

View File

@@ -17,13 +17,6 @@ import (
"github.com/zyedidia/tcell/encoding" "github.com/zyedidia/tcell/encoding"
) )
const (
synLinesUp = 75 // How many lines up to look to do syntax highlighting
synLinesDown = 75 // How many lines down to look to do syntax highlighting
doubleClickThreshold = 400 // How many milliseconds to wait before a second click is not a double click
undoThreshold = 500 // If two events are less than n milliseconds apart, undo both of them
)
var ( var (
// The main screen // The main screen
screen tcell.Screen screen tcell.Screen
@@ -41,7 +34,7 @@ var (
configDir string configDir string
// Version is the version number or commit hash // Version is the version number or commit hash
// This should be set by the linker // This should be set by the linker when compiling
Version = "Unknown" Version = "Unknown"
// L is the lua state // L is the lua state
@@ -54,15 +47,18 @@ var (
// It's just an index to the tab in the tabs array // It's just an index to the tab in the tabs array
curTab int curTab int
jobs chan JobFunction // Channel of jobs running in the background
jobs chan JobFunction
// Event channel
events chan tcell.Event events chan tcell.Event
) )
// LoadInput loads the file input for the editor // LoadInput determines which files should be loaded into buffers
// based on the input stored in os.Args
func LoadInput() []*Buffer { func LoadInput() []*Buffer {
// There are a number of ways micro should start given its input // There are a number of ways micro should start given its input
// 1. If it is given a file in os.Args, it should open that // 1. If it is given a files in os.Args, it should open those
// 2. If there is no input file and the input is not a terminal, that means // 2. If there is no input file and the input is not a terminal, that means
// something is being piped in and the stdin should be opened in an // something is being piped in and the stdin should be opened in an
@@ -71,8 +67,6 @@ func LoadInput() []*Buffer {
// 3. If there is no input file and the input is a terminal, an empty buffer // 3. If there is no input file and the input is a terminal, an empty buffer
// should be opened // should be opened
// These are empty by default so if we get to option 3, we can just returns the
// default values
var filename string var filename string
var input []byte var input []byte
var err error var err error
@@ -80,16 +74,19 @@ func LoadInput() []*Buffer {
if len(os.Args) > 1 { if len(os.Args) > 1 {
// Option 1 // Option 1
// We go through each file and load it
for i := 1; i < len(os.Args); i++ { for i := 1; i < len(os.Args); i++ {
filename = os.Args[i] filename = os.Args[i]
// Check that the file exists // Check that the file exists
if _, e := os.Stat(filename); e == nil { if _, e := os.Stat(filename); e == nil {
// If it exists we load it into a buffer
input, err = ioutil.ReadFile(filename) input, err = ioutil.ReadFile(filename)
if err != nil { if err != nil {
TermMessage(err) TermMessage(err)
continue continue
} }
} }
// If the file didn't exist, input will be empty, and we'll open an empty buffer
buffers = append(buffers, NewBuffer(input, filename)) buffers = append(buffers, NewBuffer(input, filename))
} }
} else if !isatty.IsTerminal(os.Stdin.Fd()) { } else if !isatty.IsTerminal(os.Stdin.Fd()) {
@@ -182,15 +179,18 @@ func RedrawAll() {
screen.Show() screen.Show()
} }
var flagVersion = flag.Bool("version", false, "Show version number") // Passing -version as a flag will have micro print out the version number
var flagVersion = flag.Bool("version", false, "Show the version number")
func main() { func main() {
flag.Parse() flag.Parse()
if *flagVersion { if *flagVersion {
// If -version was passed
fmt.Println("Micro version:", Version) fmt.Println("Micro version:", Version)
os.Exit(0) os.Exit(0)
} }
// Start the Lua VM for running plugins
L = lua.NewState() L = lua.NewState()
defer L.Close() defer L.Close()
@@ -200,15 +200,19 @@ func main() {
// Find the user's configuration directory (probably $XDG_CONFIG_HOME/micro) // Find the user's configuration directory (probably $XDG_CONFIG_HOME/micro)
InitConfigDir() InitConfigDir()
// Load the user's settings // Load the user's settings
InitSettings() InitSettings()
InitCommands() InitCommands()
InitBindings() InitBindings()
// Load the syntax files, including the colorscheme // Load the syntax files, including the colorscheme
LoadSyntaxFiles() LoadSyntaxFiles()
// Load the help files // Load the help files
LoadHelp() LoadHelp()
// Start the screen
InitScreen() InitScreen()
// This is just so if we have an error, we can exit cleanly and not completely // This is just so if we have an error, we can exit cleanly and not completely
@@ -224,11 +228,15 @@ func main() {
} }
}() }()
// Create a new messenger
// This is used for sending the user messages in the bottom of the editor
messenger = new(Messenger) messenger = new(Messenger)
messenger.history = make(map[string][]string) messenger.history = make(map[string][]string)
// Now we load the input
buffers := LoadInput() buffers := LoadInput()
for _, buf := range buffers { for _, buf := range buffers {
// For each buffer we create a new tab and place the view in that tab
tab := NewTabFromView(NewView(buf)) tab := NewTabFromView(NewView(buf))
tab.SetNum(len(tabs)) tab.SetNum(len(tabs))
tabs = append(tabs, tab) tabs = append(tabs, tab)
@@ -239,6 +247,8 @@ func main() {
} }
} }
// Load all the plugin stuff
// We give plugins access to a bunch of variables here which could be useful to them
L.SetGlobal("OS", luar.New(L, runtime.GOOS)) L.SetGlobal("OS", luar.New(L, runtime.GOOS))
L.SetGlobal("tabs", luar.New(L, tabs)) L.SetGlobal("tabs", luar.New(L, tabs))
L.SetGlobal("curTab", luar.New(L, curTab)) L.SetGlobal("curTab", luar.New(L, curTab))
@@ -250,6 +260,7 @@ func main() {
L.SetGlobal("CurView", luar.New(L, CurView)) L.SetGlobal("CurView", luar.New(L, CurView))
L.SetGlobal("IsWordChar", luar.New(L, IsWordChar)) L.SetGlobal("IsWordChar", luar.New(L, IsWordChar))
// Used for asynchronous jobs
L.SetGlobal("JobStart", luar.New(L, JobStart)) L.SetGlobal("JobStart", luar.New(L, JobStart))
L.SetGlobal("JobSend", luar.New(L, JobSend)) L.SetGlobal("JobSend", luar.New(L, JobSend))
L.SetGlobal("JobStop", luar.New(L, JobStop)) L.SetGlobal("JobStop", luar.New(L, JobStop))
@@ -259,6 +270,7 @@ func main() {
jobs = make(chan JobFunction, 100) jobs = make(chan JobFunction, 100)
events = make(chan tcell.Event) events = make(chan tcell.Event)
// Here is the event loop which runs in a separate thread
go func() { go func() {
for { for {
events <- screen.PollEvent() events <- screen.PollEvent()
@@ -270,8 +282,11 @@ func main() {
RedrawAll() RedrawAll()
var event tcell.Event var event tcell.Event
// Check for new events
select { select {
case f := <-jobs: case f := <-jobs:
// If a new job has finished while running in the background we should execute the callback
f.function(f.output, f.args...) f.function(f.output, f.args...)
continue continue
case event = <-events: case event = <-events:
@@ -280,13 +295,20 @@ func main() {
switch e := event.(type) { switch e := event.(type) {
case *tcell.EventMouse: case *tcell.EventMouse:
if e.Buttons() == tcell.Button1 { if e.Buttons() == tcell.Button1 {
// If the user left clicked we check a couple things
_, h := screen.Size() _, h := screen.Size()
x, y := e.Position() x, y := e.Position()
if y == h-1 && messenger.message != "" { if y == h-1 && messenger.message != "" {
// If the user clicked in the bottom bar, and there is a message down there
// we copy it to the clipboard.
// Often error messages are displayed down there so it can be useful to easily
// copy the message
clipboard.WriteAll(messenger.message) clipboard.WriteAll(messenger.message)
continue continue
} }
// We loop through each view in the current tab and make sure the current view
// it the one being clicked in
for _, v := range tabs[curTab].views { for _, v := range tabs[curTab].views {
if x >= v.x && x < v.x+v.width && y >= v.y && y < v.y+v.height { if x >= v.x && x < v.x+v.width && y >= v.y && y < v.y+v.height {
tabs[curTab].curView = v.Num tabs[curTab].curView = v.Num
@@ -295,13 +317,16 @@ func main() {
} }
} }
// This function checks the mouse event for the possibility of changing the current tab
// If the tab was changed it returns true
if TabbarHandleMouseEvent(event) { if TabbarHandleMouseEvent(event) {
continue continue
} }
if searching { if searching {
// Since searching is done in real time, we need to redraw every time // Since searching is done in real time, we need to redraw every time
// there is a new event in the search bar // there is a new event in the search bar so we need a special function
// to run instead of the standard HandleEvent.
HandleSearchEvent(event, CurView()) HandleSearchEvent(event, CurView())
} else { } else {
// Send it to the view // Send it to the view

View File

@@ -17,6 +17,7 @@ type Tab struct {
name string name string
} }
// NewTabFromView creates a new tab and puts the given view in the tab
func NewTabFromView(v *View) *Tab { func NewTabFromView(v *View) *Tab {
t := new(Tab) t := new(Tab)
t.views = append(t.views, v) t.views = append(t.views, v)
@@ -24,6 +25,7 @@ func NewTabFromView(v *View) *Tab {
return t return t
} }
// SetNum sets all this tab's views to have the correct tab number
func (t *Tab) SetNum(num int) { func (t *Tab) SetNum(num int) {
for _, v := range t.views { for _, v := range t.views {
v.TabNum = num v.TabNum = num
@@ -36,6 +38,10 @@ func CurView() *View {
return curTab.views[curTab.curView] return curTab.views[curTab.curView]
} }
// TabbarString returns the string that should be displayed in the tabbar
// It also returns a map containing which indicies correspond to which tab number
// This is useful when we know that the mouse click has occured at an x location
// but need to know which tab that corresponds to to accurately change the tab
func TabbarString() (string, map[int]int) { func TabbarString() (string, map[int]int) {
str := "" str := ""
indicies := make(map[int]int) indicies := make(map[int]int)
@@ -57,7 +63,11 @@ func TabbarString() (string, map[int]int) {
return str, indicies return str, indicies
} }
// TabbarHandleMouseEvent checks the given mouse event if it is clicking on the tabbar
// If it is it changes the current tab accordingly
// This function returns true if the tab is changed
func TabbarHandleMouseEvent(event tcell.Event) bool { func TabbarHandleMouseEvent(event tcell.Event) bool {
// There is no tabbar displayed if there are less than 2 tabs
if len(tabs) <= 1 { if len(tabs) <= 1 {
return false return false
} }
@@ -65,6 +75,7 @@ func TabbarHandleMouseEvent(event tcell.Event) bool {
switch e := event.(type) { switch e := event.(type) {
case *tcell.EventMouse: case *tcell.EventMouse:
button := e.Buttons() button := e.Buttons()
// Must be a left click
if button == tcell.Button1 { if button == tcell.Button1 {
x, y := e.Position() x, y := e.Position()
if y != 0 { if y != 0 {
@@ -94,6 +105,7 @@ func TabbarHandleMouseEvent(event tcell.Event) bool {
return false return false
} }
// DisplayTabs displays the tabbar at the top of the editor if there are multiple tabs
func DisplayTabs() { func DisplayTabs() {
if len(tabs) <= 1 { if len(tabs) <= 1 {
return return

View File

@@ -140,12 +140,15 @@ func GetModTime(path string) (time.Time, bool) {
return info.ModTime(), true return info.ModTime(), true
} }
// StringWidth returns the width of a string where tabs count as `tabsize` width
func StringWidth(str string) int { func StringWidth(str string) int {
sw := runewidth.StringWidth(str) sw := runewidth.StringWidth(str)
sw += NumOccurences(str, '\t') * (int(settings["tabsize"].(float64)) - 1) sw += NumOccurences(str, '\t') * (int(settings["tabsize"].(float64)) - 1)
return sw return sw
} }
// WidthOfLargeRunes searches all the runes in a string and counts up all the widths of runes
// that have a width larger than 1 (this also counts tabs as `tabsize` width)
func WidthOfLargeRunes(str string) int { func WidthOfLargeRunes(str string) int {
count := 0 count := 0
for _, ch := range str { for _, ch := range str {
@@ -162,6 +165,8 @@ func WidthOfLargeRunes(str string) int {
return count return count
} }
// RunePos returns the rune index of a given byte index
// This could cause problems if the byte index is between code points
func runePos(p int, str string) int { func runePos(p int, str string) int {
return utf8.RuneCountInString(str[:p]) return utf8.RuneCountInString(str[:p])
} }