about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--src/buffer/container.nim67
-rw-r--r--src/display/pager.nim8
-rw-r--r--src/display/term.nim18
-rw-r--r--src/layout/box.nim1
-rw-r--r--src/layout/engine.nim13
-rw-r--r--src/render/renderdocument.nim8
-rw-r--r--src/render/rendertext.nim16
-rw-r--r--src/utils/twtstr.nim20
8 files changed, 78 insertions, 73 deletions
diff --git a/src/buffer/container.nim b/src/buffer/container.nim
index 065177a7..8383485b 100644
--- a/src/buffer/container.nim
+++ b/src/buffer/container.nim
@@ -138,12 +138,6 @@ func fromy*(container: Container): int {.inline.} = container.pos.fromy
 func xend(container: Container): int {.inline.} = container.pos.xend
 func lastVisibleLine(container: Container): int = min(container.fromy + container.height, container.numLines) - 1
 
-func acursorx*(container: Container): int =
-  max(0, container.cursorx - container.fromx)
-
-func acursory*(container: Container): int =
-  container.cursory - container.fromy
-
 func currentLine(container: Container): string =
   return container.getLine(container.cursory).str
 
@@ -154,41 +148,47 @@ func cursorBytes(container: Container, y: int, cc = container.cursorx): int =
   while i < line.len and w < cc:
     var r: Rune
     fastRuneAt(line, i, r)
-    w += r.width()
+    w += r.twidth(w)
   return i
 
 func currentCursorBytes(container: Container, cc = container.cursorx): int =
   return container.cursorBytes(container.cursory, cc)
 
-func prevWidth(container: Container): int =
+# Returns the X position of the first cell occupied by the character the cursor
+# currently points to.
+func cursorFirstX(container: Container): int =
   if container.numLines == 0: return 0
   let line = container.currentLine
-  if line.len == 0: return 0
   var w = 0
   var i = 0
-  let cc = container.pos.fromx + container.pos.cursorx
-  var pr: Rune
   var r: Rune
-  fastRuneAt(line, i, r)
-  while i < line.len and w < cc:
-    pr = r
+  let cc = container.cursorx
+  while i < line.len:
     fastRuneAt(line, i, r)
-    w += r.width()
-  return pr.width()
-
-func currentWidth(container: Container): int =
+    let tw = r.twidth(w)
+    if w + tw > cc:
+      return w
+    w += tw
+
+# Returns the X position of the last cell occupied by the character the cursor
+# currently points to.
+func cursorLastX(container: Container): int =
   if container.numLines == 0: return 0
   let line = container.currentLine
-  if line.len == 0: return 0
   var w = 0
   var i = 0
-  let cc = container.cursorx
   var r: Rune
-  fastRuneAt(line, i, r)
-  while i < line.len and w < cc:
+  let cc = container.cursorx
+  while i < line.len and w <= cc:
     fastRuneAt(line, i, r)
-    w += r.width()
-  return r.width()
+    w += r.twidth(w)
+  return max(w - 1, 0)
+
+func acursorx*(container: Container): int =
+  max(0, container.cursorLastX() - container.fromx)
+
+func acursory*(container: Container): int =
+  container.cursory - container.fromy
 
 func maxScreenWidth(container: Container): int =
   for line in container.ilines(container.fromy..container.lastVisibleLine):
@@ -387,13 +387,10 @@ proc cursorUp(container: Container) {.jsfunc.} =
   container.setCursorY(container.cursory - 1)
 
 proc cursorLeft(container: Container) {.jsfunc.} =
-  var w = container.prevWidth()
-  if w == 0:
-    w = 1
-  container.setCursorX(container.cursorx - w)
+  container.setCursorX(container.cursorFirstX() - 1)
 
 proc cursorRight(container: Container) {.jsfunc.} =
-  container.setCursorX(container.cursorx + container.currentWidth())
+  container.setCursorX(container.cursorLastX() + 1)
 
 proc cursorLineBegin(container: Container) {.jsfunc.} =
   container.setCursorX(0)
@@ -412,7 +409,7 @@ proc cursorNextWord(container: Container) {.jsfunc.} =
     if r.breaksWord():
       b = pb
       break
-    x += r.width()
+    x += r.twidth(x)
 
   while b < container.currentLine.len:
     let pb = b
