Robot Framework Integrated Development Environment (RIDE)
contentassist.py
Go to the documentation of this file.
1 # Copyright 2008-2015 Nokia Networks
2 # Copyright 2016- Robot Framework Foundation
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
7 #
8 # http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 
16 from os.path import relpath, dirname, isdir
17 from sys import platform
18 
19 import wx
20 from wx import Colour
21 from wx.lib.expando import ExpandoTextCtrl
22 from wx.lib.filebrowsebutton import FileBrowseButton
23 
24 from .. import context, utils
25 from ..context import IS_MAC
26 from ..namespace.suggesters import SuggestionSource
27 from ..spec.iteminfo import VariableInfo
28 from .popupwindow import RidePopupWindow, HtmlPopupWindow
29 
30 
33 _PREFERRED_POPUP_SIZE = (400, 200)
34 
35 
37 
38  def __init__(self, suggestion_source):
39  from ..preferences import RideSettings
40 
43  _settings = RideSettings()
44  self.general_settingsgeneral_settings = _settings['General']
45  self.color_backgroundcolor_background = self.general_settingsgeneral_settings['background']
46  self.color_foregroundcolor_foreground = self.general_settingsgeneral_settings['foreground']
47  self.color_secondary_backgroundcolor_secondary_background = self.general_settingsgeneral_settings['secondary background']
48  self.color_secondary_foregroundcolor_secondary_foreground = self.general_settingsgeneral_settings['secondary foreground']
49  self.color_background_helpcolor_background_help = self.general_settingsgeneral_settings['background help']
50  self.color_foreground_textcolor_foreground_text = self.general_settingsgeneral_settings['foreground text']
51  self._popup_popup = ContentAssistPopup(self, suggestion_source)
52  self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDownOnKeyDown)
53  self.Bind(wx.EVT_CHAR, self.OnCharOnChar)
54  self.Bind(wx.EVT_KILL_FOCUS, self.OnFocusLostOnFocusLost)
55  self.Bind(wx.EVT_MOVE, self.OnFocusLostOnFocusLost)
56  self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroyOnDestroy)
57  self._showing_content_assist_showing_content_assist = False
58  self._row_row = None
59  self.gherkin_prefixgherkin_prefix = '' # Store gherkin prefix from input to add \
60  # later after search is performed
61  if IS_MAC:
62  self.OSXDisableAllSmartSubstitutions()
63 
64  def set_row(self, row):
65  self._row_row = row
66 
67  def OnKeyDown(self, event):
68  # TODO: This might benefit from some cleanup
69  keycode, control_down = event.GetKeyCode(), event.CmdDown()
70  # Ctrl-Space handling needed for dialogs # DEBUG add Ctrl-m
71  if (control_down or event.AltDown()) and keycode in [wx.WXK_SPACE, ord('m')]:
72  self.show_content_assistshow_content_assist()
73  elif keycode == wx.WXK_RETURN and self._popup_popup.is_shown():
74  self.OnFocusLostOnFocusLost(event)
75  elif keycode == wx.WXK_TAB:
76  self.OnFocusLostOnFocusLost(event, False)
77  elif keycode == wx.WXK_ESCAPE and self._popup_popup.is_shown():
78  self._popup_popup.hide()
79  elif keycode in [wx.WXK_UP, wx.WXK_DOWN, wx.WXK_PAGEUP, wx.WXK_PAGEDOWN] \
80  and self._popup_popup.is_shown():
81  self._popup_popup.select_and_scroll(keycode)
82  elif keycode in (ord('1'), ord('2'), ord('5')) and event.ControlDown() and not \
83  event.AltDown():
84  self.execute_variable_creatorexecute_variable_creator(list_variable=(keycode == ord('2')),
85  dict_variable=(keycode == ord('5')))
86  elif keycode == ord('3') and event.ControlDown() and event.ShiftDown() \
87  and not event.AltDown():
88  self.execute_add_text(add_text='# ', on_the_left=True, on_the_right=False)
89  elif keycode == ord('4') and event.ControlDown() and event.ShiftDown() \
90  and not event.AltDown():
91  self.execute_remove_text(remove_text='# ', on_the_left=True, on_the_right=False)
92  elif self._popup_popup.is_shown() and keycode < 256:
93  wx.CallAfter(self._populate_content_assist_populate_content_assist)
94  event.Skip()
95  else:
96  event.Skip()
97 
98  def OnChar(self, event):
99  keychar = event.GetUnicodeKey()
100  # print(f"DEBUG: OnChar at contentassist {chr(keychar)} {keychar}")
101  if keychar == wx.WXK_NONE:
102  event.Skip()
103  return
104  if keychar in [ord('['), ord('{'), ord('('), ord("'"), ord('\"'), ord('`')]:
105  # TODO fix recursion error in Linux
106  if platform.lower().startswith('linux'):
107  event.Skip()
108  else:
109  self.execute_enclose_textexecute_enclose_text(chr(keychar))
110  else:
111  event.Skip()
112 
113  def execute_variable_creator(self, list_variable=False, dict_variable=False):
114  from_, to_ = self.GetSelection()
115  if list_variable:
116  symbol = '@'
117  elif dict_variable:
118  symbol = '&'
119  else:
120  symbol = '$'
121  self.SetValue(self._variable_creator_value_variable_creator_value(
122  self.Value, symbol, from_, to_))
123  if from_ == to_:
124  self.SetInsertionPoint(from_ + 2)
125  else:
126  self.SetInsertionPoint(to_ + 3)
127  self.SetSelection(from_ + 2, to_ + 2)
128 
129  def _variable_creator_value(self, value, symbol, from_, to_):
130  return value[:from_]+symbol+'{'+value[from_:to_]+'}'+value[to_:]
131 
132  def execute_enclose_text(self, keycode):
133  # TODO move this code to kweditor and fix when in cell editor in Linux
134  from_, to_ = self.GetSelection()
135  self.SetValue(self._enclose_text_enclose_text(self.Value, keycode, from_, to_))
136  elem = self
137  if from_ == to_:
138  elem.SetInsertionPoint(from_ + 1)
139  else:
140  elem.SetInsertionPoint(to_ + 2)
141  elem.SetSelection(from_ + 1, to_ + 1)
142 
143  def _enclose_text(self, value, open_symbol, from_, to_):
144  if open_symbol == '[':
145  close_symbol = ']'
146  elif open_symbol == '{':
147  close_symbol = '}'
148  elif open_symbol == '(':
149  close_symbol = ')'
150  else:
151  close_symbol = open_symbol
152  return value[:from_]+open_symbol+value[from_:to_]+close_symbol+value[to_:]
153 
155  # Todo: Will only comment the left top cell for a multi cell select block!
156  from_, to_ = self.GetSelection()
157  #print(f"DEBUG: value from/to: " + str(from_) + " " + str(to_))
158  #print(f"DEBUG: value selected: " + self.Value)
159  add_text = '# '
160  self.SetValue(self._add_text_add_text(self.Value, add_text, True, False, from_, to_))
161  lenadd = len(add_text)
162  elem = self
163  elem.SetInsertionPoint(from_ + lenadd)
164  if from_ != to_:
165  elem.SetInsertionPoint(to_ + lenadd)
166  elem.SetSelection(from_ + lenadd, to_ + lenadd)
167  return
168 
169  @staticmethod
170  def _add_text(value, add_text, on_the_left, on_the_right, from_, to_):
171  if on_the_left and on_the_right:
172  return value[:from_]+add_text+value[from_:to_]+add_text+value[to_:]
173  if on_the_left:
174  return value[:from_]+add_text+value[from_:to_]+value[to_:]
175  if on_the_right:
176  return value[:from_]+value[from_:to_]+add_text+value[to_:]
177  return value
178 
180  # Todo: Will only uncomment the left top cell for a multi cell select block!
181  from_, to_ = self.GetSelection()
182  lenold = len(self.Value)
183  self.SetValue(self._remove_text_remove_text(self.Value, '# ', True, False, from_, to_))
184  lenone = len(self.Value)
185  diffone = lenold - lenone
186  elem = self
187  if from_ == to_:
188  elem.SetInsertionPoint(from_ - diffone)
189  else:
190  elem.SetInsertionPoint(to_ - diffone)
191  elem.SetSelection(from_ - diffone, to_ - diffone)
192 
193  def _remove_text(self, value, remove_text, on_the_left, on_the_right, from_, to_):
194  if on_the_left and on_the_right:
195  return value[:from_]+value[from_:to_].strip(remove_text)+remove_text+value[to_:]
196  if on_the_left:
197  return value[:from_]+value[from_:to_].lstrip(remove_text)+value[to_:]
198  if on_the_right:
199  return value[:from_]+value[from_:to_].rstrip(remove_text)+value[to_:]
200  return value
201 
202  def OnFocusLost(self, event, set_value=True):
203  event.Skip()
204  if not self._popup_popup.is_shown():
205  return
206  if self.gherkin_prefixgherkin_prefix:
207  value = self.gherkin_prefixgherkin_prefix + self._popup_popup.get_value() or self.GetValue()
208  else:
209  value = self._popup_popup.get_value() or self.GetValue()
210  if set_value and value:
211  self.SetValue(value)
212  self.SetInsertionPoint(len(value)) # DEBUG was self.Value
213  else:
214  self.Clear()
215  self.hidehide()
216 
217  def OnDestroy(self, event):
218  # all pushed eventHandlers need to be popped before close
219  # the last event handler is window object itself - do not pop itself
220  while self.GetEventHandler() is not self:
221  self.PopEventHandler()
222 
223  def reset(self):
224  self._popup_popup.reset()
225  self._showing_content_assist_showing_content_assist = False
226 
228  if self._showing_content_assist_showing_content_assist:
229  return
230  self._showing_content_assist_showing_content_assist = True
231  if self._populate_content_assist_populate_content_assist():
232  self._show_content_assist_show_content_assist()
233 
235  value = self.GetValue()
236  (self.gherkin_prefixgherkin_prefix, value) = self._remove_bdd_prefix_remove_bdd_prefix(value)
237  return self._popup_popup.content_assist_for(value, row=self._row_row)
238 
239  def _remove_bdd_prefix(self, name):
240  for match in ['given ', 'when ', 'then ', 'and ', 'but ']:
241  if name.lower().startswith(match):
242  return name[:len(match)], name[len(match):]
243  return '', name
244 
246  _, height = self.GetSize()
247  x, y = self.ClientToScreen((0, 0))
248  self._popup_popup.show(x, y, height)
249 
251  suggestion = self._popup_popup.content_assist_value(self.Value)
252  if suggestion is None:
253  return suggestion
254  else:
255  return self.gherkin_prefixgherkin_prefix + suggestion
256 
257  def hide(self):
258  self._popup_popup.hide()
259  self._showing_content_assist_showing_content_assist = False
260 
261 
263  ExpandoTextCtrl):
264 
265  def __init__(self, parent, plugin, controller):
266  ExpandoTextCtrl.__init__(self, parent, size=wx.DefaultSize,
267  style=wx.WANTS_CHARS|wx.TE_NOHIDESEL)
268  _ContentAssistTextCtrlBase.__init__(self, SuggestionSource(plugin, controller))
269  self.SetBackgroundColour(context.POPUP_BACKGROUND)
270  # self.SetOwnBackgroundColour(Colour(200, 222, 40))
271  self.SetForegroundColour(context.POPUP_FOREGROUND)
272  # self.SetOwnForegroundColour(Colour(7, 0, 70))
273 
274 
276 
277  def __init__(self, parent, suggestion_source, size=wx.DefaultSize):
278  wx.TextCtrl.__init__(self, parent, size=size, style=wx.WANTS_CHARS|wx.TE_NOHIDESEL)
279  _ContentAssistTextCtrlBase.__init__(self, suggestion_source)
280  self.SetBackgroundColour(Colour(self.color_background_helpcolor_background_help))
281  self.SetOwnBackgroundColour(Colour(self.color_background_helpcolor_background_help))
282  self.SetForegroundColour(Colour(self.color_foreground_textcolor_foreground_text))
283  self.SetOwnForegroundColour(Colour(self.color_foreground_textcolor_foreground_text))
284 
285 
287 
288  def __init__(self, parent, suggestion_source, pos, size=wx.DefaultSize):
289  wx.TextCtrl.__init__(self, parent, -1, "", pos, size=size, style=wx.WANTS_CHARS|wx.BORDER_NONE|wx.WS_EX_TRANSIENT|wx.TE_PROCESS_ENTER|wx.TE_NOHIDESEL)
290  _ContentAssistTextCtrlBase.__init__(self, suggestion_source)
291  self.SetBackgroundColour(Colour(self.color_background_helpcolor_background_help))
292  self.SetOwnBackgroundColour(Colour(self.color_background_helpcolor_background_help))
293  self.SetForegroundColour(Colour(self.color_foreground_textcolor_foreground_text))
294  self.SetOwnForegroundColour(Colour(self.color_foreground_textcolor_foreground_text))
295  """
296  self.SetBackgroundColour(Colour(200, 222, 40))
297  self.SetOwnBackgroundColour(Colour(200, 222, 40))
298  self.SetForegroundColour(Colour(7, 0, 70))
299  self.SetOwnForegroundColour(Colour(7, 0, 70))
300  """
301 
302 
304 
305  def __init__(self, parent, suggestion_source, label, controller,
306  size=wx.DefaultSize):
307  FileBrowseButton.__init__(self, parent, labelText=label,
308  size=size, fileMask="*",
309  changeCallback=self.OnFileChangedOnFileChanged)
310  self._parent_parent = parent
311  self._controller_controller = controller
312  self._browsed_browsed = False
313  _ContentAssistTextCtrlBase.__init__(self, suggestion_source)
314  self.SetBackgroundColour(Colour(context.POPUP_BACKGROUND))
315  self.SetOwnBackgroundColour(Colour(context.POPUP_BACKGROUND))
316  self.SetForegroundColour(Colour(context.POPUP_FOREGROUND))
317  self.SetOwnForegroundColour(Colour(context.POPUP_FOREGROUND))
318 
319  def Bind(self, *args):
320  self.textControl.Bind(*args)
321 
322  def __getattr__(self, item):
323  return getattr(self.textControl, item)
324 
325  def OnBrowse(self, evt=None):
326  self._browsed_browsed = True
327  FileBrowseButton.OnBrowse(self, evt)
328  self._browsed_browsed = False
329 
330  def OnDestroy(self, event):
331  # all pushed eventHandlers need to be popped before close
332  # the last event handler is window object itself - do not pop itself
333  try:
334  while self.GetEventHandler() is not self:
335  self.PopEventHandler()
336  except RuntimeError:
337  pass
338 
339  def OnFileChanged(self, evt):
340  if self._browsed_browsed:
341  self._browsed_browsed = False
342  self.SetValue(self._relative_path_relative_path(self.GetValue()))
343  self._parent_parent.setFocusToOK()
344 
345  def _relative_path(self, value):
346  src = self._controller_controller.datafile.source
347  if utils.is_same_drive(src, value):
348  path = relpath(value, src if isdir(src) else dirname(src))
349  else:
350  path = value
351  return path.replace('\\', '/') if context.IS_WINDOWS else\
352  path.replace('\\', '\\\\')
353 
354 
355 class Suggestions():
356 
357  def __init__(self, suggestion_source):
358  self._suggestion_source_suggestion_source = suggestion_source
359  self._previous_value_previous_value = None
360  self._previous_choices_previous_choices = []
361 
362  def get_for(self, value, row=None):
363  self._previous_choices_previous_choices = self._get_choices_get_choices(value, row)
364  self._previous_value_previous_value = value
365  return [k for k, _ in self._previous_choices_previous_choices]
366 
367  def get_item(self, name):
368  for k, v in self._previous_choices_previous_choices:
369  if k == name:
370  return v
371  raise Exception('Item not in choices "%s"' % (name))
372 
373  def _get_choices(self, value, row):
374  if self._previous_value_previous_value and value.startswith(self._previous_value_previous_value):
375  return [(key, val) for key, val in self._previous_choices_previous_choices
376  if utils.normalize(key).startswith(utils.normalize(value))]
377  choices = self._suggestion_source_suggestion_source.get_suggestions(value, row)
378  duplicate_names = self._get_duplicate_names_get_duplicate_names(choices)
379  return self._format_choices_format_choices(choices, value, duplicate_names)
380 
381  def _get_duplicate_names(self, choices):
382  results = set()
383  normalized_names = [utils.normalize(ch.name) for ch in choices]
384  for choice in choices:
385  normalized = utils.normalize(choice.name)
386  if normalized_names.count(normalized) > 1:
387  results.add(normalized)
388  return results
389 
390  def _format_choices(self, choices, prefix, duplicate_names):
391  return [(self._format_format(val, prefix, duplicate_names), val) for val in
392  choices]
393 
394  def _format(self, choice, prefix, duplicate_names):
395  return choice.name if self._matches_unique_shortname_matches_unique_shortname(
396  choice, prefix, duplicate_names) else choice.longname
397 
398  def _matches_unique_shortname(self, choice, prefix, duplicate_names):
399  if isinstance(choice, VariableInfo):
400  return True
401  if not utils.normalize(choice.name).startswith(
402  utils.normalize(prefix)):
403  return False
404  if utils.normalize(choice.name) in duplicate_names:
405  return False
406  return True
407 
408 
410 
411  def __init__(self, parent, suggestion_source):
412  self._parent_parent = parent
413  self._main_popup_main_popup = RidePopupWindow(parent, _PREFERRED_POPUP_SIZE)
414  self._details_popup_details_popup = HtmlPopupWindow(parent, _PREFERRED_POPUP_SIZE)
415  self._selection_selection = -1
416  self._list_list = ContentAssistList(self._main_popup_main_popup,
417  self.OnListItemSelectedOnListItemSelected,
418  self.OnListItemActivatedOnListItemActivated)
419  self._suggestions_suggestions = Suggestions(suggestion_source)
420 
421  def reset(self):
422  self._selection_selection = -1
423 
424  def get_value(self):
425  return self._selection_selection != -1 and self._list_list.get_text(
426  self._selection_selection) or None
427 
428  def content_assist_for(self, value, row=None):
429  self._choices_choices = self._suggestions_suggestions.get_for(value, row=row)
430  if not self._choices_choices:
431  self._list_list.ClearAll()
432  self._parent_parent.hide()
433  return False
434  self._list_list.populate(self._choices_choices)
435  return True
436 
437  def _starts(self, val1, val2):
438  return val1.lower().startswith(val2.lower())
439 
440  def content_assist_value(self, value):
441  if self._selection_selection > -1:
442  return self._list_list.GetItem(self._selection_selection).GetText()
443  return None
444 
445  def show(self, xcoord, ycoord, cell_height):
446  self._main_popup_main_popup.SetPosition((xcoord,
447  self._move_y_where_room_move_y_where_room(ycoord,
448  cell_height)))
449  self._details_popup_details_popup.SetPosition((self._move_x_where_room_move_x_where_room(xcoord),
450  self._move_y_where_room_move_y_where_room(ycoord,
451  cell_height)))
452  self._main_popup_main_popup.Show()
453  self._list_list.SetFocus()
454 
455  def _move_x_where_room(self, start_x):
456  width = _PREFERRED_POPUP_SIZE[0]
457  max_horizontal = wx.GetDisplaySize()[0]
458  free_right = max_horizontal - start_x - width
459  free_left = start_x - width
460  if max_horizontal - start_x < 2 * width:
461  if free_left > free_right:
462  return start_x - width
463  return start_x + width
464 
465  def _move_y_where_room(self, start_y, cell_height):
466  height = _PREFERRED_POPUP_SIZE[1]
467  max_vertical = wx.GetDisplaySize()[1]
468  if max_vertical - start_y - cell_height < height:
469  return start_y - height
470  return start_y + cell_height
471 
472  def is_shown(self):
473  return self._main_popup_main_popup.IsShown()
474 
475  def select_and_scroll(self, keycode):
476  sel = self._list_list.GetFirstSelected()
477  if keycode == wx.WXK_DOWN:
478  if sel < (self._list_list.GetItemCount() - 1):
479  self._select_and_scroll_select_and_scroll(sel + 1)
480  else:
481  self._select_and_scroll_select_and_scroll(0)
482  elif keycode == wx.WXK_UP:
483  if sel > 0:
484  self._select_and_scroll_select_and_scroll(sel - 1)
485  else:
486  self._select_and_scroll_select_and_scroll(self._list_list.GetItemCount() - 1)
487  elif keycode == wx.WXK_PAGEDOWN:
488  if self._list_list.ItemCount - self._selection_selection > 14:
489  self._select_and_scroll_select_and_scroll(self._selection_selection + 14)
490  else:
491  self._select_and_scroll_select_and_scroll(self._list_list.ItemCount - 1)
492  elif keycode == wx.WXK_PAGEUP:
493  if self._selection_selection > 14:
494  self._select_and_scroll_select_and_scroll(self._selection_selection - 14)
495  else:
496  self._select_and_scroll_select_and_scroll(0)
497 
498  def _select_and_scroll(self, selection):
499  self._selection_selection = selection
500  self._list_list.Select(self._selection_selection)
501  self._list_list.EnsureVisible(self._selection_selection)
502  value = self.get_valueget_value()
503  if value:
504  self._parent_parent.SetValue(value)
505 
506  def hide(self):
507  self._selection_selection = -1
508  self._main_popup_main_popup.Show(False)
509  self._details_popup_details_popup.Show(False)
510 
511  def OnListItemActivated(self, event):
512  self._parent_parent.OnFocusLost(event)
513 
514  def OnListItemSelected(self, event):
515  self._selection_selection = event.GetIndex()
516  item = self._suggestions_suggestions.get_item(event.GetText())
517  if item.details:
518  self._details_popup_details_popup.Show()
519  self._details_popup_details_popup.set_content(item.details, item.name)
520  elif self._details_popup_details_popup.IsShown():
521  self._details_popup_details_popup.Show(False)
522 
523 
524 class ContentAssistList(wx.ListCtrl):
525 
526  def __init__(self, parent, selection_callback, activation_callback=None):
527  from ..preferences import RideSettings
528 
531  _settings = RideSettings()
532  self.general_settingsgeneral_settings = _settings['General']
533  self.color_background_helpcolor_background_help = self.general_settingsgeneral_settings['background help']
534  self.color_foreground_textcolor_foreground_text = self.general_settingsgeneral_settings['foreground text']
535  style = wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_NO_HEADER
536  wx.ListCtrl.__init__(self, parent, style=style)
537  self._selection_callback_selection_callback = selection_callback
538  self._activation_callback_activation_callback = activation_callback
539  self.SetSize(parent.GetSize())
540  self.SetBackgroundColour(self.color_background_helpcolor_background_help)
541  self.SetForegroundColour(self.color_foreground_textcolor_foreground_text)
542  self.Bind(wx.EVT_LIST_ITEM_SELECTED, selection_callback)
543  self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, activation_callback)
544 
545  def populate(self, data):
546  self.ClearAll()
547  self.InsertColumn(0, '', width=self.Size[0])
548  for row, item in enumerate(data):
549  self.InsertItem(row, item)
550  self.Select(0)
551 
552  def get_text(self, index):
553  return self.GetItem(index).GetText()
def __init__(self, parent, suggestion_source, label, controller, size=wx.DefaultSize)
def __init__(self, parent, selection_callback, activation_callback=None)
def __init__(self, parent, suggestion_source)
def show(self, xcoord, ycoord, cell_height)
def _move_y_where_room(self, start_y, cell_height)
def __init__(self, parent, suggestion_source, size=wx.DefaultSize)
def __init__(self, parent, suggestion_source, pos, size=wx.DefaultSize)
def __init__(self, suggestion_source)
def _format(self, choice, prefix, duplicate_names)
def _format_choices(self, choices, prefix, duplicate_names)
def _matches_unique_shortname(self, choice, prefix, duplicate_names)
def get_for(self, value, row=None)
def execute_variable_creator(self, list_variable=False, dict_variable=False)
def _add_text(value, add_text, on_the_left, on_the_right, from_, to_)
def _remove_text(self, value, remove_text, on_the_left, on_the_right, from_, to_)
def _variable_creator_value(self, value, symbol, from_, to_)
def _enclose_text(self, value, open_symbol, from_, to_)