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)