@@ -420,7 +417,7 @@ proc cursorNextWord(container: Container) {.jsfunc.} =
     if not r.breaksWord():
       b = pb
       break
-    x += r.width()
+    x += r.twidth(x)
 
   if b < container.currentLine.len:
     container.setCursorX(x)
@@ -442,14 +439,14 @@ proc cursorPrevWord(container: Container) {.jsfunc.} =
       if r.breaksWord():
         break
       b -= o
-      x -= r.width()
+      x -= r.twidth(x)
 
     while b >= 0:
       let (r, o) = lastRune(container.currentLine, b)
       if not r.breaksWord():
         break
       b -= o
-      x -= r.width()
+      x -= r.twidth(x)
   else:
     b = -1
 
@@ -611,7 +608,7 @@ proc cursorNextMatch*(container: Container, regex: Regex, wrap: bool) {.jsfunc.}
         container.setCursorXY(res.x, res.y)
         if container.hlon:
           container.clearSearchHighlights()
-          let ex = res.x + res.str.width() - 1
+          let ex = res.x + res.str.twidth(res.x) - 1
           let hl = Highlight(x: res.x, y: res.y, endx: ex, endy: res.y, clear: true)
           container.highlights.add(hl)
           container.triggerEvent(UPDATE)
@@ -630,7 +627,7 @@ proc cursorPrevMatch*(container: Container, regex: Regex, wrap: bool) {.jsfunc.}
         container.setCursorXY(res.x, res.y)
         if container.hlon:
           container.clearSearchHighlights()
-          let ex = res.x + res.str.width() - 1
+          let ex = res.x + res.str.twidth(res.x) - 1
           let hl = Highlight(x: res.x, y: res.y, endx: ex, endy: res.y, clear: true)
           container.highlights.add(hl)
           container.hlon = false)
diff --git a/src/display/pager.nim b/src/display/pager.nim
index 49ff2227..057f9238 100644
--- a/src/display/pager.nim
+++ b/src/display/pager.nim
@@ -201,7 +201,7 @@ proc refreshDisplay(pager: Pager, container = pager.container) =
     # Skip cells till fromx.
     while w < container.fromx and i < line.str.len:
       fastRuneAt(line.str, i, r)
-      w += r.width()
+      w += r.twidth(w)
     let dls = by * pager.display.width # starting position of row in display
     # Fill in the gap in case we skipped more cells than fromx mandates (i.e.
     # we encountered a double-width character.)
@@ -218,7 +218,7 @@ proc refreshDisplay(pager: Pager, container = pager.container) =
     while i < line.str.len:
       let pw = w
       fastRuneAt(line.str, i, r)
-      w += r.width()
+      w += r.twidth(w)
       if w > container.fromx + pager.display.width:
         break # die on exceeding the width limit
       if nf.pos != -1 and nf.pos <= pw:
@@ -228,7 +228,7 @@ proc refreshDisplay(pager: Pager, container = pager.container) =
       lan &= r
       if cf.pos != -1:
         pager.display[dls + k].format = cf.format
-      let tk = k + r.width()
+      let tk = k + r.twidth(k)
       while k < tk and k < pager.display.width - 1:
         inc k
     # Finally, override cell formatting for highlighted cells.
@@ -258,7 +258,7 @@ proc writeStatusMessage(pager: Pager, str: string, format: Format = newFormat())
     else:
       pager.statusgrid[i].str &= r
     pager.statusgrid[i].format = format
-    i += r.width()
+    i += r.twidth(i)
 
 proc refreshStatusMsg*(pager: Pager) =
   let container = pager.container
diff --git a/src/display/term.nim b/src/display/term.nim
index aca20637..1a3b8b9e 100644
--- a/src/display/term.nim
+++ b/src/display/term.nim
@@ -320,14 +320,20 @@ proc windowChange*(term: Terminal, attrs: WindowAttributes) =
   term.canvas = newFixedGrid(attrs.width, attrs.height)
   term.cleared = false
 
-proc processOutputString(term: Terminal, str: string): string =
+proc processOutputString(term: Terminal, str: string, w: var int): string =
   if str.validateUtf8() != -1:
     return "?"
   for r in str.runes():
-    if r.isControlChar():
+    let tw = r.twidth(w)
+    if r == Rune('\t'):
+      # Needs to be replaced with spaces, otherwise bgcolor isn't displayed.
+      for i in 0 ..< tw:
+        result &= ' '
+    elif r.isControlChar():
       result &= "^" & getControlLetter(char(r))
