bouzidanas/streamlit-code-editor

How to change value in code editor

SamoPP opened this issue · 10 comments

SamoPP commented

I am trying to use your great component.

What I need to do is change the value/text of code in the code editor (most of the times I need to append, comment stuff) using code if button is pressed. Unfortunately I did not come up with a working solution. Callback on pressing the button does not work. Having session_state variable as input to code_editor works only after two consecutive button presses. Please see code below.

How to do this changing of value/text/code in code editor using streamlit app code itself and that this code change is reflected in the code editor?

import streamlit as st
from code_editor import code_editor

def change_value(value):
    st.session_state.code = value

if 'code' not in st.session_state:
    st.session_state.code_text = 'a = 2;'

response_dict = code_editor(st.session_state.code_text, lang="c_cpp", allow_reset=True, key="code")

if 'type' in response_dict and 'text' in response_dict:
        if response_dict['type'] == 'submit' and len(response_dict['text']) != 0:
            st.session_state.code_text = response_dict['text']

# Try approach 1: Nothing happens. Code in editor not changed.
value_to_change = st.text_input("Enter changed value", value="test = 1;")
if st.button("Change code",  on_click=change_value, args=[value_to_change]):
    st.write(f"Changed value to {value_to_change}")

# Try approach 2: Code in editor is changed only after consecutive button press.
if st.button("Try to change code again"):
    st.session_state.code_text = "test = 2;"
    st.write(f"Changed value to {st.session_state.code_text}")

Hi!

There is a typo with your change_text function that is responsible for your code not working as intended.

Change the line of code in the function:

def change_value(value):
-    st.session_state.code = value
+    st.session_state.code_text = value

That should fix the main issue with your first method. The reason your second method works after two tries is because you need to run the script again after changing session state variable so that the code_editor function sees the new value. So just add st.experimental_rerun()

if st.button("Try to change code again"):
    st.session_state.code_text = "test = 5;" # change to different value from 2 to differentiate from initial value 
    st.write(f"Changed value to {st.session_state.code_text}")
+   st.experimental_rerun()

I prefer your first method in this case

So fix code should look like:

import streamlit as st
from code_editor import code_editor

if 'code' not in st.session_state:
    st.session_state.code_text = 'a = 2;'

def change_value(value):
    st.session_state.code_text = value

response_dict = code_editor(st.session_state.code_text, lang="c_cpp", allow_reset=True, key="code")

if 'type' in response_dict and 'text' in response_dict:
        if response_dict['type'] == 'submit' and len(response_dict['text']) != 0:
            st.session_state.code_text = response_dict['text']

# Try approach 1: Nothing happens. Code in editor not changed.
value_to_change = st.text_input("Enter changed value", value="test = 1;")
if st.button("Change code",  on_click=change_value, args=[value_to_change]):
    st.write(f"Changed value to {value_to_change}")

# Try approach 2: Code in editor is changed only after consecutive button press.
if st.button("Try to change code again"):
    st.session_state.code_text = "test = 5;"
    st.write(f"Changed value to {st.session_state.code_text}")
    st.experimental_rerun()
SamoPP commented

This was very helpful. Thanks.

Unfortunately it does not solve my problem...

I have updated my code of the app so it more clearly show what the issue is:

import streamlit as st
from code_editor import code_editor

def does_code_start_with_if(code):
    return code.startswith("if")

def prepend_if_name_equals_variable_statement_and_enclose_in_curly_brackets(input_string, variable):
    if_statement = f'if (name() == "{variable}") {{'
    indented_string = '\n'.join(['\t' + line for line in input_string.splitlines()])
    modified_string = if_statement + '\n' + indented_string + '\n' + '}'
    return modified_string

if 'code' not in st.session_state:
    st.session_state.code_text = ""

if 'variable_to_add' not in st.session_state:
    st.session_state.variable_to_add = ""

def change_value():
    st.session_state.code_text = prepend_if_name_equals_variable_statement_and_enclose_in_curly_brackets(st.session_state.code_text, st.session_state.variable_to_add)

response_dict = code_editor(st.session_state.code_text, lang="c_cpp", theme="dark", height="200px", allow_reset=True, options={"wrap": True, "Focus": True}, key="code")

