Repository: xonsh/xonsh

Issue:
$ENABLE_ASYNC_PROMPT not handling nested format string in $PROMPT
Using a prompt with the "nested format string" syntax, e.g {_<prompt_field>_: _text_ **{}** _more_text_}, async prompt seems to ignore static string (e.g _text_, _more_text_) when it does the delayed update of the prompt field.  The static text (or something) is initially displayed, but then overwritten when the async update happens.

## xonfig

<details>

```
$ xonfig
+------------------+----------------------+
| xonsh            | 0.9.23               |
| Git SHA          | fdbc82a1             |
| Commit Date      | Oct 11 23:34:07 2020 |
| Python           | 3.8.6                |
| PLY              | 3.11                 |
| have readline    | False                |
| prompt toolkit   | 3.0.7                |
| shell type       | prompt_toolkit       |
| pygments         | 2.7.1                |
| on posix         | False                |
| on linux         | False                |
| on darwin        | False                |
| on windows       | True                 |
| on cygwin        | False                |
| on msys2         | False                |
| is superuser     | False                |
| default encoding | utf-8                |
| xonsh encoding   | utf-8                |
| encoding errors  | surrogateescape      |
| on jupyter       | False                |
| jupyter kernel   | None                 |
+------------------+----------------------+
```

</details>

## Expected Behavior
Prompt should look the same after async update as it would look without async update.

## Current Behavior

![image](https://user-images.githubusercontent.com/1971300/95779210-81eae680-0c7e-11eb-8dff-5370316251e9.png)

@jnoortheen, could you please take a look?  I couldn't figure out what was happening here.

## For community
⬇️  **Please click the 👍 reaction instead of leaving a `+1` or 👍  comment**



Discussion:
Thanks for the bug report!
@bobhy it has unresolved tokens in a list and updates the item at index in place. It happens in formatter.py and updator.py . I will try to check this coming weekend. 

Resolving patch:
diff --git a/xonsh/prompt/base.py b/xonsh/prompt/base.py
--- a/xonsh/prompt/base.py
+++ b/xonsh/prompt/base.py
@@ -73,7 +73,7 @@ def _format_field(self, field, spec, conv, **kwargs):
             val = builtins.__xonsh__.env[field[1:]]
             return _format_value(val, spec, conv)
         elif field in self.fields:
-            val = self._get_field_value(field, **kwargs)
+            val = self._get_field_value(field, spec=spec, conv=conv, **kwargs)
             return _format_value(val, spec, conv)
         else:
             # color or unknown field, return as is
diff --git a/xonsh/ptk_shell/formatter.py b/xonsh/ptk_shell/formatter.py
--- a/xonsh/ptk_shell/formatter.py
+++ b/xonsh/ptk_shell/formatter.py
@@ -48,14 +48,14 @@ def _format_prompt(self, template=DEFAULT_PROMPT, **kwargs):
         return self._get_tokens(template, **kwargs)
 
     def _no_cache_field_value(
-        self, field, field_value, idx=None, async_prompt=None, **_
+        self, field, field_value, async_prompt=None, idx=None, spec=None, conv=None, **_
     ):
         """This branch is created so that caching fields per prompt would still work."""
         func = functools.partial(super()._no_cache_field_value, field, field_value)
 
         if async_prompt is not None and callable(field_value):
             # create a thread and return an intermediate result
-            return async_prompt.submit_section(func, field, idx)
+            return async_prompt.submit_section(func, field, idx, spec, conv)
 
         return func()
 
diff --git a/xonsh/ptk_shell/updator.py b/xonsh/ptk_shell/updator.py
--- a/xonsh/ptk_shell/updator.py
+++ b/xonsh/ptk_shell/updator.py
@@ -7,6 +7,8 @@
 
 from prompt_toolkit import PromptSession
 from prompt_toolkit.formatted_text import PygmentsTokens
+
+from xonsh.prompt.base import _format_value
 from xonsh.style_tools import partial_color_tokenize, style_as_faded
 
 
@@ -68,7 +70,10 @@ def __init__(self, name: str, session: PromptSession, executor: Executor):
         self.executor = executor
 
         # (Key: the future object) that is created for the (value: index/field_name) in the tokens list
-        self.futures: tp.Dict[concurrent.futures.Future, tp.Union[int, str]] = {}
+        self.futures: tp.Dict[
+            concurrent.futures.Future,
+            tp.Tuple[str, tp.Optional[int], tp.Optional[str], tp.Optional[str]],
+        ] = {}
 
     def start_update(self, on_complete):
         """Listen on futures and update the prompt as each one completed.
@@ -81,26 +86,40 @@ def start_update(self, on_complete):
             callback to notify after all the futures are completed
         """
         for fut in concurrent.futures.as_completed(self.futures):
-            val = fut.result() or ""
+            val = fut.result()
 
             if fut not in self.futures:
                 # rare case where the future is completed but the container is already cleared
                 # because new prompt is called
                 continue
 
-            token_index = self.futures[fut]
-            if isinstance(token_index, int):
-                self.tokens[token_index] = val
+            placeholder, idx, spec, conv = self.futures[fut]
+            # example: placeholder="{field}", idx=10, spec="env: {}"
+
+            if isinstance(idx, int):
+                self.update_token(idx, val, spec, conv)
             else:  # when the function is called outside shell.
                 for idx, sect in enumerate(self.tokens):
-                    if token_index in sect:
-                        self.tokens[idx] = sect.replace(token_index, val)
+                    if placeholder in sect:
+                        val = sect.replace(placeholder, val)
+                        self.update_token(idx, val, spec, conv)
 
             # calling invalidate in less period is inefficient
             self.invalidate()
 
         on_complete(self.name)
 
+    def update_token(
+        self,
+        idx: int,
+        val: tp.Optional[str],
+        spec: tp.Optional[str],
+        conv: tp.Optional[str],
+    ) -> None:
+        """Update tokens list in-place"""
+        if idx < len(self.tokens):
+            self.tokens[idx] = _format_value(val, spec, conv)
+
     def invalidate(self):
         """Create a timer to update the prompt. The timing can be configured through env variables.
         threading.Timer is used to stop calling invalidate frequently.
@@ -129,9 +148,16 @@ def stop(self):
             fut.cancel()
         self.futures.clear()
 
-    def submit_section(self, func: tp.Callable, field: str, idx: int = None):
+    def submit_section(
+        self,
+        func: tp.Callable,
+        field: str,
+        idx: tp.Optional[int] = None,
+        spec: tp.Optional[str] = None,
+        conv=None,
+    ):
         future, intermediate_value, placeholder = self.executor.submit(func, field)
-        self.futures[future] = placeholder if idx is None else idx
+        self.futures[future] = (placeholder, idx, spec, conv)
         return intermediate_value
 
 


Test patch: