Skip to content

Commit ca25038

Browse files
Improve text editor (#2619)
* Add bracket match validation. WIP better content assist Text Editor * Final improvements in autocomplete in Text Editor
1 parent 9056859 commit ca25038

File tree

5 files changed

+173
-32
lines changed

5 files changed

+173
-32
lines changed

CHANGELOG.adoc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to http://semver.org/spec/v2.0.0.html[Semantic Versioni
1010

1111
=== Added
1212

13+
- Added indication of matching brackets, ``()``, ``{}``, ``[]``, in Text Editor
1314
- Added context menu to RIDE tray icon. Options Show, Hide and Close
1415
- Added sincronization with Project Explorer to navigate to selected item, Test Case, Keyword, Variable, in Text Editor
1516
Note: This feature is working fine in Fedora 38, but not on Windows and macOS.
@@ -24,6 +25,10 @@ and this project adheres to http://semver.org/spec/v2.0.0.html[Semantic Versioni
2425
- Fixed title of User Keyword in Grid Editor always showing ``Find Usages`` instead of the keyword name
2526
- Fixed renaming keywords when they were arguments of ``Run Keywords`` in Setups and Teardowns
2627

28+
=== Changed
29+
30+
- Improve Text Editor auto-suggestions to keep libraries prefixes.
31+
2732
== https://github.com/robotframework/RIDE/blob/master/doc/releasenotes/ride-2.0.6.rst[2.0.6] - 2023-06-10
2833

2934
=== Added

src/robotide/application/releasenotes.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -168,15 +168,16 @@ def set_content(self, html_win, content):
168168
</ul>
169169
<p><strong>New Features and Fixes Highlights</strong></p>
170170
<ul class="simple">
171+
<li>Added indication of matching brackets, <b>()</b>, <b>"""'''{}'''f"""</b>, <b>[]</b>, in Text Editor</li>
171172
<li>Fixed non syncronized expanding/collapse of Settings panel in Grid Editor, on Linux</li>
172-
<li>Fixed not working the deletion of cells commented with ``# `` in Grid Editor with ``Ctrl-Shift-D``</li>
173+
<li>Fixed not working the deletion of cells commented with <b># </b> in Grid Editor with <b>Ctrl-Shift-D</b></li>
173174
<li>Fixed empty line being always added to the Variables section in Text Editor</li>
174175
<li>Improved project file system changes and reloading</li>
175176
<li>Added context menu to RIDE tray icon. Options Show, Hide and Close</li>
176177
<li>Added sincronization with Project Explorer to navigate to selected item, Test Case, Keyword, Variable, in Text
177178
Editor</li>
178-
<li>Control commands (``FOR``, ``IF``, ``TRY``, etc) will only be colorized as valid keywords when typed in all caps in Grid Editor</li>
179-
<li>Newlines in Grid Editor can be made visible with the `filter newlines` set to False, by editing `settings.cfg`</li>
179+
<li>Control commands (<b>FOR</b>, <b>IF</b>, <b>TRY</b>, etc) will only be colorized as valid keywords when typed in all caps in Grid Editor</li>
180+
<li>Newlines in Grid Editor can be made visible with the <b>filter newlines</b> set to False, by editing <em>settings.cfg</em></li>
180181
<li>Improve auto-suggestions of keywords in Grid Editor by allowing to close suggestions list with keys ARROW_LEFT or ARROW_RIGHT</li>
181182
<li>Improve Text Editor auto-suggestions by using: selected text, text at left or at right of cursor</li>
182183
</ul>
@@ -229,6 +230,6 @@ def set_content(self, html_win, content):
229230
<pre class="literal-block">
230231
python -m robotide.postinstall -install
231232
</pre>
232-
<p>RIDE {VERSION} was released on 06/Aug/2023.</p>
233+
<p>RIDE {VERSION} was released on 13/Aug/2023.</p>
233234
</div>
234235
"""

src/robotide/editor/texteditor.py

Lines changed: 153 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -612,13 +612,17 @@ def on_content_assist(self, event):
612612
return
613613
self.store_position()
614614
selected = self.source_editor.get_selected_or_near_text()
615-
sugs = [s.name for s in self._suggestions.get_suggestions(
616-
selected or '')]
615+
sugs = []
616+
for start in selected:
617+
sugs.extend(s.name for s in self._suggestions.get_suggestions(start))
618+
if len(sugs) > 0:
619+
sugs = [s for s in sugs if s != '']
617620
if sugs:
618621
self.source_editor.AutoCompSetDropRestOfWord(True)
619622
self.source_editor.AutoCompSetSeparator(ord(';'))
620623
self.source_editor.AutoCompShow(0, ";".join(sugs))
621624
self._showing_list = True
625+
self.set_editor_caret_position()
622626
else:
623627
self.source_editor.SetInsertionPoint(self._position) # We should know if list was canceled or value change
624628

@@ -1479,16 +1483,75 @@ class RobotDataEditor(stc.StyledTextCtrl):
14791483

14801484
def __init__(self, parent, readonly=False):
14811485
stc.StyledTextCtrl.__init__(self, parent)
1486+
self.parent = parent
14821487
self._settings = parent.source_editor_parent.app.settings
14831488
self.readonly = readonly
14841489
self.SetMarginType(self.margin, stc.STC_MARGIN_NUMBER)
14851490
self.SetLexer(stc.STC_LEX_CONTAINER)
14861491
self.SetReadOnly(True)
14871492
self.SetUseTabs(False)
14881493
self.SetTabWidth(parent.tab_size)
1494+
self.Bind(stc.EVT_STC_UPDATEUI, self.on_update_ui)
14891495
self.Bind(stc.EVT_STC_STYLENEEDED, self.on_style)
14901496
self.Bind(stc.EVT_STC_ZOOM, self.on_zoom)
1497+
# DEBUG:
1498+
self.Bind(wx.EVT_KEY_DOWN, self.on_key_pressed)
14911499
self.stylizer = RobotStylizer(self, self._settings, self.readonly)
1500+
# register some images for use in the AutoComplete box.
1501+
# self.RegisterImage(1, Smiles.GetBitmap()) # DEBUG was images.
1502+
self.RegisterImage(1, wx.ArtProvider.GetBitmap(wx.ART_FLOPPY, size=(16, 16)))
1503+
self.RegisterImage(2, wx.ArtProvider.GetBitmap(wx.ART_NEW, size=(16, 16)))
1504+
self.RegisterImage(3, wx.ArtProvider.GetBitmap(wx.ART_COPY, size=(16, 16)))
1505+
1506+
def on_key_pressed(self, event):
1507+
if self.CallTipActive():
1508+
self.CallTipCancel()
1509+
key = event.GetKeyCode()
1510+
if key == 32 and event.ControlDown():
1511+
pos = self.GetCurrentPos()
1512+
1513+
# Tips
1514+
if event.ShiftDown():
1515+
self.CallTipSetBackground("yellow")
1516+
self.CallTipShow(pos, 'lots of of text: blah, blah, blah\n\n'
1517+
'show some suff, maybe parameters..\n\n'
1518+
'fubar(param1, param2)')
1519+
# Code completion
1520+
else:
1521+
"""
1522+
kw = list(keyword.kwlist[:])
1523+
kw.append("zzzzzz?2")
1524+
kw.append("aaaaa?2")
1525+
kw.append("__init__?3")
1526+
kw.append("zzaaaaa?2")
1527+
kw.append("zzbaaaa?2")
1528+
kw.append("this_is_a_longer_value")
1529+
# kw.append("this_is_a_much_much_much_much_much_much_much_longer_value")
1530+
1531+
kw.sort() # Python sorts are case-sensitive
1532+
self.AutoCompSetIgnoreCase(True) # so this needs to match
1533+
1534+
# Images are specified with an appended "?type"
1535+
for i in range(len(kw)):
1536+
if kw[i] in keyword.kwlist:
1537+
kw[i] = kw[i] + "?1"
1538+
self.AutoCompSetDropRestOfWord(True)
1539+
self.AutoCompSetSeparator(ord(';'))
1540+
self.AutoCompShow(0, ";".join(kw))
1541+
"""
1542+
selected = self.get_selected_or_near_text()
1543+
sugs = []
1544+
for start in selected:
1545+
sugs.extend(s.name for s in self.parent._suggestions.get_suggestions(start))
1546+
if len(sugs) > 0:
1547+
sugs = [s for s in sugs if s != '']
1548+
if sugs:
1549+
self.AutoCompSetDropRestOfWord(True)
1550+
self.AutoCompSetIgnoreCase(True)
1551+
self.AutoCompSetSeparator(ord(';'))
1552+
self.AutoCompShow(0, ";".join(sugs))
1553+
else:
1554+
event.Skip()
14921555

14931556
def set_text(self, text):
14941557
self.SetReadOnly(False)
@@ -1522,32 +1585,98 @@ def calc_margin_width(self):
15221585
return width + self.TextWidth(style, "1")
15231586

15241587
def get_selected_or_near_text(self):
1588+
content = set()
15251589
# First get selected text
15261590
selected = self.GetSelectedText()
15271591
if selected:
1528-
self.SetInsertionPoint(self.GetSelectionStart())
1529-
return selected
1592+
start_pos = self.GetSelectionStart()
1593+
if selected.endswith('.'): # Special cases for libraries prefix
1594+
self.SetInsertionPoint(start_pos + len(selected))
1595+
elif len(selected.split('.')) > 1:
1596+
parts = selected.split('.')
1597+
self.SetSelectionStart(start_pos + len(parts[0]) + 1)
1598+
self.SetSelectionEnd(start_pos + len(selected))
1599+
self.SetInsertionPoint(start_pos + len(parts[0]) + 1)
1600+
else:
1601+
self.SetSelectionStart(start_pos)
1602+
self.SetSelectionEnd(start_pos + len(selected))
1603+
self.SetInsertionPoint(start_pos + len(selected))
1604+
content.add(selected.strip())
15301605
# Next get text on the left
1531-
cur_pos = self.GetInsertionPoint()
1532-
self.WordLeftEndExtend()
1533-
selected = self.GetSelectedText()
1534-
select = selected.lstrip()
1535-
if select and len(select) > 0:
1536-
start_pos = cur_pos - len(select)
1537-
self.SetInsertionPoint(start_pos)
1538-
self.SetSelectionStart(start_pos)
1539-
self.SetSelectionEnd(cur_pos - len(select))
1540-
return select
1541-
# Finally get text on the right
1542-
cur_pos = self.GetInsertionPoint()
1543-
self.SetSelectionStart(cur_pos)
1544-
self.WordRightEndExtend()
1545-
selected = self.GetSelectedText()
1546-
select = selected.strip()
1547-
if select and len(select) > 0:
1548-
cur_pos = self.GetInsertionPoint()
1549-
self.SetInsertionPoint(cur_pos - len(select))
1550-
return select
1606+
text = self.GetCurLine()[0]
1607+
start_pos = self.GetInsertionPoint()
1608+
line = self.GetCurrentLine()
1609+
line_end = self.GetLineEndPosition(line)
1610+
size = self.GetLineLength(line)
1611+
min_pos = line_end - size
1612+
pos_in_line = start_pos - min_pos
1613+
if pos_in_line > 0:
1614+
start_chr = end_chr = None
1615+
for i in range(pos_in_line, 1, -1):
1616+
if text[i] == ' ' and text[i-1] == ' ':
1617+
start_chr = i + 1
1618+
break
1619+
for i in range(pos_in_line, size):
1620+
if text[i] == ' ' and text[i+1] == ' ':
1621+
end_chr = i
1622+
break
1623+
value = None
1624+
if start_chr is not None:
1625+
if end_chr is not None:
1626+
value = text[start_chr:end_chr]
1627+
else:
1628+
value = text[start_chr:].strip()
1629+
elif end_chr is not None:
1630+
value = text[pos_in_line:end_chr]
1631+
if value:
1632+
# self.SetInsertionPoint(self.GetSelectionStart())
1633+
if start_chr:
1634+
start_pos = min_pos + start_chr
1635+
else:
1636+
start_pos = min_pos + pos_in_line
1637+
if value.endswith('.'): # Special cases for libraries prefix
1638+
self.SetInsertionPoint(start_pos + len(value))
1639+
elif len(value.split('.')) > 1:
1640+
parts = value.split('.')
1641+
self.SetSelectionStart(start_pos + len(parts[0]) + 1)
1642+
self.SetSelectionEnd(start_pos + len(value))
1643+
self.SetInsertionPoint(start_pos + len(parts[0]) + 1)
1644+
else:
1645+
self.SetSelectionStart(start_pos)
1646+
self.SetSelectionEnd(start_pos + len(value))
1647+
self.SetInsertionPoint(start_pos)
1648+
content.add(value)
1649+
return content if content else ['']
1650+
1651+
def on_update_ui(self, evt):
1652+
_ = evt
1653+
# check for matching braces
1654+
brace_at_caret = -1
1655+
brace_opposite = -1
1656+
char_before = None
1657+
caret_pos = self.GetCurrentPos()
1658+
1659+
if caret_pos > 0:
1660+
char_before = self.GetCharAt(caret_pos - 1)
1661+
1662+
# check before
1663+
if char_before and chr(char_before) in "[]{}()":
1664+
brace_at_caret = caret_pos - 1
1665+
1666+
# check after
1667+
if brace_at_caret < 0:
1668+
char_after = self.GetCharAt(caret_pos)
1669+
1670+
if char_after and chr(char_after) in "[]{}()":
1671+
brace_at_caret = caret_pos
1672+
1673+
if brace_at_caret >= 0:
1674+
brace_opposite = self.BraceMatch(brace_at_caret)
1675+
1676+
if brace_at_caret != -1 and brace_opposite == -1:
1677+
self.BraceBadLight(brace_at_caret)
1678+
else:
1679+
self.BraceHighlight(brace_at_caret, brace_opposite)
15511680

15521681

15531682
class FromStringIOPopulator(robotapi.populators.FromFilePopulator):

src/robotide/ui/mainframe.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -254,9 +254,15 @@ def on_close(self, event):
254254
PUBLISHER.unsubscribe(self._set_label, RideTreeSelection)
255255
RideClosing().publish()
256256
# DEBUG: Wrap in try/except for RunTime error
257-
self._task_bar_icon.RemoveIcon()
258-
self._task_bar_icon.Destroy()
259-
self.Destroy()
257+
try:
258+
self._task_bar_icon.RemoveIcon()
259+
self._task_bar_icon.Destroy()
260+
except RuntimeError:
261+
pass
262+
try:
263+
self.Destroy()
264+
except RuntimeError:
265+
pass
260266
app = wx.GetApp()
261267
if app is not self._application:
262268
# other wx app instance created unexpectedly

src/robotide/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@
1414
# limitations under the License.
1515
#
1616
# Automatically generated by `tasks.py`.
17-
VERSION = 'v2.0.7dev9'
17+
VERSION = 'v2.0.7dev10'

0 commit comments

Comments
 (0)