Skip to content

Commit fe53be4

Browse files
authored
Prevent text from being fully selected after replace value (#8262)
1 parent e7bbfb0 commit fe53be4

File tree

2 files changed

+85
-1
lines changed

2 files changed

+85
-1
lines changed

panel/models/ace.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,28 @@ export class AcePlotView extends HTMLBoxView {
8484

8585
_update_code_from_model(): void {
8686
if (this._editor && this._editor.getValue() != this.model.code) {
87-
this._editor.setValue(this.model.code)
87+
// Save the current cursor position
88+
const cursorPosition = this._editor.getCursorPosition()
89+
const scrollTop = this._editor.session.getScrollTop()
90+
const scrollLeft = this._editor.session.getScrollLeft()
91+
92+
// Update the value without selecting all text
93+
// The second parameter (-1) keeps cursor where it is
94+
this._editor.setValue(this.model.code, -1)
95+
96+
// Restore cursor position only if it's valid in the new content
97+
const newRowCount = this._editor.session.getLength()
98+
if (cursorPosition.row < newRowCount) {
99+
const newRowLength = this._editor.session.getLine(cursorPosition.row).length
100+
const newColumn = Math.min(cursorPosition.column, newRowLength)
101+
this._editor.moveCursorToPosition({row: cursorPosition.row, column: newColumn})
102+
} else {
103+
// If the cursor was beyond the new document length, move to end
104+
this._editor.moveCursorToPosition({row: newRowCount - 1, column: this._editor.session.getLine(newRowCount - 1).length})
105+
}
106+
this._editor.clearSelection()
107+
this._editor.session.setScrollTop(scrollTop)
108+
this._editor.session.setScrollLeft(scrollLeft)
88109
}
89110
}
90111

panel/tests/ui/widgets/test_codeeditor.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,69 @@ def test_code_editor_on_keyup(page):
4141
wait_until(lambda: editor.value == "print(\"Hello UI!\")", page)
4242

4343

44+
def test_code_editor_value_update_no_selection(page):
45+
"""Test that updating editor value programmatically doesn't select all text."""
46+
editor = CodeEditor(value="foo", on_keyup=True)
47+
serve_component(page, editor)
48+
ace_input = page.locator(".ace_content")
49+
expect(ace_input).to_have_count(1)
50+
51+
# Click in the editor and position cursor after 'f' (position 1)
52+
ace_input.click()
53+
page.keyboard.press('End') # Go to end
54+
page.keyboard.press('ArrowLeft') # Move left twice to be after 'f'
55+
page.keyboard.press('ArrowLeft')
56+
57+
# Type 'X' to verify cursor position (should result in "fXoo")
58+
page.keyboard.type('X')
59+
wait_until(lambda: editor.value == "fXoo", page)
60+
61+
# Update the value programmatically (simulating periodic callback)
62+
# This should preserve cursor position at index 2 (after "fX")
63+
editor.value = "fXoo bar"
64+
wait_until(lambda: editor.value == "fXoo bar", page)
65+
expect(page.locator(".ace_content")).to_have_text("fXoo bar", use_inner_text=True)
66+
67+
# Type some more text - cursor should still be at position 2
68+
# If cursor position was preserved, typing 'Y' should give "fXYoo bar"
69+
# If cursor moved to end, typing 'Y' would give "fXoo barY"
70+
page.keyboard.type('Y')
71+
72+
# Check that cursor was preserved (Y inserted at original position)
73+
wait_until(lambda: editor.value == "fXYoo bar", page)
74+
75+
76+
def test_code_editor_value_update_cursor_to_end_when_invalid(page):
77+
"""Test that cursor moves to end when its position becomes invalid after update."""
78+
editor = CodeEditor(value="line1\nline2\nline3\nline4", on_keyup=True)
79+
serve_component(page, editor)
80+
ace_input = page.locator(".ace_content")
81+
expect(ace_input).to_have_count(1)
82+
83+
# Click in the editor and position cursor on line 3
84+
ace_input.click()
85+
page.keyboard.press('End') # Go to end of document
86+
page.keyboard.press('ArrowUp') # Move up to line 3
87+
page.keyboard.press('Home') # Go to start of line 3
88+
89+
# Type 'X' to verify cursor position (should be at start of line 3)
90+
page.keyboard.type('X')
91+
wait_until(lambda: "Xline3" in editor.value, page)
92+
93+
# Update the value programmatically to something with fewer lines
94+
# The cursor was on line 3 (row 2), but now there's only 1 line
95+
editor.value = "short"
96+
wait_until(lambda: editor.value == "short", page)
97+
expect(page.locator(".ace_content")).to_have_text("short", use_inner_text=True)
98+
99+
# Type some text - cursor should now be at the end since old position is invalid
100+
# Typing 'Y' should give "shortY"
101+
page.keyboard.type('Y')
102+
103+
# Check that cursor moved to end (Y appended at end)
104+
wait_until(lambda: editor.value == "shortY", page)
105+
106+
44107
def test_code_editor_not_on_keyup(page):
45108

46109
editor = CodeEditor(value="print('Hello World!')", on_keyup=False)

0 commit comments

Comments
 (0)