-    elif r.width() != 0:
+    elif tw != 0:
       result &= r
+    w += tw
 
 proc generateFullOutput(term: Terminal, grid: FixedGrid): string =
   var format = newFormat()
@@ -342,8 +348,7 @@ proc generateFullOutput(term: Terminal, grid: FixedGrid): string =
         inc w
       let cell = grid[y * grid.width + x]
       result &= term.processFormat(format, cell.format)
-      result &= term.processOutputString(cell.str)
-      w += cell.width()
+      result &= term.processOutputString(cell.str, w)
     if y != grid.height - 1:
       result &= "\r\n"
 
@@ -371,8 +376,7 @@ proc generateSwapOutput(term: Terminal, grid: FixedGrid, prev: FixedGrid): strin
       line = ""
     lr = lr or (grid[i] != prev[i])
     line &= term.processFormat(format, cell.format)
-    line &= term.processOutputString(cell.str)
-    w += cell.width()
+    line &= term.processOutputString(cell.str, w)
     inc x
   if lr:
     result &= term.cursorGoto(0, grid.height - 1)
diff --git a/src/layout/box.nim b/src/layout/box.nim
index 9ce95fb8..a436cfae 100644
--- a/src/layout/box.nim
+++ b/src/layout/box.nim
@@ -93,6 +93,7 @@ type
     height*: int
     lines*: seq[LineBox]
     currentLine*: LineBox
+    charwidth*: int
 
     whitespacenum*: int
     minwidth*: int
diff --git a/src/layout/engine.nim b/src/layout/engine.nim
index c5536517..b26e0ebe 100644
--- a/src/layout/engine.nim
+++ b/src/layout/engine.nim
@@ -212,6 +212,7 @@ proc flushWhitespace(ictx: InlineContext, computed: CSSComputedValues) =
 proc finishLine(ictx: InlineContext, computed: CSSComputedValues, maxwidth: int, force = false) =
   if ictx.currentLine.atoms.len != 0 or force:
     ictx.whitespacenum = 0
+    ictx.charwidth = 0
     ictx.flushWhitespace(computed)
     ictx.verticalAlignLine()
 