if 'type' in response_dict and 'text' in response_dict:
        if response_dict['type'] == 'submit' and len(response_dict['text']) != 0:
            st.session_state.code_text = response_dict['text']
st.write(response_dict)
if st.session_state.code_text is not None and st.session_state.code_text != "":
    is_if_statement_already_present = does_code_start_with_if(st.session_state.code_text)
    if is_if_statement_already_present is False:
        st.session_state.variable_to_add = st.text_input("Add variable to code file", value=st.session_state.variable_to_add)
        st.button("Add if name() == 'variable' ... ",  on_click=change_value)
    if st.button("Save code"):
        st.code(st.session_state.code_text, language="c_cpp")
        print(st.session_state.code_text)

What I want to do is the following: I want to edit some code, then add a variable name and then prepend if statement and when I press Save code button the code should be visible (in actuality saved to disk, but not implemented for the sake of brevity) with the if statement added. Unfortunately it is not. It reverts back to code without if statement.

Please see attached video. Thanks for your help since it is obvious I do not understand how Streamlit is processing code...

output.mp4
SamoPP commented

What is really strange is that if I follow this scenario I can just press Ctrl+Enter and code in code editor will alternate between one with and without (original) if statement. See attached video below.

Scenario for replication:

  1. Enter "print("Hello!") in code editor and press Ctrl+Enter
  2. Enter name of variable "AAA"
  3. Press button "Add if name() == 'variable' ... " (if statement added)
  4. Press "Save code" button and if statement disapears
  5. Once again press button "Add if name() == 'variable' ... "
  6. Just keep pressing Ctr+Enter and see how content of code edito switches between two versions of code (one with and one without if statement).

To me it is strange. I am sure it is just me not understanding how Streamlit works...

output.mp4

I am sure it is just me not understanding how Streamlit works...

This is kindof what you have to mentally wrestle with when it comes to Streamlit. When any variable changes or a response from a component like a button is returned, your Streamlit script is rerun from top to bottom. However, not everyline is re-executed and not every component is re-rendered. Streamlit does some "magic" where it determines where the change in the script occurs then looks after this line to see what components and variables are impacted by the change and updates them (rerenders components).

I think this is kindof the issue with your script. I also noticed that you are using a fixed key but are not checking for new 'id' value so you might be entering the conditional when you dont intend to.

I will try to explain what I mean. When you use a fixed key for a component, Streamlit reuses the component in the next reruns of the script instead of recreating the component from zero each time. This helps with fluidity and reduces risk of losing the contents of the editor (stabalizes the editor in your app) so this is a good thing. But there is another consequence that you might not be aware of. Since the component isnt destroyed and recreated, its response dictionary isnt either. Basically, the last response of the code editor sticks around until you get a new response. This can be a problem when your script rerenders because the if statements that come after only check if it the dictionary has contents and since the last response sticks around, the script will enter the if statement as if it received a new response.
The way to prevent this is to check if the id value in the response has changed. Every new response has a new id value. So you can use another session_state variable to hold the response id and check if the new id is different from the old. If so, then handle the response. If not, do nothing...

I noticed something else.

if 'code' not in st.session_state:
    st.session_state.code_text = ""

This is a little confusing to me. The typical way to initialize a session_state variable is to make sure the key you check for is the same as the key you use to initialize the variable:

if 'key' not in st.session_state:
    st.session_state["key"] = ""

or

if 'key' not in st.session_state:
    st.session_state.key = ""

so in your case:

if 'code_text' not in st.session_state:
    st.session_state["code_text"] = ""

Looks like adding an 'id' check fixes your issue.

Change this:

if 'type' in response_dict and 'text' in response_dict:
        if response_dict['type'] == 'submit' and len(response_dict['text']) != 0:
            st.session_state.code_text = response_dict['text']

to this:

if 'code_id' not in st.session_state:
    st.session_state['code_id'] = ""

if response_dict['type'] == 'submit' and len(response_dict['text']) != 0 and response_dict['id'] != st.session_state.code_id:
    st.session_state.code_text = response_dict['text']
    st.session_state.code_id = response_dict['id']

I also recommend you make the change I suggested in the previous comment.

SamoPP commented

Thank you very much for this advanced Streamlit tutorial. Really appreciated.

I implemented all the suggestions you posted above. Unfortunately it still does not work. After some pressing the buttons and some editing of code in code editor thing stops working. I am confused as never before. See scenario how to replicate the problem and code below. I also post the video.

