Repository: gokcehan/lf
Pull request #1281 (2023-06-03 09:15:40)

Avoid refreshing previews for shell pipe commands
Follow up of #1164 

Like `shell-async` commands, `shell-pipe` commands are run in the background and can take some time to complete, so there is no point refreshing the preview immediately. When the `shell-pipe` command finishes, it internally sends a `load` command, so that should be sufficient.

For `shell` and `shell-wait` commands, the loading is handled in the `runCmdSync` function, see #1243 for details.

---

I did notice that `%` triggers the `load` command afterwards but `&` doesn't, I'm not sure if there's a reason for this though, but I think it can be addressed separately outside of this PR.

ilyagr: I think this is a good idea and it *seems* safe. In fact, I was considering doing this in #1243. I'm not quite sure if there was some weird bug those two lines fixed, though.

I was *also* considering adding some form of refresh when the command *stops* executing, in the two `go func()`-s below that line. Again, I'm not sure whether that can cause some subtle bug (e.g. an infinite loop).

I'll keep thinking about it, but it's also possible that at this point you and @gokcehan might be better experts on subtleties of changing such behavior than I am.
joelim-work: > I think this is a good idea and it _seems_ safe. In fact, I was considering doing this in #1243. I'm not quite sure if there was some weird bug those two lines fixed, though.

Looking back at the [original code](https://github.com/gokcehan/lf/blob/41cdd02572a0ef349482e630bfa71202e8065f05/app.go#L566), I'm quite sure `app.ui.loadFile` was used just for refreshing the preview after a `$` or `!` command finishes and for `&` and `%` commands it would be a no-op since they would still be running in the background. The problem is that `app.ui.loadFile` actually has the side effect of running the `cleaner` and previewer` script (noticiable for image previews).

> I was _also_ considering adding some form of refresh when the command _stops_ executing, in the two `go func()`-s below that line. Again, I'm not sure whether that can cause some subtle bug (e.g. an infinite loop).

I did notice that `%` triggers the `load` command afterwards but `&` doesn't, I'm not sure if there's a reason for this though, but I think it can be addressed separately outside of this PR.


ilyagr: I think you are likely correct. I went spelunking through the blame, and the code seems to originate in https://github.com/gokcehan/lf/commit/52fbe460b767d827e26e612d43414d356c11bf9c.

I think that, unless @gokcehan sees a reason not to, we can merge this and see if any bugs appear (which is, hopefully, unlikely).

> The problem is that app.ui.loadFile actually has the side effect of running the cleaner and previewer` script (noticiable for image previews).

I'd put a sentence about this in the PR description, perhaps with a link to https://github.com/gokcehan/lf/issues/414#issuecomment-1468557109.

> I did notice that % triggers the load command afterwards but & doesn't, I'm not sure if there's a reason for this though, but I think it can be addressed separately outside of this PR.

👍

I also realized we already talked about this in https://github.com/gokcehan/lf/pull/1164#issuecomment-1483981792
joelim-work: > I also realized we already talked about this in [#1164 (comment)](https://github.com/gokcehan/lf/pull/1164#issuecomment-1483981792)

BTW I did some quick testing, adding a `load` call at the end of a `&` command (similar to `%`) actually reintroduces the bug in https://github.com/gokcehan/lf/issues/414#issuecomment-1468557109, at least on my setup. The reason is that image previews are always volatile (they cannot be cached inside `lf` and the `cleaner`/`previewer` scripts need to run every time to display the image preview), and `load` will trigger this, causing the image to flash.

This is also why the `period` timer performs a non-volatile load, otherwise image previews would constantly be flashing, see #546: https://github.com/gokcehan/lf/blob/a9d90bc56044aa5edeb2b45c161af3c0099a3bed/app.go#L467-L470

If you can make the `load` command accept some kind of `nonvolatile` argument and pass it to `app.ui.loadFile` then that should work.

Patch:
diff --git a/app.go b/app.go
index f05a7a3..57936b4 100644
--- a/app.go
+++ b/app.go
@@ -526,7 +526,7 @@ func (app *app) runShell(s string, args []string, prefix string) {
 		return
 	}
 
-	// We are running the command asynchroniously
+	// We are running the command asynchronously
 	if prefix == "%" {
 		if app.ui.cmdPrefix == ">" {
 			return
@@ -549,12 +549,6 @@ func (app *app) runShell(s string, args []string, prefix string) {
 		app.ui.echoerrf("running shell: %s", err)
 	}
 
-	// Asynchronous shell invocations return immediately without waiting for the
-	// command to finish, so there is no point refreshing the preview if nothing
-	// has changed yet.
-	volatile := prefix != "&"
-	app.ui.loadFile(app, volatile)
-
 	switch prefix {
 	case "%":
 		normal(app)
@@ -602,26 +596,16 @@ func (app *app) runShell(s string, args []string, prefix string) {
 
 }
 
-func (app *app) runPagerOnText(text io.Reader) {
+func (app *app) runPagerOn(stdin io.Reader) {
 	app.nav.exportFiles()
 	app.ui.exportSizes()
 	exportOpts()
 
 	cmd := shellCommand(envPager, nil)
 
+	cmd.Stdin = stdin
 	cmd.Stdout = os.Stdout
 	cmd.Stderr = os.Stderr
 
-	stdin, err := cmd.StdinPipe()
-	if err != nil {
-		app.ui.echoerrf("obtaining stdin pipe: %s", err)
-		return
-	}
-
-	go func() {
-		io.Copy(stdin, text)
-		stdin.Close()
-	}()
-
 	app.runCmdSync(cmd, false)
 }
diff --git a/complete.go b/complete.go
index d2e04dc..024da3a 100644
--- a/complete.go
+++ b/complete.go
@@ -16,6 +16,7 @@ var (
 		"cmap",
 		"cmaps",
 		"cmd",
+		"cmds",
 		"quit",
 		"up",
 		"half-up",
@@ -42,6 +43,7 @@ var (
 		"glob-select",
 		"glob-unselect",
 		"calcdirsize",
+		"clearmaps",
 		"copy",
 		"cut",
 		"paste",
diff --git a/doc.go b/doc.go
index 096cf5f..bdafebe 100644
--- a/doc.go
+++ b/doc.go
@@ -41,6 +41,7 @@ The following commands are provided by lf:
 	glob-select
 	glob-unselect
 	calcdirsize
+	clearmaps
 	copy                     (default 'y')
 	cut                      (default 'd')
 	paste                    (default 'p')
@@ -81,6 +82,7 @@ The following commands are provided by lf:
 	tag-toggle               (default 't')
 	maps
 	cmaps
+	cmds
 	jumps
 
 The following command line commands are provided by lf:
@@ -208,11 +210,13 @@ The following special shell commands are used to customize the behavior of lf wh
 
 The following commands/keybindings are provided by default:
 
-	Unix                     Windows
-	cmd open &$OPENER "$f"   cmd open &%OPENER% %f%
-	map e $$EDITOR "$f"      map e $%EDITOR% %f%
-	map i $$PAGER "$f"       map i !%PAGER% %f%
-	map w $$SHELL            map w $%SHELL%
+	Unix                          Windows
+	cmd open &$OPENER "$f"        cmd open &%OPENER% %f%
+	map e $$EDITOR "$f"           map e $%EDITOR% %f%
+	map i $$PAGER "$f"            map i !%PAGER% %f%
+	map w $$SHELL                 map w $%SHELL%
+	cmd doc $$lf -doc | $PAGER    cmd doc !%lf% -doc | %PAGER%
+	map <f-1> doc                 map <f-1> doc
 
 The following additional keybindings are provided by default:
 
@@ -379,6 +383,12 @@ Calculate the total size for each of the selected directories.
 Option 'info' should include 'size' and option 'dircounts' should be disabled to show this size.
 If the total size of a directory is not calculated, it will be shown as '-'.
 
+	clearmaps
+
+Remove all keybindings associated with the `map` command.
+This command can be used in the config file to remove the default keybindings.
+For safety purposes, `:` is left mapped to the `read` command, and `cmap` keybindings are retained so that it is still possible to exit `lf` using `:quit`.
+
 	copy                     (default 'y')
 
 If there are no selections, save the path of the current file to the copy buffer, otherwise, copy the paths of selected files.
@@ -620,6 +630,10 @@ Capitalize/uppercase/lowercase the current word and jump to the next word.
 
 List all key mappings in normal mode or command-line editing mode.
 
+	cmds
+
+List all custom commands defined using the `cmd` command
+
 	jumps
 
 List the contents of the jump list, in order of the most recently visited locations.
diff --git a/docstring.go b/docstring.go
index f716670..8947671 100644
--- a/docstring.go
+++ b/docstring.go
@@ -44,6 +44,7 @@ The following commands are provided by lf:
     glob-select
     glob-unselect
     calcdirsize
+    clearmaps
     copy                     (default 'y')
     cut                      (default 'd')
     paste                    (default 'p')
@@ -84,6 +85,7 @@ The following commands are provided by lf:
     tag-toggle               (default 't')
     maps
     cmaps
+    cmds
     jumps
 
 The following command line commands are provided by lf:
@@ -212,11 +214,13 @@ when defined:
 
 The following commands/keybindings are provided by default:
 
-    Unix                     Windows
-    cmd open &$OPENER "$f"   cmd open &%OPENER% %f%
-    map e $$EDITOR "$f"      map e $%EDITOR% %f%
-    map i $$PAGER "$f"       map i !%PAGER% %f%
-    map w $$SHELL            map w $%SHELL%
+    Unix                          Windows
+    cmd open &$OPENER "$f"        cmd open &%OPENER% %f%
+    map e $$EDITOR "$f"           map e $%EDITOR% %f%
+    map i $$PAGER "$f"            map i !%PAGER% %f%
+    map w $$SHELL                 map w $%SHELL%
+    cmd doc $$lf -doc | $PAGER    cmd doc !%lf% -doc | %PAGER%
+    map <f-1> doc                 map <f-1> doc
 
 The following additional keybindings are provided by default:
 
@@ -395,6 +399,13 @@ should include 'size' and option 'dircounts' should be disabled to show this
 size. If the total size of a directory is not calculated, it will be shown as
 '-'.
 
+    clearmaps
+
+Remove all keybindings associated with the 'map' command. This command can be
+used in the config file to remove the default keybindings. For safety purposes,
+':' is left mapped to the 'read' command, and 'cmap' keybindings are retained so
+that it is still possible to exit 'lf' using ':quit'.
+
     copy                     (default 'y')
 
 If there are no selections, save the path of the current file to the copy
@@ -648,6 +659,10 @@ Capitalize/uppercase/lowercase the current word and jump to the next word.
 
 List all key mappings in normal mode or command-line editing mode.
 
+    cmds
+
+List all custom commands defined using the 'cmd' command
+
     jumps
 
 List the contents of the jump list, in order of the most recently visited
diff --git a/etc/icons.example b/etc/icons.example
index 8039c93..404f9e5 100644
--- a/etc/icons.example
+++ b/etc/icons.example
@@ -87,7 +87,7 @@ fi             # FILE
 *.cc            
 *.cp            
 *.c             
-*.cs            
+*.cs            󰌛
 *.h             
 *.hh            
 *.hpp           
@@ -152,10 +152,10 @@ fi             # FILE
 *.elm           
 *.swift         
 *.xcplayground  
-*.tex           ﭨ
-*.r             ﳒ
-*.rproj         鉶
-*.sol           ﲹ
+*.tex           󰙩
+*.r             󰟔
+*.rproj         󰗆
+*.sol           󰡪
 *.pem           
 
 # file names (vim-devicons) (case-insensitive not supported in lf)
@@ -193,7 +193,7 @@ fi             # FILE
 *gemfile                
 *makefile               
 *cmakelists.txt         
-*robots.txt             ﮧ
+*robots.txt             󰚩
 
 # file names (case-sensitive adaptations)
 *Gruntfile.coffee       
diff --git a/eval.go b/eval.go
index 0ddf107..048ee92 100644
--- a/eval.go
+++ b/eval.go
@@ -1614,6 +1614,10 @@ func (e *callExpr) eval(app *app, args []string) {
 		app.ui.loadFileInfo(app.nav)
 		app.nav.sort()
 		app.ui.sort()
+	case "clearmaps":
+		// leave `:` and cmaps bound so the user can still exit using `:quit`
+		gOpts.keys = make(map[string]expr)
+		gOpts.keys[":"] = &callExpr{"read", nil, 1}
 	case "copy":
 		if !app.nav.init {
 			return
@@ -2244,64 +2248,60 @@ func (e *callExpr) eval(app *app, args []string) {
 			}
 		case "rename: ":
 			app.ui.cmdPrefix = ""
-			if curr, err := app.nav.currFile(); err != nil {
-				app.ui.echoerrf("rename: %s", err)
-			} else {
-				wd, err := os.Getwd()
-				if err != nil {
-					log.Printf("getting current directory: %s", err)
-					return
-				}
-
-				oldPath := filepath.Join(wd, curr.Name())
-
-				newPath := filepath.Clean(replaceTilde(s))
-				if !filepath.IsAbs(newPath) {
-					newPath = filepath.Join(wd, newPath)
-				}
 
-				if oldPath == newPath {
-					return
-				}
+			curr, err := app.nav.currFile()
+			if err != nil {
+				app.ui.echoerrf("rename: %s", err)
+				return
+			}
+			wd, err := os.Getwd()
+			if err != nil {
+				log.Printf("getting current directory: %s", err)
+				return
+			}
 
-				app.nav.renameOldPath = oldPath
-				app.nav.renameNewPath = newPath
+			oldPath := filepath.Join(wd, curr.Name())
+			newPath := filepath.Clean(replaceTilde(s))
+			if !filepath.IsAbs(newPath) {
+				newPath = filepath.Join(wd, newPath)
+			}
+			if oldPath == newPath {
+				return
+			}
+			app.nav.renameOldPath = oldPath
+			app.nav.renameNewPath = newPath
 
-				newDir := filepath.Dir(newPath)
-				if _, err := os.Stat(newDir); os.IsNotExist(err) {
-					app.ui.cmdPrefix = "create '" + newDir + "' ? [y/N] "
-					return
-				}
+			newDir := filepath.Dir(newPath)
+			if _, err := os.Stat(newDir); os.IsNotExist(err) {
+				app.ui.cmdPrefix = "create '" + newDir + "' ? [y/N] "
+				return
+			}
 
-				oldStat, err := os.Lstat(oldPath)
-				if err != nil {
-					app.ui.echoerrf("rename: %s", err)
-					return
-				}
+			oldStat, err := os.Lstat(oldPath)
+			if err != nil {
+				app.ui.echoerrf("rename: %s", err)
+				return
+			}
+			if newStat, err := os.Lstat(newPath); !os.IsNotExist(err) && !os.SameFile(oldStat, newStat) {
+				app.ui.cmdPrefix = "replace '" + newPath + "' ? [y/N] "
+				return
+			}
 
-				if newStat, err := os.Lstat(newPath); !os.IsNotExist(err) && !os.SameFile(oldStat, newStat) {
-					app.ui.cmdPrefix = "replace '" + newPath + "' ? [y/N] "
-					return
-				}
+			if err := app.nav.rename(); err != nil {
+				app.ui.echoerrf("rename: %s", err)
+				return
+			}
 
-				if err := app.nav.rename(); err != nil {
+			if gSingleMode {
+				app.nav.renew()
+			} else {
+				if err := remote("send load"); err != nil {
 					app.ui.echoerrf("rename: %s", err)
 					return
 				}
-
-				if gSingleMode {
-					app.nav.renew()
-					app.ui.loadFile(app, true)
-				} else {
-					if err := remote("send load"); err != nil {
-						app.ui.echoerrf("rename: %s", err)
-						return
-					}
-				}
-
-				app.ui.loadFile(app, true)
-				app.ui.loadFileInfo(app.nav)
 			}
+			app.ui.loadFile(app, true)
+			app.ui.loadFileInfo(app.nav)
 		default:
 			log.Printf("entering unknown execution prefix: %q", app.ui.cmdPrefix)
 		}
@@ -2347,9 +2347,10 @@ func (e *callExpr) eval(app *app, args []string) {
 			switch app.ui.cmdPrefix {
 			case "!", "$", "%", "&":
 				app.ui.cmdPrefix = ":"
-			case ">", "rename: ":
+			case ">", "rename: ", "filter: ":
 				// Don't mess with programs waiting for input.
-				// Exiting on backspace is also inconvenient for renames since the text field starts out nonempty.
+				// Exiting on backspace is also inconvenient for 'rename' and 'filter',
+				// since the text field can start out nonempty.
 			default:
 				normal(app)
 			}
@@ -2525,12 +2526,14 @@ func (e *callExpr) eval(app *app, args []string) {
 
 		app.ui.cmdAccLeft = acc
 		update(app)
+	case "cmds":
+		app.runPagerOn(listCmds())
 	case "maps":
-		app.runPagerOnText(listBinds(gOpts.keys))
+		app.runPagerOn(listBinds(gOpts.keys))
 	case "cmaps":
-		app.runPagerOnText(listBinds(gOpts.cmdkeys))
+		app.runPagerOn(listBinds(gOpts.cmdkeys))
 	case "jumps":
-		app.runPagerOnText(listJumps(app.nav.jumpList, app.nav.jumpListInd))
+		app.runPagerOn(listJumps(app.nav.jumpList, app.nav.jumpListInd))
 	default:
 		cmd, ok := gOpts.cmds[e.name]
 		if !ok {
diff --git a/lf.1 b/lf.1
index 6de2b89..7b59b1c 100644
--- a/lf.1
+++ b/lf.1
@@ -56,6 +56,7 @@ The following commands are provided by lf:
     glob-select
     glob-unselect
     calcdirsize
+    clearmaps
     copy                     (default 'y')
     cut                      (default 'd')
     paste                    (default 'p')
@@ -96,6 +97,7 @@ The following commands are provided by lf:
     tag-toggle               (default 't')
     maps
     cmaps
+    cmds
     jumps
 .EE
 .PP
@@ -233,11 +235,13 @@ The following special shell commands are used to customize the behavior of lf wh
 The following commands/keybindings are provided by default:
 .PP
 .EX
-    Unix                     Windows
-    cmd open &$OPENER "$f"   cmd open &%OPENER% %f%
-    map e $$EDITOR "$f"      map e $%EDITOR% %f%
-    map i $$PAGER "$f"       map i !%PAGER% %f%
-    map w $$SHELL            map w $%SHELL%
+    Unix                          Windows
+    cmd open &$OPENER "$f"        cmd open &%OPENER% %f%
+    map e $$EDITOR "$f"           map e $%EDITOR% %f%
+    map i $$PAGER "$f"            map i !%PAGER% %f%
+    map w $$SHELL                 map w $%SHELL%
+    cmd doc $$lf -doc | $PAGER    cmd doc !%lf% -doc | %PAGER%
+    map <f-1> doc                 map <f-1> doc
 .EE
 .PP
 The following additional keybindings are provided by default:
@@ -441,6 +445,12 @@ Select/unselect files that match the given glob.
 .PP
 Calculate the total size for each of the selected directories. Option 'info' should include 'size' and option 'dircounts' should be disabled to show this size. If the total size of a directory is not calculated, it will be shown as '-'.
 .PP
+.EX
+    clearmaps
+.EE
+.PP
+Remove all keybindings associated with the `map` command. This command can be used in the config file to remove the default keybindings. For safety purposes, `:` is left mapped to the `read` command, and `cmap` keybindings are retained so that it is still possible to exit `lf` using `:quit`.
+.PP
 .EX
     copy                     (default 'y')
 .EE
@@ -764,6 +774,12 @@ Capitalize/uppercase/lowercase the current word and jump to the next word.
 .PP
 List all key mappings in normal mode or command-line editing mode.
 .PP
+.EX
+    cmds
+.EE
+.PP
+List all custom commands defined using the `cmd` command
+.PP
 .EX
     jumps
 .EE
diff --git a/misc.go b/misc.go
index ea33664..b0303ea 100644
--- a/misc.go
+++ b/misc.go
@@ -31,23 +31,26 @@ func runeSliceWidth(rs []rune) int {
 }
 
 func runeSliceWidthRange(rs []rune, beg, end int) []rune {
+	if beg == end {
+		return []rune{}
+	}
+
 	curr := 0
 	b := 0
+	foundb := false
 	for i, r := range rs {
 		w := runewidth.RuneWidth(r)
-		switch {
-		case curr == beg:
+		if curr >= beg && !foundb {
 			b = i
-		case curr < beg && curr+w > beg:
-			b = i + 1
-		case curr == end:
+			foundb = true
+		}
+		if curr == end || curr+w > end {
 			return rs[b:i]
-		case curr > end:
-			return rs[b : i-1]
 		}
 		curr += w
 	}
-	return nil
+
+	return rs[b:]
 }
 
 // This function is used to escape whitespaces and special characters with
diff --git a/ui.go b/ui.go
index 4c01391..1f8af2c 100644
--- a/ui.go
+++ b/ui.go
@@ -441,8 +441,16 @@ func (win *win) printDir(screen tcell.Screen, dir *dir, context *dirContext, dir
 			}
 		}
 
+		// make space for select marker, and leave another space at the end
+		maxWidth := win.w - lnwidth - 2
+		// make extra space to separate windows if drawbox is not enabled
+		if !gOpts.drawbox {
+			maxWidth -= 1
+		}
+
 		var s []rune
 
+		// leave space for displaying the tag
 		s = append(s, ' ')
 
 		if gOpts.icons {
@@ -450,40 +458,26 @@ func (win *win) printDir(screen tcell.Screen, dir *dir, context *dirContext, dir
 			s = append(s, ' ')
 		}
 
-		for _, r := range f.Name() {
-			s = append(s, r)
-		}
-
-		w := runeSliceWidth(s)
+		maxFilenameWidth := maxWidth - runeSliceWidth(s)
 
-		// make space for select marker, and leave another space at the end
-		maxlength := win.w - lnwidth - 2
-		// make extra space to separate windows if drawbox is not enabled
-		if !gOpts.drawbox {
-			maxlength -= 1
+		info := fileInfo(f, dir)
+		showInfo := len(info) > 0 && 2*len(info) < maxWidth
+		if showInfo {
+			maxFilenameWidth -= len(info)
 		}
 
-		if w > maxlength {
-			s = runeSliceWidthRange(s, 0, maxlength-1)
-			s = append(s, []rune(gOpts.truncatechar)...)
-		} else {
-			for i := 0; i < maxlength-w; i++ {
-				s = append(s, ' ')
-			}
+		filename := []rune(f.Name())
+		if runeSliceWidth(filename) > maxFilenameWidth {
+			filename = runeSliceWidthRange(filename, 0, maxFilenameWidth-1)
+			filename = append(filename, []rune(gOpts.truncatechar)...)
 		}
+		for i := runeSliceWidth(filename); i < maxFilenameWidth; i++ {
+			filename = append(filename, ' ')
+		}
+		s = append(s, filename...)
 
-		info := fileInfo(f, dir)
-
-		if len(info) > 0 && 2*len(info) < maxlength {
-			if w+len(info) > maxlength {
-				s = runeSliceWidthRange(s, 0, maxlength-len(info)-1)
-				s = append(s, []rune(gOpts.truncatechar)...)
-			} else {
-				s = runeSliceWidthRange(s, 0, maxlength-len(info))
-			}
-			for _, r := range info {
-				s = append(s, r)
-			}
+		if showInfo {
+			s = append(s, []rune(info)...)
 		}
 
 		ce := ""
@@ -1052,7 +1046,7 @@ func findBinds(keys map[string]expr, prefix string) (binds map[string]expr, ok b
 	return
 }
 
-func listBinds(binds map[string]expr) *bytes.Buffer {
+func listExprMap(binds map[string]expr, title string) *bytes.Buffer {
 	t := new(tabwriter.Writer)
 	b := new(bytes.Buffer)
 
@@ -1063,7 +1057,7 @@ func listBinds(binds map[string]expr) *bytes.Buffer {
 	sort.Strings(keys)
 
 	t.Init(b, 0, gOpts.tabstop, 2, '\t', 0)
-	fmt.Fprintln(t, "keys\tcommand")
+	fmt.Fprintf(t, "%s\tcommand\n", title)
 	for _, k := range keys {
 		fmt.Fprintf(t, "%s\t%v\n", k, binds[k])
 	}
@@ -1072,6 +1066,14 @@ func listBinds(binds map[string]expr) *bytes.Buffer {
 	return b
 }
 
+func listBinds(binds map[string]expr) *bytes.Buffer {
+	return listExprMap(binds, "keys")
+}
+
+func listCmds() *bytes.Buffer {
+	return listExprMap(gOpts.cmds, "name")
+}
+
 func listJumps(jumps []string, ind int) *bytes.Buffer {
 	t := new(tabwriter.Writer)
 	b := new(bytes.Buffer)


Test patch:
diff --git a/misc_test.go b/misc_test.go
index 3568fa2..aa6f6df 100644
--- a/misc_test.go
+++ b/misc_test.go
@@ -55,21 +55,30 @@ func TestRuneSliceWidthRange(t *testing.T) {
 		end int
 		exp []rune
 	}{
+		{[]rune{}, 0, 0, []rune{}},
 		{[]rune{'a', 'b', 'c', 'd'}, 1, 3, []rune{'b', 'c'}},
 		{[]rune{'a', 'ı', 'b', 'ş'}, 1, 3, []rune{'ı', 'b'}},
 		{[]rune{'世', '界', '世', '界'}, 2, 6, []rune{'界', '世'}},
 		{[]rune{'世', '界', '世', '界'}, 3, 6, []rune{'世'}},
 		{[]rune{'世', '界', '世', '界'}, 2, 5, []rune{'界'}},
 		{[]rune{'世', '界', '世', '界'}, 3, 5, []rune{}},
+		{[]rune{'世', '界', '世', '界'}, 4, 4, []rune{}},
+		{[]rune{'世', '界', '世', '界'}, 5, 5, []rune{}},
+		{[]rune{'世', '界', '世', '界'}, 4, 7, []rune{'世'}},
+		{[]rune{'世', '界', '世', '界'}, 4, 8, []rune{'世', '界'}},
 		{[]rune{'世', 'a', '界', 'ı'}, 2, 5, []rune{'a', '界'}},
 		{[]rune{'世', 'a', '界', 'ı'}, 2, 4, []rune{'a'}},
 		{[]rune{'世', 'a', '界', 'ı'}, 3, 5, []rune{'界'}},
 		{[]rune{'世', 'a', '界', 'ı'}, 3, 4, []rune{}},
+		{[]rune{'世', 'a', '界', 'ı'}, 3, 3, []rune{}},
+		{[]rune{'世', 'a', '界', 'ı'}, 4, 4, []rune{}},
+		{[]rune{'世', 'a', '界', 'ı'}, 4, 6, []rune{'ı'}},
+		{[]rune{'世', 'a', '界', 'ı'}, 5, 6, []rune{'ı'}},
 	}
 
 	for _, test := range tests {
 		if got := runeSliceWidthRange(test.rs, test.beg, test.end); !reflect.DeepEqual(got, test.exp) {
-			t.Errorf("at input '%v' expected '%v' but got '%v'", test.rs, test.rs, got)
+			t.Errorf("at input '%v' expected '%v' but got '%v'", test.rs, test.exp, got)
 		}
 	}
 }