@@ -255,6 +256,7 @@ proc addAtom(ictx: InlineContext, atom: InlineAtom, maxwidth: int, pcomputed: CS
     if atom of InlineWord:
       ictx.format = InlineWord(atom).format
     else:
+      ictx.charwidth = 0
       ictx.format = nil
     ictx.currentLine.atoms.add(atom)
 
@@ -315,13 +317,14 @@ proc checkWrap(state: var InlineState, r: Rune) =
 proc processWhitespace(state: var InlineState, c: char) =
   state.addWord()
   case state.computed{"white-space"}
-  of WHITESPACE_NORMAL, WHITESPACE_NOWRAP:
+  of WHITESPACE_NORMAL, WHITESPACE_NOWRAP, WHITESPACE_PRE_LINE:
     state.ictx.whitespacenum = max(state.ictx.whitespacenum, 1)
-  of WHITESPACE_PRE_LINE, WHITESPACE_PRE, WHITESPACE_PRE_WRAP:
+  of WHITESPACE_PRE, WHITESPACE_PRE_WRAP:
     if c == '\n':
       state.ictx.flushLine(state.computed, state.maxwidth)
     elif c == '\t':
-      state.ictx.whitespacenum = (state.ictx.whitespacenum div 8 + 1) * 8
+      state.ictx.charwidth = ((state.ictx.charwidth div 8) + 1) * 8
+      state.word.str &= c
     else:
       inc state.ictx.whitespacenum
 
@@ -348,7 +351,9 @@ proc renderText*(ictx: InlineContext, str: string, maxwidth: int, computed: CSSC
         state.hasshy = true
       else:
         state.word.str &= r
-        state.word.width += r.width() * state.ictx.cellwidth
+        let w = r.width()
+        state.word.width += w * state.ictx.cellwidth
+        state.ictx.charwidth += w
         if r == Rune('-'): # ascii dash
           state.wrappos = state.word.str.len
           state.hasshy = false
diff --git a/src/render/renderdocument.nim b/src/render/renderdocument.nim
index bdcf4df8..a988cca7 100644
--- a/src/render/renderdocument.nim
+++ b/src/render/renderdocument.nim
@@ -40,7 +40,7 @@ proc setText(lines: var FlexibleGrid, linestr: string, cformat: ComputedFormat,
   while cx < x and i < lines[y].str.len:
     let pi = i
     fastRuneAt(lines[y].str, i, r)
-    let w = r.width()
+    let w = r.twidth(cx)
     # we must ensure x is max(cx, x), otherwise our assumption of cx <= x
     # breaks down
     if cx + w > x:
@@ -55,13 +55,13 @@ proc setText(lines: var FlexibleGrid, linestr: string, cformat: ComputedFormat,
     lines[y].str &= ' '.repeat(padwidth)
 
   lines[y].str &= linestr
-  let linestrwidth = linestr.width()
+  let linestrwidth = linestr.twidth(x) - x
 
   i = 0
   var nx = x # last x of new string
   while nx < x + linestrwidth and i < ostr.len:
     fastRuneAt(ostr, i, r)
-    nx += r.width()
+    nx += r.twidth(nx)
 
   if i < ostr.len:
     lines[y].str &= ostr.substr(i)
@@ -153,7 +153,7 @@ proc setRowWord(lines: var FlexibleGrid, word: InlineWord, x, y: int, window: Wi
   var i = 0
   while x < 0 and i < word.str.len:
     fastRuneAt(word.str, i, r)
-    x += r.width()
+    x += r.twidth(x)
   if x < 0: return # highest x is outside the canvas, no need to draw
   let linestr = word.str.substr(i)
 
diff --git a/src/render/rendertext.nim b/src/render/rendertext.nim
index e3d01638..24d7039e 100644
--- a/src/render/rendertext.nim
+++ b/src/render/rendertext.nim
@@ -6,9 +6,7 @@ import data/charset
 import encoding/decoderstream
 import utils/twtstr
 
-const tabwidth = 8
 type StreamRenderer* = object
-  w: int
   ansiparser: AnsiCodeParser
   format: Format
   af: bool
@@ -35,7 +33,6 @@ proc renderStream*(grid: var FlexibleGrid, renderer: var StreamRenderer, len: in
       # avoid newline at end of stream
       grid.addLine()
       renderer.newline = false
-      renderer.w = 0
     let r = buf[i]
     if r.isAscii():
       let c = cast[char](r)
@@ -44,27 +41,16 @@ proc renderStream*(grid: var FlexibleGrid, renderer: var StreamRenderer, len: in
         if not cancel:
           if renderer.ansiparser.state == PARSE_DONE:
             renderer.af = true
-          continue
       case c
       of '\n':
         add_format
         renderer.newline = true
-        continue
-      of '\r': continue
-      of '\t':
-        add_format
-        let w = ((renderer.w div tabwidth) + 1) * tabwidth
-        while renderer.w < w:
-          grid[^1].str &= ' '
-          inc renderer.w
-        continue
+      of '\r': discard
       of '\e':
         renderer.ansiparser.reset()
-        continue
       else:
         add_format
         grid[^1].str &= c
     else:
       add_format
       grid[^1].str &= r
-    renderer.w += r.width()
diff --git a/src/utils/twtstr.nim b/src/utils/twtstr.nim
index 91633734..96d4bf2f 100644
--- a/src/utils/twtstr.nim
+++ b/src/utils/twtstr.nim
@@ -993,10 +993,6 @@ func width*(r: Rune): int =
     return int(width_table[int(r)])
 {.pop.}
 
-func width*(s: string): int =
-  for r in s.runes():
-    result += width(r)
-
 func width*(s: string, len: int): int =
   var i = 0
   var m = len
@@ -1023,6 +1019,22 @@ func width*(s: seq[Rune], min: int): int =
     result += width(s[i])
     inc i
 
+# Width, but also works with tabs.
+# Needs the column width of the text so far.
+func twidth*(r: Rune, w: int): int =
+  if r != Rune('\t'):
+    return r.width()
+  return ((w div 8) + 1) * 8 - w
+
+func width*(s: string): int =
+  for r in s.runes():
+    result += twidth(r, result)
+
+func twidth*(s: string, w: int): int =
+  result = w
+  for r in s.runes():
+    result += twidth(r, result)
+
 func breaksWord*(r: Rune): bool =
   return not (r.isDigitAscii() or r.width() == 0 or r.isAlpha())