This is the code (with your suggested edits implemented:

import streamlit as st
from code_editor import code_editor

def does_code_start_with_if(code):
    return code.startswith("if")

def prepend_if_name_equals_variable_statement_and_enclose_in_curly_brackets(input_string, variable):
    if_statement = f'if (name() == "{variable}") {{'
    indented_string = '\n'.join(['\t' + line for line in input_string.splitlines()])
    modified_string = if_statement + '\n' + indented_string + '\n' + '}'
    return modified_string

if 'code_text' not in st.session_state:
    st.session_state.code_text = ""

if 'variable_to_add' not in st.session_state:
    st.session_state.variable_to_add = ""

if 'code_id' not in st.session_state:
    st.session_state.code_id = ""

def change_value():
    st.session_state.code_text = prepend_if_name_equals_variable_statement_and_enclose_in_curly_brackets(st.session_state.code_text, st.session_state.variable_to_add)

response_dict = code_editor(st.session_state.code_text, lang="c_cpp", theme="dark", height="200px", allow_reset=True, options={"wrap": True, "Focus": True}, key="code")

if 'type' in response_dict and 'text' in response_dict:
        if response_dict['type'] == 'submit' and len(response_dict['text']) != 0 and response_dict['id'] != st.session_state.code_id:
            st.session_state.code_text = response_dict['text']
            st.session_state.code_id = response_dict['id']

st.write(response_dict)

if st.session_state.code_text is not None and st.session_state.code_text != "":
    is_if_statement_already_present = does_code_start_with_if(st.session_state.code_text)
    if is_if_statement_already_present is False:
        st.session_state.variable_to_add = st.text_input("Add variable to code file", value=st.session_state.variable_to_add, key="variable_to_add_text_input")
        st.button("Add if name() == 'variable' ... ",  on_click=change_value, key="add_if_name_equals_variable_button")
    if st.button("Save code", key="save_code_button"):
        st.code(st.session_state.code_text, language="cpp")
        print(st.session_state.code_text)

This is the scenario to replicate the problem:

  1. Enter some code in code editor. Say "print("Working?");"
  2. Press "Add if name() == 'variable' ... " button. "if ..." statement is added to code correctly.
  3. Press "Save code" button
  4. Go and delete "if ..." statement in editor
  5. Press "Add if name() == 'variable' ... " button has no effect. "if ..." statement does not appear in code editor. It is however reflected if you press "Save code" button

Here is the video:

output.mp4

Hi,

This is my fault. The code I gave you in my last comment is missing a line. I checked the code I ran last time and noticed that your code is missing a line thats in my code. Then I checked my comment and noticed that I omitted this line. Sorry about this.

Change this:

if response_dict['type'] == 'submit' and len(response_dict['text']) != 0 and response_dict['id'] != st.session_state.code_id:
    st.session_state.code_text = response_dict['text']
    st.session_state.code_id = response_dict['id']

to this:

if response_dict['type'] == 'submit' and len(response_dict['text']) != 0 and response_dict['id'] != st.session_state.code_id:
    st.session_state.code_text = response_dict['text']
    st.session_state.code_id = response_dict['id']
    st.experimental_rerun()

This makes sense because you gotta always rerun the script when you change state variables (some way or fashion like with callback or with st.experimental_rerun() of st.rerun()) so the rest of the app can incorporate/respond to the changed state. State data (ie variables) is always supposed to be in-sync with what the app is currently reflecting/showing. So when you change any one of them or all of them, you should make sure the app is able to see the changes and adjust itself accordingly. An easy way to guarantee this is to use something like st.rerun() or st.experimental_rerun() (depending on which version of streamlit you have installed).

Also, you dont need:

if 'type' in response_dict and 'text' in response_dict:

because if response_dict exists, then it will always have the type and text properties (even if they just contain empty strings). So the if statement will never be false. I adjusted the code editor a while back to make sure of this so people wouldnt have to always check if properties exists (reducing lines of code you have to write)

SamoPP commented

This indeed seems to work. Thanks a thousand times.

No worries!

I also noticed you are using the focus property of options which will not work. There is an argument added to code_editor function that will put active cursor in the editor. See here in the docs: https://code-editor-documentation.streamlit.app/Basic_customization#focus