patch 9.1.0817: termdebug: cannot evaluate expr in a popup

Problem:  termdebug: cannot evaluate expr in a popup
Solution: enhance termdebug plugin and allow to evaluate expressions in
          a popup window, add a unit test (Peter Wolf).

fixes: #15877
closes: #15933

Signed-off-by: Peter Wolf <pwolf2310@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
diff --git a/runtime/doc/tags b/runtime/doc/tags
index 0c1b31a..75359f2 100644
--- a/runtime/doc/tags
+++ b/runtime/doc/tags
@@ -10664,6 +10664,7 @@
 termdebug-timeout	terminal.txt	/*termdebug-timeout*
 termdebug-variables	terminal.txt	/*termdebug-variables*
 termdebug_disasm_window	terminal.txt	/*termdebug_disasm_window*
+termdebug_evaluate_in_popup	terminal.txt	/*termdebug_evaluate_in_popup*
 termdebug_map_K	terminal.txt	/*termdebug_map_K*
 termdebug_map_minus	terminal.txt	/*termdebug_map_minus*
 termdebug_map_plus	terminal.txt	/*termdebug_map_plus*
diff --git a/runtime/doc/terminal.txt b/runtime/doc/terminal.txt
index 5020ed5..6b53e02 100644
--- a/runtime/doc/terminal.txt
+++ b/runtime/doc/terminal.txt
@@ -1,4 +1,4 @@
-*terminal.txt*	For Vim version 9.1.  Last change: 2024 Jul 28
+*terminal.txt*	For Vim version 9.1.  Last change: 2024 Oct 27
 
 
 		  VIM REFERENCE MANUAL	  by Bram Moolenaar
@@ -1537,6 +1537,7 @@
 <
 However, the latter form will be deprecated in future releases.
 
+
 Mappings ~
 The termdebug plugin enables a few default mappings.  All those mappings
 are reset to their original values once the termdebug session concludes.
@@ -1591,6 +1592,7 @@
 and the Var window will be shown side by side with the source code window (and
 the height options won't be used).
 
+
 Communication ~
 						*termdebug-communication*
 There is another, hidden, buffer, which is used for Vim to communicate with
@@ -1675,10 +1677,11 @@
 
 However, the latter form will be deprecated in future releases.
 
+
 Change default signs ~
 							*termdebug_signs*
 Termdebug uses the hex number of the breakpoint ID in the signcolumn to
-represent breakpoints. if it is greater than "0xFF", then it will be displayed
+represent breakpoints. If it is greater than "0xFF", then it will be displayed
 as "F+", due to we really only have two screen cells for the sign.
 
 If you want to customize the breakpoint signs: >
@@ -1716,4 +1719,18 @@
 'columns'.  This is useful when the terminal can't be resized by Vim.
 
 
+Evaluate in Popup Window at Cursor ~
+						*termdebug_evaluate_in_popup*
+By default |:Evaluate| will simply echo its output. For larger entities this
+might become difficult to read or even truncated.
+Alternatively, the evaluation result may be output into a popup window at the
+current cursor position: >
+	let g:termdebug_config['evaluate_in_popup'] = v:true
+This can also be used in a "one-shot" manner: >
+	func OnCursorHold()
+	  let g:termdebug_config['evaluate_in_popup'] = v:true
+	  :Evaluate
+	  let g:termdebug_config['evaluate_in_popup'] = v:false
+	endfunc
+<
  vim:tw=78:ts=8:noet:ft=help:norl:
diff --git a/runtime/pack/dist/opt/termdebug/plugin/termdebug.vim b/runtime/pack/dist/opt/termdebug/plugin/termdebug.vim
index 377827e..e7c010d 100644
--- a/runtime/pack/dist/opt/termdebug/plugin/termdebug.vim
+++ b/runtime/pack/dist/opt/termdebug/plugin/termdebug.vim
@@ -121,7 +121,9 @@
 var BreakpointSigns: list<string>
 
 var evalFromBalloonExpr: bool
-var evalFromBalloonExprResult: string
+var evalInPopup: bool
+var evalPopupId: number
+var evalExprResult: string
 var ignoreEvalError: bool
 var evalexpr: string
 # Remember the old value of 'signcolumn' for each buffer that it's set in, so
@@ -202,7 +204,9 @@
   BreakpointSigns = []
 
   evalFromBalloonExpr = false
-  evalFromBalloonExprResult = ''
+  evalInPopup = false
+  evalPopupId = -1
+  evalExprResult = ''
   ignoreEvalError = false
   evalexpr = ''
   # Remember the old value of 'signcolumn' for each buffer that it's set in, so
@@ -1478,10 +1482,23 @@
   evalexpr = exprLHS
 enddef
 
+# Returns whether to evaluate in a popup or not, defaults to false.
+def EvaluateInPopup(): bool
+  if exists('g:termdebug_config')
+    return get(g:termdebug_config, 'evaluate_in_popup', false)
+  endif
+  return false
+enddef
+
 # :Evaluate - evaluate what is specified / under the cursor
 def Evaluate(range: number, arg: string)
   var expr = GetEvaluationExpression(range, arg)
-  echom $"expr: {expr}"
+  if EvaluateInPopup()
+    evalInPopup = true
+    evalExprResult = ''
+  else
+    echomsg $'expr: {expr}'
+  endif
   ignoreEvalError = false
   SendEval(expr)
 enddef
@@ -1541,6 +1558,37 @@
   endif
 enddef
 
+def Popup_format(expr: string): list<string>
+  var lines = expr
+    ->substitute('{', '{\n', 'g')
+    ->substitute('}', '\n}', 'g')
+    ->substitute(',', ',\n', 'g')
+    ->split('\n')
+  var indentation = 0
+  var formatted_lines = []
+  for line in lines
+    var stripped = line->substitute('^\s\+', '', '')
+    if stripped =~ '^}'
+      indentation -= 2
+    endif
+    formatted_lines->add(repeat(' ', indentation) .. stripped)
+    if stripped =~ '{$'
+      indentation += 2
+    endif
+  endfor
+  return formatted_lines
+enddef
+
+def Popup_show(expr: string)
+  var formatted = Popup_format(expr)
+  if evalPopupId != -1
+    popup_close(evalPopupId)
+  endif
+  # Specifying the line is necessary, as the winbar seems to cause issues
+  # otherwise. I.e., the popup would be shown one line too high.
+  evalPopupId = popup_atcursor(formatted, {'line': 'cursor-1'})
+enddef
+
 def HandleEvaluate(msg: string)
   var value = msg
     ->substitute('.*value="\(.*\)"', '\1', '')
@@ -1555,13 +1603,12 @@
     #\ ->substitute('\\0x00', NullRep, 'g')
     #\ ->substitute('\\0x\(\x\x\)', {-> eval('"\x' .. submatch(1) .. '"')}, 'g')
     ->substitute(NullRepl, '\\000', 'g')
-  if evalFromBalloonExpr
-    if empty(evalFromBalloonExprResult)
-      evalFromBalloonExprResult = $'{evalexpr}: {value}'
+  if evalFromBalloonExpr || evalInPopup
+    if empty(evalExprResult)
+      evalExprResult = $'{evalexpr}: {value}'
     else
-      evalFromBalloonExprResult ..= $' = {value}'
+      evalExprResult ..= $' = {value}'
     endif
-    Balloon_show(evalFromBalloonExprResult)
   else
     echomsg $'"{evalexpr}": {value}'
   endif
@@ -1570,8 +1617,12 @@
     # Looks like a pointer, also display what it points to.
     ignoreEvalError = true
     SendEval($'*{evalexpr}')
-  else
+  elseif evalFromBalloonExpr
+    Balloon_show(evalExprResult)
     evalFromBalloonExpr = false
+  elseif evalInPopup
+    Popup_show(evalExprResult)
+    evalInPopup = false
   endif
 enddef
 
@@ -1588,7 +1639,7 @@
     return ''
   endif
   evalFromBalloonExpr = true
-  evalFromBalloonExprResult = ''
+  evalExprResult = ''
   ignoreEvalError = true
   var expr = CleanupExpr(v:beval_text)
   SendEval(expr)
diff --git a/src/testdir/dumps/Test_termdebug_evaluate_in_popup_01.dump b/src/testdir/dumps/Test_termdebug_evaluate_in_popup_01.dump
new file mode 100644
index 0000000..3dde6cc
--- /dev/null
+++ b/src/testdir/dumps/Test_termdebug_evaluate_in_popup_01.dump
@@ -0,0 +1,20 @@
+|U+0&#ffffff0|s|i|n|g| |h|o|s|t| |l|i|b|t|h|r|e|a|d|_|d|b| |l|i|b|r|a|r|y| |"|/+0#00e0003&|l|i|b|/|x|8|6|_|6|4|-|l|i|n|u|x|-|g|n|u|/|l|i|b|t|h|r|e|a|d|_|d|b|.|s|o|.|1|"+0#0000000&|.| 
+@75
+|B|r|e|a|k|p|o|i|n|t| |1|,| |m+0#e0e0004&|a|i|n| +0#0000000&|(|a+0#00e0e07&|r|g|c|=+0#0000000&|1|,| |a+0#00e0e07&|r|g|v|=+0#0000000&|0|x|7|f@6|d|e|f|8|)| @26
+@4|a|t| |X+0#00e0003&|T|D|_|e|v|a|l|u|a|t|e|_|i|n|_|p|o|p|u|p|.|c|:+0#0000000&|9| @42
+|9| @8|r+2#0000e05&|e|t|u|r|n| +0#0000000&|0+0#e000e06&|;+0#e000002&| +0#0000000&@55
+@75
+|g+0#ffffff16#00e0003|d|b| |[|r|u|n@1|i|n|g|]| @43|1|,|1| @11|T|o|p
+| +0#0000000#ffffff0@74
+@75
+@75
+@75
+@75
+|d+0#ffffff16#00e0003|e|b|u|p+0#0000001#ffd7ff255|:| |{| @3|g+0#ffffff16#00e0003|r|a|m| |[|a|c|t|i|v|e|]| @31|0|,|0|-|1| @9|A|l@1
+| +0#0000000#e0e0e08| +2#ffffff16#6c6c6c255|S|t| +0#0000001#ffd7ff255@1|x| |=| |1|,|x+2#ffffff16#6c6c6c255|t| | +0#0000000#e0e0e08@1| +2#ffffff16#6c6c6c255|F|i|n|i|s|h| | +0#0000000#e0e0e08@1| +2#ffffff16#6c6c6c255|C|o|n|t| | +0#0000000#e0e0e08@1| +2#ffffff16#6c6c6c255|S|t|o|p| | +0#0000000#e0e0e08@1| +2#ffffff16#6c6c6c255|E|v|a|l| | +0#0000000#e0e0e08@25
+| +0#0000e05#a8a8a8255@1| +0#0000000#ffffff0@1| +0#0000001#ffd7ff255@1|y| |=| |2| |o+0#0000000#ffffff0|i|n|t| |p| |=| |{|a|r|g|c|,| |2+0#e000002&|}+0#0000000&|;| @43
+| +0#0000e05#a8a8a8255@1| +0#0000000#ffffff0@1|}+0#0000001#ffd7ff255| @6|o+0#0000000#ffffff0|i|n|t|*| |p|_|p|t|r| |=| |&|p|;| @45
+|0+0&#ff404010|1| +0&#5fd7ff255@1>r+0#af5f00255&|e|t|u|r|n| +0#0000000&|0+0#e000002&|;+0#0000000&| @61
+| +0#0000e05#a8a8a8255@1|}+0#0000000#ffffff0| @71
+|X+3&&|T|D|_|e|v|a|l|u|a|t|e|_|i|n|_|p|o|p|u|p|.|c| @33|9|,|3| @11|B|o|t
+|:+0&&|E|v|a|l|u|a|t|e| |p| @63
diff --git a/src/testdir/dumps/Test_termdebug_evaluate_in_popup_01.vim b/src/testdir/dumps/Test_termdebug_evaluate_in_popup_01.vim
new file mode 100644
index 0000000..959798a
--- /dev/null
+++ b/src/testdir/dumps/Test_termdebug_evaluate_in_popup_01.vim
@@ -0,0 +1,9 @@
+" replace hex addresses with |0|x|f@12|
+:%s/|0|x|\(\(\w\|@\)\+|\)\+/|0|x|f@12|/g
+
+" Only keep screen lines relevant to the actual popup and evaluation.
+" Especially the top lines are too instable and cause flakiness between
+" different systems and tool versions.
+normal! G
+normal! 8k
+normal! dgg
diff --git a/src/testdir/dumps/Test_termdebug_evaluate_in_popup_02.dump b/src/testdir/dumps/Test_termdebug_evaluate_in_popup_02.dump
new file mode 100644
index 0000000..0e3fa33
--- /dev/null
+++ b/src/testdir/dumps/Test_termdebug_evaluate_in_popup_02.dump
@@ -0,0 +1,20 @@
+|U+0&#ffffff0|s|i|n|g| |h|o|s|t| |l|i|b|t|h|r|e|a|d|_|d|b| |l|i|b|r|a|r|y| |"|/+0#00e0003&|l|i|b|/|x|8|6|_|6|4|-|l|i|n|u|x|-|g|n|u|/|l|i|b|t|h|r|e|a|d|_|d|b|.|s|o|.|1|"+0#0000000&|.| 
+@75
+|B|r|e|a|k|p|o|i|n|t| |1|,| |m+0#e0e0004&|a|i|n| +0#0000000&|(|a+0#00e0e07&|r|g|c|=+0#0000000&|1|,| |a+0#00e0e07&|r|g|v|=+0#0000000&|0|x|7|f@6|d|e|f|8|)| @26
+@4|a|t| |X+0#00e0003&|T|D|_|e|v|a|l|u|a|t|e|_|i|n|_|p|o|p|u|p|.|c|:+0#0000000&|9| @42
+|9| @8|r+2#0000e05&|e|t|u|r|n| +0#0000000&|0+0#e000e06&|;+0#e000002&| +0#0000000&@55
+@75
+|g+0#ffffff16#00e0003|d|b| |[|r|u|n@1|i|n|g|]| @43|1|,|1| @11|T|o|p
+| +0#0000000#ffffff0@74
+@75
+@75
+@75
+@75
+|d+0#ffffff16#00e0003|e|b|u|p+0#0000001#ffd7ff255|_|p|t|r|:| |0|x|7|f@6|d@1|c|0| |=| |{| +0#ffffff16#00e0003@27|0|,|0|-|1| @9|A|l@1
+| +0#0000000#e0e0e08| +2#ffffff16#6c6c6c255|S|t| +0#0000001#ffd7ff255@1|x| |=| |1|,| @16|o+2#ffffff16#6c6c6c255|n|t| | +0#0000000#e0e0e08@1| +2#ffffff16#6c6c6c255|S|t|o|p| | +0#0000000#e0e0e08@1| +2#ffffff16#6c6c6c255|E|v|a|l| | +0#0000000#e0e0e08@25
+| +0#0000e05#a8a8a8255@1| +0#0000000#ffffff0@1| +0#0000001#ffd7ff255@1|y| |=| |2| @17|}+0#0000000#ffffff0|;| @43
+| +0#0000e05#a8a8a8255@1| +0#0000000#ffffff0@1|}+0#0000001#ffd7ff255| @23| +0#0000000#ffffff0@45
+|0+0&#ff404010|1| +0&#5fd7ff255@1>r+0#af5f00255&|e|t|u|r|n| +0#0000000&|0+0#e000002&|;+0#0000000&| @61
+| +0#0000e05#a8a8a8255@1|}+0#0000000#ffffff0| @71
+|X+3&&|T|D|_|e|v|a|l|u|a|t|e|_|i|n|_|p|o|p|u|p|.|c| @33|9|,|3| @11|B|o|t
+|:+0&&|E|v|a|l|u|a|t|e| |p|_|p|t|r| @59
diff --git a/src/testdir/dumps/Test_termdebug_evaluate_in_popup_02.vim b/src/testdir/dumps/Test_termdebug_evaluate_in_popup_02.vim
new file mode 100644
index 0000000..959798a
--- /dev/null
+++ b/src/testdir/dumps/Test_termdebug_evaluate_in_popup_02.vim
@@ -0,0 +1,9 @@
+" replace hex addresses with |0|x|f@12|
+:%s/|0|x|\(\(\w\|@\)\+|\)\+/|0|x|f@12|/g
+
+" Only keep screen lines relevant to the actual popup and evaluation.
+" Especially the top lines are too instable and cause flakiness between
+" different systems and tool versions.
+normal! G
+normal! 8k
+normal! dgg
diff --git a/src/testdir/test_termdebug.vim b/src/testdir/test_termdebug.vim
index b5c12ae..30176cb 100644
--- a/src/testdir/test_termdebug.vim
+++ b/src/testdir/test_termdebug.vim
@@ -1,6 +1,7 @@
 " Test for the termdebug plugin
 
 source shared.vim
+source screendump.vim
 source check.vim
 
 CheckUnix
@@ -243,6 +244,94 @@
   %bw!
 endfunc
 
+func Test_termdebug_evaluate()
+  let bin_name = 'XTD_evaluate'
+  let src_name = bin_name .. '.c'
+  call s:generate_files(bin_name)
+
+  edit XTD_evaluate.c
+  Termdebug ./XTD_evaluate
+  call WaitForAssert({-> assert_true(get(g:, "termdebug_is_running", v:false))})
+  call WaitForAssert({-> assert_equal(3, winnr('$'))})
+  let gdb_buf = winbufnr(1)
+  wincmd b
+
+  " return stmt in main
+  Break 22
+  call term_wait(gdb_buf)
+  Run
+  call term_wait(gdb_buf, 400)
+  redraw!
+
+  " Evaluate an expression
+  Evaluate n
+  call term_wait(gdb_buf)
+  call assert_equal(execute('1messages')->trim(), '"n": 7')
+  Evaluate argc
+  call term_wait(gdb_buf)
+  call assert_equal(execute('1messages')->trim(), '"argc": 1')
+  Evaluate isprime(n)
+  call term_wait(gdb_buf)
+  call assert_equal(execute('1messages')->trim(), '"isprime(n)": 1')
+
+  wincmd t
+  quit!
+  redraw!
+  call s:cleanup_files(bin_name)
+  %bw!
+endfunc
+
+func Test_termdebug_evaluate_in_popup()
+  CheckScreendump
+  let bin_name = 'XTD_evaluate_in_popup'
+  let src_name = bin_name .. '.c'
+  let code =<< trim END
+    struct Point {
+      int x;
+      int y;
+    };
+
+    int main(int argc, char* argv[]) {
+      struct Point p = {argc, 2};
+      struct Point* p_ptr = &p;
+      return 0;
+    }
+  END
+  call writefile(code, src_name, 'D')
+  call system($'{g:GCC} -g -o {bin_name} {src_name}')
+
+  let lines =<< trim END
+    edit XTD_evaluate_in_popup.c
+    packadd termdebug
+    let g:termdebug_config = {}
+    let g:termdebug_config['evaluate_in_popup'] = v:true
+    Termdebug ./XTD_evaluate_in_popup
+    wincmd b
+    Break 9
+    Run
+  END
+
+  call writefile(lines, 'Xscript', 'D')
+  let buf = RunVimInTerminal('-S Xscript', {})
+  call TermWait(buf, 400)
+
+  call term_sendkeys(buf, ":Evaluate p\<CR>")
+  call TermWait(buf, 400)
+  call VerifyScreenDump(buf, 'Test_termdebug_evaluate_in_popup_01', {})
+
+  call term_sendkeys(buf, ":Evaluate p_ptr\<CR>")
+  call TermWait(buf, 400)
+  call VerifyScreenDump(buf, 'Test_termdebug_evaluate_in_popup_02', {})
+
+  " Cleanup
+  call term_sendkeys(buf, ":Gdb")
+  call term_sendkeys(buf, ":quit!\<CR>")
+  call term_sendkeys(buf, ":qa!\<CR>")
+  call StopVimInTerminal(buf)
+  call delete(bin_name)
+  %bw!
+endfunc
+
 func Test_termdebug_mapping()
   %bw!
   call assert_true(maparg('K', 'n', 0, 1)->empty())
diff --git a/src/version.c b/src/version.c
index 4831281..90fc3b3 100644
--- a/src/version.c
+++ b/src/version.c
@@ -705,6 +705,8 @@
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    817,
+/**/
     816,
 /**/
     815,