Coverage for src/robotide/editor/texteditor.py: 59%
2350 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-06 10:40 +0100
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-06 10:40 +0100
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.
15import builtins
16import os
17import re
18import tempfile
19from io import StringIO, BytesIO
20from os.path import dirname
21from time import time
23import wx
24from wx import stc, Colour
25from wx.adv import HyperlinkCtrl, EVT_HYPERLINK
26from multiprocessing import shared_memory
27from .popupwindow import HtmlPopupWindow
28from .pythoneditor import PythonSTC
29from . import _EDIT_nt, get_menudata
30from .. import robotapi
31from ..context import IS_WINDOWS, IS_MAC
32from ..controller.ctrlcommands import SetDataFile, INDENTED_START
33from ..controller.dataloader import TestDataDirectoryWithExcludes
34from ..controller.filecontrollers import ResourceFileController
35from ..controller.macrocontrollers import WithStepsController
36from ..namespace.suggesters import SuggestionSource
37from ..pluginapi import Plugin, action_info_collection, TreeAwarePluginMixin
38from ..publish.messages import (RideSaved, RideTreeSelection, RideNotebookTabChanging, RideDataChanged, RideOpenSuite,
39 RideDataChangedToDirty, RideBeforeSaving, RideSaving, RideDataDirtyCleared)
40from ..preferences import TextEditorPreferences, PreferenceEditor
41from ..preferences.editors import read_fonts
42from ..publish import RideSettingsChanged, PUBLISHER
43from ..publish.messages import RideMessage
44from ..widgets import SearchField, Label, HtmlDialog
45from ..widgets import VerticalSizer, HorizontalSizer, ButtonWithHandler, RIDEDialog
47from robotide.lib.compat.parsing.language import Language
48robotframeworklexer = None
49if Language: 49 ↛ 55line 49 didn't jump to line 55 because the condition on line 49 was always true
50 try: # import our modified version
51 from robotide.lib.compat.pygments import robotframework as robotframeworklexer
52 except ImportError:
53 robotframeworklexer = None
55if not robotframeworklexer: 55 ↛ 56line 55 didn't jump to line 56 because the condition on line 55 was never true
56 try: # import original version
57 from pygments.lexers import robotframework as robotframeworklexer
58 except ImportError:
59 robotframeworklexer = None
61_ = wx.GetTranslation # To keep linter/code analyser happy
62builtins.__dict__['_'] = wx.GetTranslation
64AUTO_SUGGESTIONS = 'enable auto suggestions'
65LANG_SETTING = 'Language: '
66PATH_EXCLUSIONS = dirname(__file__)
67PLUGIN_NAME = 'Text Edit'
68TOKEN_TXT = 'Token('
69TXT_NUM_SPACES = 'txt number of spaces'
70ZOOM_FACTOR = 'zoom factor'
71RSPC = r"\s{2}"
74def read_language(content):
75 from tempfile import mkstemp 1rcbjefdgh
76 from ..lib.compat.parsing.language import read as lread 1rcbjefdgh
78 fp, fname = mkstemp() 1rcbjefdgh
80 with open(fp, mode='wb') as fp: 1rcbjefdgh
81 fp.write(content) 1rcbjefdgh
82 fp.close() 1rcbjefdgh
83 with open(fname, mode='rb') as readfp: 1rcbjefdgh
84 lang = lread(readfp) 1rcbjefdgh
85 os.remove(fname) 1rcbjefdgh
86 return lang 1rcbjefdgh
89def obtain_language(existing, content):
90 try: 1rcbjefdgh
91 set_lang = shared_memory.ShareableList(name="language") 1rcbjefdgh
92 except Exception as e: # AttributeError: # Unittests fails here
93 print(f"DEBUG: texteditor.py obtain_language EXCEPTION: {e}")
94 set_lang = []
95 doc_lang = read_language(content) 1rcbjefdgh
96 adoc_lang = [] 1rcbjefdgh
97 if doc_lang is not None: 97 ↛ 101line 97 didn't jump to line 101 because the condition on line 97 was always true1rcbjefdgh
98 if isinstance(doc_lang, str): 98 ↛ 100line 98 didn't jump to line 100 because the condition on line 98 was always true1rcbjefdgh
99 adoc_lang.append(doc_lang) 1rcbjefdgh
100 set_lang = _get_lang(set_lang, adoc_lang) 1rcbjefdgh
101 elif len(set_lang) > 0:
102 if existing is not None:
103 if isinstance(existing, list):
104 lang = existing[0]
105 else:
106 lang = existing
107 try:
108 mlang = Language.from_name(lang.replace('_', '-'))
109 set_lang[0] = get_rf_lang_code(mlang.code) # .replace('-', '_')
110 except ValueError:
111 set_lang[0] = 'en'
112 else:
113 set_lang[0] = 'en'
114 return [set_lang[0]] 1rcbjefdgh
117def _get_lang(set_lang:list, adoc_lang: list) -> list:
118 for idx, lang in enumerate(adoc_lang): 1rcbjefdgh
119 try: 1rcbjefdgh
120 mlang = Language.from_name(lang.replace('_', '-').strip()) 1rcbjefdgh
121 except ValueError as e: 1rcb
122 print(f"DEBUG: TextEditor, could not find Language:{lang}") 1rcb
123 raise e 1rcb
124 set_lang[idx] = get_rf_lang_code(mlang.code) # .code.replace('-','_') 1rcbjefdgh
125 return set_lang 1rcbjefdgh
128def get_rf_lang_code(lang: (str, list), iso: bool=False) -> str:
129 if isinstance(lang, list): 1Frcbjefdgh
130 clean_lang = lang 1F
131 else:
132 clean_lang = lang.split(' ') # The cases we have two words 1Frcbjefdgh
133 clean_lang = clean_lang[0].replace('-', '_').split('_') # The cases we have variant code 1Frcbjefdgh
134 lc = len(clean_lang) 1Frcbjefdgh
135 code = clean_lang[0].lower() 1Frcbjefdgh
136 if not iso: 1Frcbjefdgh
137 if lc == 1: 1Frcbjefdgh
138 return code.title() 1Frcbjefdgh
139 elif lc == 2: 139 ↛ 143line 139 didn't jump to line 143 because the condition on line 139 was always true1Frcd
140 with_variant_code = f"{code.title()}{clean_lang[1].lower().title()}" 1Frcd
141 if with_variant_code in ("PtBr", "ZhCn", "ZhTw") and not iso: 141 ↛ 143line 141 didn't jump to line 143 because the condition on line 141 was always true1Frcd
142 return with_variant_code 1Frcd
143 if iso: 143 ↛ 145line 143 didn't jump to line 145 because the condition on line 143 was always true1befdgh
144 return _four_letters_code(clean_lang) 1befdgh
145 return code.title()
148def _four_letters_code(clean_lang: list) -> str:
149 variant = {"bs": "BA", "cs": "CZ", "da": "DK", "en": "US", "hi": "IN", "ja": "JP", 1befdgh
150 "ko": "KR", "sv": "SE", "uk": "UA", "vi": "VN"}
151 lc = len(clean_lang) 1befdgh
152 code = clean_lang[0].lower() 1befdgh
153 if lc == 1: 1befdgh
154 if code in variant.keys(): 1befdgh
155 return f"{code}_{variant[code].upper()}" 1d
156 else:
157 return f"{code}_{code.upper()}" 1befdgh
158 else:
159 return f"{code}_{clean_lang[1].upper()}" 1d
162def _get_lang_classes(old_lang: str, new_lang: str) -> (Language, Language):
163 try: 1bjefdgh
164 old_lang_class = Language.from_name(old_lang) 1bjefdgh
165 except ValueError as ex: 1bj
166 print(ex) 1bj
167 old_lang_class = Language.from_name('English') 1bj
168 try: 1bjefdgh
169 new_lang_class = Language.from_name(new_lang) 1bjefdgh
170 except ValueError as ex: 1j
171 print(ex) 1j
172 new_lang_class = Language.from_name('English') 1j
173 return old_lang_class, new_lang_class 1bjefdgh
176def _check_lang_error(node_info: tuple, m_text) -> (bool, str):
177 signal_correct_language = False 1befdgh
178 if node_info != ('', ) and node_info[0] == 'ERROR': 1befdgh
179 c_msg = node_info[1].replace(TOKEN_TXT, '').replace(')', '').split(',') 1b
180 line = c_msg[1].replace('\'', '').strip() 1b
181 # print(f"DEBUG: textedit.py transform_doc_language ERROR:{line}")
182 if line.startswith(LANG_SETTING): 182 ↛ 187line 182 didn't jump to line 187 because the condition on line 182 was always true1b
183 tail = line.replace(LANG_SETTING, '') 1b
184 # print(f"DEBUG: textedit.py transform_doc_language INSIDE BLOCK {tail=}")
185 m_text = m_text.replace(LANG_SETTING + tail, LANG_SETTING + 'English' + ' # ' + tail) 1b
186 signal_correct_language = True 1b
187 return signal_correct_language, m_text 1befdgh
190def _final_lang_transformation(signal_correct_language: bool, old_lang_name: str, new_lang_name: str, m_text: str) -> str:
191 if signal_correct_language: 1befdgh
192 m_text = m_text.replace(fr'{LANG_SETTING}English', fr'{LANG_SETTING}{new_lang_name}') 1b
193 else:
194 m_text = m_text.replace(fr'{LANG_SETTING}{old_lang_name}', fr'{LANG_SETTING}{new_lang_name}') 1efdgh
195 try: 1befdgh
196 set_lang = shared_memory.ShareableList(name="language") 1befdgh
197 except AttributeError: # Unittests fails here
198 set_lang = []
199 try: 1befdgh
200 mlang = Language.from_name(new_lang_name.replace('_', '-')) 1befdgh
201 set_lang[0] = get_rf_lang_code(mlang.code) 1befdgh
202 except ValueError:
203 set_lang[0] = 'en'
204 return m_text 1befdgh
206def transform_doc_language(old_lang, new_lang, m_text, node_info: tuple = ('', )):
207 if isinstance(old_lang, list): 1bjefdgh
208 old_lang = old_lang[0] 1jefdgh
209 if isinstance(new_lang, list): 1bjefdgh
210 new_lang = new_lang[0] 1jefdgh
211 old_lang = old_lang.title() 1bjefdgh
212 new_lang = new_lang.title() 1bjefdgh
213 if old_lang == new_lang: 1bjefdgh
214 return m_text 1jd
215 old_lang_class, new_lang_class = _get_lang_classes(old_lang, new_lang) 1bjefdgh
216 old_lang_name = old_lang_class.name 1bjefdgh
217 new_lang_name = new_lang_class.name 1bjefdgh
218 if old_lang_name == new_lang_name: 1bjefdgh
219 return m_text 1j
220 old_lang_headers = old_lang_class.headers 1befdgh
221 new_lang_headers = new_lang_class.headers 1befdgh
222 old_library_setting = old_lang_class.library_setting 1befdgh
223 old_resource_setting = old_lang_class.resource_setting 1befdgh
224 old_variables_setting = old_lang_class.variables_setting 1befdgh
225 old_name_setting = old_lang_class.name_setting 1befdgh
226 old_documentation_setting = old_lang_class.documentation_setting 1befdgh
227 old_metadata_setting = old_lang_class.metadata_setting 1befdgh
228 old_suite_setup_setting = old_lang_class.suite_setup_setting 1befdgh
229 old_suite_teardown_setting = old_lang_class.suite_teardown_setting 1befdgh
230 old_test_setup_setting = old_lang_class.test_setup_setting 1befdgh
231 old_task_setup_setting = old_lang_class.task_setup_setting 1befdgh
232 old_test_teardown_setting = old_lang_class.test_teardown_setting 1befdgh
233 old_task_teardown_setting = old_lang_class.task_teardown_setting 1befdgh
234 old_test_template_setting = old_lang_class.test_template_setting 1befdgh
235 old_task_template_setting = old_lang_class.task_template_setting 1befdgh
236 old_test_timeout_setting = old_lang_class.test_timeout_setting 1befdgh
237 old_task_timeout_setting = old_lang_class.task_timeout_setting 1befdgh
238 old_test_tags_setting = old_lang_class.test_tags_setting 1befdgh
239 old_task_tags_setting = old_lang_class.task_tags_setting 1befdgh
240 old_keyword_tags_setting = old_lang_class.keyword_tags_setting 1befdgh
241 old_tags_setting = old_lang_class.tags_setting 1befdgh
242 old_setup_setting = old_lang_class.setup_setting 1befdgh
243 old_teardown_setting = old_lang_class.teardown_setting 1befdgh
244 old_template_setting = old_lang_class.template_setting 1befdgh
245 old_timeout_setting = old_lang_class.timeout_setting 1befdgh
246 old_arguments_setting = old_lang_class.arguments_setting 1befdgh
247 new_library_setting = new_lang_class.library_setting 1befdgh
248 new_resource_setting = new_lang_class.resource_setting 1befdgh
249 new_variables_setting = new_lang_class.variables_setting 1befdgh
250 new_name_setting = new_lang_class.name_setting 1befdgh
251 new_documentation_setting = new_lang_class.documentation_setting 1befdgh
252 new_metadata_setting = new_lang_class.metadata_setting 1befdgh
253 new_suite_setup_setting = new_lang_class.suite_setup_setting 1befdgh
254 new_suite_teardown_setting = new_lang_class.suite_teardown_setting 1befdgh
255 new_test_setup_setting = new_lang_class.test_setup_setting 1befdgh
256 new_task_setup_setting = new_lang_class.task_setup_setting 1befdgh
257 new_test_teardown_setting = new_lang_class.test_teardown_setting 1befdgh
258 new_task_teardown_setting = new_lang_class.task_teardown_setting 1befdgh
259 new_test_template_setting = new_lang_class.test_template_setting 1befdgh
260 new_task_template_setting = new_lang_class.task_template_setting 1befdgh
261 new_test_timeout_setting = new_lang_class.test_timeout_setting 1befdgh
262 new_task_timeout_setting = new_lang_class.task_timeout_setting 1befdgh
263 new_test_tags_setting = new_lang_class.test_tags_setting 1befdgh
264 new_task_tags_setting = new_lang_class.task_tags_setting 1befdgh
265 new_keyword_tags_setting = new_lang_class.keyword_tags_setting 1befdgh
266 new_tags_setting = new_lang_class.tags_setting 1befdgh
267 new_setup_setting = new_lang_class.setup_setting 1befdgh
268 new_teardown_setting = new_lang_class.teardown_setting 1befdgh
269 new_template_setting = new_lang_class.template_setting 1befdgh
270 new_timeout_setting = new_lang_class.timeout_setting 1befdgh
271 new_arguments_setting = new_lang_class.arguments_setting 1befdgh
272 old_lang_given_prefixes = old_lang_class.given_prefixes 1befdgh
273 old_lang_when_prefixes = old_lang_class.when_prefixes 1befdgh
274 old_lang_then_prefixes = old_lang_class.then_prefixes 1befdgh
275 old_lang_and_prefixes = old_lang_class.and_prefixes 1befdgh
276 old_lang_but_prefixes = old_lang_class.but_prefixes 1befdgh
277 new_lang_given_prefixes = new_lang_class.given_prefixes 1befdgh
278 new_lang_when_prefixes = new_lang_class.when_prefixes 1befdgh
279 new_lang_then_prefixes = new_lang_class.then_prefixes 1befdgh
280 new_lang_and_prefixes = new_lang_class.and_prefixes 1befdgh
281 new_lang_but_prefixes = new_lang_class.but_prefixes 1befdgh
282 old_true_strings = old_lang_class.true_strings 1befdgh
283 old_false_strings = old_lang_class.false_strings 1befdgh
284 new_true_strings = new_lang_class.true_strings 1befdgh
285 new_false_strings = new_lang_class.false_strings 1befdgh
286 # If error in Language, do final replacement
287 signal_correct_language, m_text = _check_lang_error(node_info, m_text) 1befdgh
288 for old, new in zip(old_lang_headers.keys(), new_lang_headers.keys()): 1befdgh
289 m_text = re.sub(r"[*]+\s"+fr"{old}"+r"\s[*]+", fr"*** {new} ***", m_text) 1befdgh
290 # Settings must be replaced individually
291 # Order of replacements seems to be important
292 m_text = re.sub(fr'^{old_library_setting}\b', fr'{new_library_setting}', m_text, flags=re.M) 1befdgh
293 m_text = re.sub(fr'^{old_resource_setting}\b', fr'{new_resource_setting}', m_text, flags=re.M) 1befdgh
294 m_text = re.sub(fr'^{old_variables_setting}\b', fr'{new_variables_setting}', m_text, flags=re.M) 1befdgh
295 m_text = re.sub(fr'^{old_documentation_setting}\b', fr'{new_documentation_setting}', m_text, flags=re.M) 1befdgh
296 m_text = re.sub(fr'\[{old_documentation_setting}]', fr'[{new_documentation_setting}]', m_text) 1befdgh
297 m_text = re.sub(fr'\[{old_arguments_setting}]', fr'[{new_arguments_setting}]', m_text) 1befdgh
298 m_text = re.sub(fr'^{old_test_timeout_setting}\b', fr'{new_test_timeout_setting}', m_text, flags=re.M) 1befdgh
299 m_text = re.sub(fr'^{old_task_timeout_setting}\b', fr'{new_task_timeout_setting}', m_text, flags=re.M) 1befdgh
300 m_text = re.sub(fr'^{old_suite_setup_setting}\b', fr'{new_suite_setup_setting}', m_text, flags=re.M) 1befdgh
301 m_text = re.sub(fr'^{old_suite_teardown_setting}\b', fr'{new_suite_teardown_setting}', m_text, flags=re.M) 1befdgh
302 m_text = re.sub(fr'^{old_test_setup_setting}\b', fr'{new_test_setup_setting}', m_text, flags=re.M) 1befdgh
303 m_text = re.sub(fr'^{old_task_setup_setting}\b', fr'{new_task_setup_setting}', m_text, flags=re.M) 1befdgh
304 m_text = re.sub(fr'\[{old_template_setting}]', fr'[{new_template_setting}]', m_text) 1befdgh
305 m_text = re.sub(fr'^{old_test_teardown_setting}\b', fr'{new_test_teardown_setting}', m_text, flags=re.M) 1befdgh
306 m_text = re.sub(fr'^{old_task_teardown_setting}\b', fr'{new_task_teardown_setting}', m_text, flags=re.M) 1befdgh
307 m_text = re.sub(fr'\[{old_tags_setting}]', fr'[{new_tags_setting}]', m_text) 1befdgh
308 m_text = re.sub(fr'\[{old_setup_setting}]', fr'[{new_setup_setting}]', m_text) 1befdgh
309 m_text = re.sub(fr'\[{old_teardown_setting}]', fr'[{new_teardown_setting}]', m_text) 1befdgh
310 m_text = re.sub(fr'^{old_metadata_setting}\b', fr'{new_metadata_setting}', m_text, flags=re.M) 1befdgh
311 m_text = re.sub(fr'^{old_test_template_setting}\b', fr'{new_test_template_setting}', m_text, flags=re.M) 1befdgh
312 m_text = re.sub(fr'^{old_task_template_setting}\b', fr'{new_task_template_setting}', m_text, flags=re.M) 1befdgh
313 m_text = re.sub(fr'\[{old_keyword_tags_setting}]', fr'[{new_keyword_tags_setting}]', m_text) 1befdgh
314 m_text = re.sub(fr'\[{old_timeout_setting}]', fr'[{new_timeout_setting}]', m_text) 1befdgh
315 m_text = re.sub(fr'^{old_test_tags_setting}\b', fr'{new_test_tags_setting}', m_text, flags=re.M) 1befdgh
316 m_text = re.sub(fr'^{old_task_tags_setting}\b', fr'{new_task_tags_setting}', m_text, flags=re.M) 1befdgh
318 # Only the False/True words will be replaced not positionally or bound by []
319 for old, new in zip(old_true_strings, new_true_strings): 1befdgh
320 m_text = re.sub(fr"\b{old}\b", fr"{new}", m_text) 1befdgh
321 for old, new in zip(old_false_strings, new_false_strings): 1befdgh
322 m_text = re.sub(fr"\b{old}\b", fr"{new}", m_text) 1befdgh
323 # At least in Portuguese No Operation would change to Não Operation,
324 # But Name would change to Nãome when should be Nome, so we do after false strings
325 m_text = re.sub(fr'^{old_name_setting}\b', fr'{new_name_setting}', m_text, flags=re.M) 1befdgh
327 for old, new in zip(old_lang_given_prefixes, new_lang_given_prefixes): 1befdgh
328 m_text = re.sub(RSPC+fr"{old}"+r"\s", fr" {new} ", m_text) 1befdgh
329 for old, new in zip(old_lang_when_prefixes, new_lang_when_prefixes): 1befdgh
330 m_text = re.sub(RSPC+fr"{old}"+r"\s", fr" {new} ", m_text) 1befdgh
331 for old, new in zip(old_lang_then_prefixes, new_lang_then_prefixes): 1befdgh
332 m_text = re.sub(RSPC+fr"{old}"+r"\s", fr" {new} ", m_text) 1befdgh
333 for old, new in zip(old_lang_and_prefixes, new_lang_and_prefixes): 1befdgh
334 m_text = re.sub(RSPC+fr"{old}"+r"\s", fr" {new} ", m_text) 1befdgh
335 for old, new in zip(old_lang_but_prefixes, new_lang_but_prefixes): 1befdgh
336 m_text = re.sub(RSPC+fr"{old}"+r"\s", fr" {new} ", m_text) 1befdgh
338 # Before ending, we replace broken keywords from excluded known bad tanslations
339 m_text = transform_standard_keywords(new_lang_name, m_text) 1befdgh
340 return _final_lang_transformation(signal_correct_language, old_lang_name, new_lang_name, m_text) 1befdgh
343def transform_standard_keywords(new_lang: str, content: str) -> str:
344 """
345 This function must be called after proper setting of parameters old_lang, new_lang. From transform_doc_language.
346 It reads the corresponding new_lang exclusion file and does the replacing.
347 :param new_lang: Name of the language to correct
348 :param content: Content to apply recovery of keywords
349 :return: Corrected test suite content with English keywords from Standard libraries
350 """
351 try: 1befdgh
352 mlang = Language.from_name(new_lang.replace('_', '-')) 1befdgh
353 lang_code = get_rf_lang_code(mlang.code, iso=True) 1befdgh
354 except ValueError:
355 return content
357 path_to_exclusion = f"{PATH_EXCLUSIONS}/../localization/{lang_code}/restore_keywords.json" 1befdgh
358 print(f"DEBUG: texteditor.py transform_standard_keywords path={path_to_exclusion}\n" 1befdgh
359 f"{lang_code=}\n"
360 f"{mlang.code=}")
361 import json 1befdgh
363 try: 1befdgh
364 with open(path_to_exclusion) as file: 1befdgh
365 data = json.load(file) 1befdgh
366 except FileNotFoundError: 1d
367 return content 1d
369 fix_list = data['fix_list'] 1befdgh
370 if len(fix_list) == 0: 370 ↛ 371line 370 didn't jump to line 371 because the condition on line 370 was never true1befdgh
371 return content
372 # print(f"DEBUG: texteditor.py transform_standard_keywords my_variable={fix_list}")
373 for kw in fix_list: 1befdgh
374 # print(f"DEBUG: texteditor.py transform_standard_keywords kws BAD={kw[1]} GOOD={kw[0]}")
375 content = re.sub(fr'\b{kw[1]}\b', fr'{kw[0]}', content) 1befdgh
376 return content 1befdgh
379class TextEditorPlugin(Plugin, TreeAwarePluginMixin):
380 title = PLUGIN_NAME
382 def __init__(self, application):
383 self.title = _('Text Edit')
384 Plugin.__init__(self, application)
385 self._editor_component = None
386 self._tab = None
387 self._doc_language = None
388 self._save_flag = 0
389 self.jump = True
390 # self.status_bar = self.frame.FindWindowByName("StatusBar", self.frame)
391 # self.frame.SetStatusBarPane(1)
392 self.reformat = application.settings.get('reformat', False)
393 self.settings = application.settings
394 self.application = application
395 self._register_shortcuts()
397 @property
398 def _editor(self):
399 if self._editor_component is None: 1akDcE
400 self._editor_component = SourceEditor(self, self.notebook,
401 self.title,
402 DataValidationHandler(self, lang=self._doc_language))
403 self._refresh_timer = wx.Timer(self._editor_component)
404 self._editor_component.Bind(wx.EVT_TIMER, self._on_timer)
405 return self._editor_component 1akDcE
407 def enable(self):
408 self._tab = self._editor 1aE
409 _menudata = get_menudata() 1aE
410 self.register_actions(action_info_collection(_menudata, self._tab, data_nt=_EDIT_nt, container=self._tab)) 1aE
411 self.subscribe(self.on_tree_selection, RideTreeSelection) 1aE
412 self.subscribe(self.on_data_changed, RideMessage) 1aE
413 self.subscribe(self.on_tab_change, RideNotebookTabChanging) 1aE
414 self.add_self_as_tree_aware_plugin() 1aE
415 if self._editor.is_focused(): 415 ↛ exitline 415 didn't return from function 'enable' because the condition on line 415 was always true1aE
416 self._register_shortcuts() 1aE
417 self._open() 1aE
419 def _register_shortcuts(self):
420 def focused(func): 1aE
421 def f(event): 1aE
422 if self.is_focused() and self._editor.is_focused():
423 func(event)
425 return f 1aE
427 if IS_MAC: # Mac needs this key binding 427 ↛ 431line 427 didn't jump to line 431 because the condition on line 427 was always true1aE
428 self.register_shortcut('CtrlCmd-A', focused(lambda e: self._editor.select_all())) 1aE
429 # No system needs this key binding, because is already global
430 # self.register_shortcut('CtrlCmd-V', focused(lambda e: self._editor.paste()))
431 self.register_shortcut('CtrlCmd-F', focused(lambda e: self._editor.search_field.SetFocus())) 1aE
432 # To avoid double actions these moved to on_key_down
433 # self.register_shortcut('CtrlCmd-G', focused(lambda e: self._editor.on_find(e)))
434 # self.register_shortcut('CtrlCmd-Shift-G', focused(lambda e: self._editor.on_find_backwards(e)))
435 self.register_shortcut('Ctrl-Space', lambda e: focused(self._editor.on_content_assist(e))) 1aE
436 self.register_shortcut('CtrlCmd-Space', lambda e: focused(self._editor.on_content_assist(e))) 1aE
437 self.register_shortcut('Alt-Space', lambda e: focused(self._editor.on_content_assist(e))) 1aE
439 def disable(self):
440 self.remove_self_from_tree_aware_plugins()
441 self.unsubscribe_all()
442 self.unregister_actions()
443 self.delete_tab(self._editor)
444 wx.CallLater(500, self.unregister_actions())
445 self._tab = None
446 self._editor_component = None
448 def on_open(self, event):
449 __ = event
450 self._open()
452 def _open(self):
453 # print(f"DEBUG: texteditor.py TextEditorPlugin _open ENTER curpos={self._editor._position}")
454 datafile_controller = self.tree.get_selected_datafile_controller() 1aDrcbjefdghE
455 if datafile_controller: 455 ↛ 456line 455 didn't jump to line 456 because the condition on line 455 was never true1aDrcbjefdghE
456 self._save_flag = 0
457 if hasattr(datafile_controller, 'language'):
458 if datafile_controller.language is not None:
459 self._set_shared_doc_lang(datafile_controller.language)
460 # print(f"DEBUG: texteditor _open SET FROM CONTROLLER language={self._doc_language}")
461 else:
462 self._set_shared_doc_lang('en')
463 self._editor.language = self._doc_language
464 # print(f"DEBUG: texteditor _open language={self._doc_language}")
465 self._open_data_for_controller(datafile_controller)
466 self._editor.store_position()
467 wx.CallLater(2000, self.statusbar_message, f'{_("Source: ")}{self._editor.datafile_controller.source}', 2000)
469 def _get_shared_doc_lang(self):
470 try:
471 set_lang = shared_memory.ShareableList(name="language")
472 except AttributeError: # Unittests fails here
473 set_lang = []
474 if set_lang is not None:
475 self._doc_language = set_lang[0]
476 else:
477 self._doc_language = 'en'
478 return self._doc_language
480 def _set_shared_doc_lang(self, lang='en'):
481 # print(f"DEBUG: textedit.py TextEditorPlugin _set_shared_doc_lang ENTER"
482 # f" params: {lang=}")
483 if isinstance(lang, list):
484 lang = lang[0]
485 # Shared memory to store language definition
486 try:
487 set_lang = shared_memory.ShareableList([lang], name="language")
488 except FileExistsError: # Other instance created file
489 set_lang = shared_memory.ShareableList(name="language")
490 except AttributeError: # Unittests fails here
491 set_lang = []
492 self._doc_language = set_lang[0] = lang
494 def _check_message(self, message: RideMessage) -> None:
495 if isinstance(message, RideOpenSuite): # Not reached 495 ↛ 496line 495 didn't jump to line 496 because the condition on line 495 was never true
496 self._editor.reset()
497 self._editor.set_editor_caret_position()
498 if isinstance(message, RideNotebookTabChanging): # Not reached 498 ↛ 499line 498 didn't jump to line 499 because the condition on line 498 was never true
499 return
500 # Workaround for remarked dirty with Ctrl-S
501 if self.is_focused() and self._save_flag == 0 and isinstance(message, RideSaving): 501 ↛ 502line 501 didn't jump to line 502 because the condition on line 501 was never true
502 self._save_flag = 1
503 RideBeforeSaving().publish()
504 if self.is_focused() and self._save_flag == 1 and isinstance(message, RideDataDirtyCleared): 504 ↛ 505line 504 didn't jump to line 505 because the condition on line 504 was never true
505 self._save_flag = 2
506 if self.is_focused() and self._save_flag == 2 and isinstance(message, RideSaved): 506 ↛ 507line 506 didn't jump to line 507 because the condition on line 506 was never true
507 self._save_flag = 3
508 print(f"DEBUG: textedit.py TextEditorPlugin _check_message call undirty, {message}")
509 # wx.CallAfter(self._editor.mark_file_dirty, False)
510 self._editor.mark_file_dirty(False)
511 self.tree._data_undirty(message)
512 # DEBUG: This is the unwanted chnge after saving but excluded in this block for performance
513 # if self.is_focused() and self._save_flag == 3 and isinstance(message, RideDataChangedToDirty):
514 # self._save_flag = 4
515 # wx.CallAfter(self._editor.mark_file_dirty, False)
516 if isinstance(message, RideBeforeSaving): 516 ↛ 519line 516 didn't jump to line 519 because the condition on line 516 was never true
517 # self._editor.is_saving = False
518 # Reset counter for Workaround for remarked dirty with Ctrl-S
519 self._save_flag = 0
520 self._apply_txt_changes_to_model()
522 def on_data_changed(self, message):
523 """ This block is now inside try/except to avoid errors from unit test """
524 try: 1akDcE
525 if self.is_focused() and isinstance(message, RideDataChangedToDirty): 525 ↛ 527line 525 didn't jump to line 527 because the condition on line 525 was never true1akDcE
526 # print(f"DEBUG: textedit OnDataChanged returning RideDataChangedToDirty {self._save_flag=}")
527 return
528 if self._should_process_data_changed_message(message): 1akDcE
529 # print(f"DEBUG: textedit after _should_process_data_changed_message save_flag={self._save_flag}")
530 self._check_message(message)
531 self._refresh_timer.Start(500, True)
532 # For performance reasons only run after all the data changes
533 except AttributeError:
534 pass
536 def _on_timer(self, event):
537 self._editor.store_position()
538 self._open_tree_selection_in_editor()
539 event.Skip()
541 @staticmethod
542 def _should_process_data_changed_message(message):
543 return isinstance(message, (RideDataChanged, RideBeforeSaving, RideSaved, RideSaving, RideDataDirtyCleared)) 1akDcE
544 # and not isinstance(message, RideDataChangedToDirty))
546 def on_tree_selection(self, message):
547 if not self.is_focused(): 547 ↛ 548line 547 didn't jump to line 548 because the condition on line 547 was never true1ak
548 return
549 # print(f"DEBUG: texteditor.py TextEditorPlugin on_tree_selection ENTER {message=} type={type(message.item)}")
550 self._editor.store_position() 1ak
551 # self.jump = True
552 if self.is_focused(): 552 ↛ 574line 552 didn't jump to line 574 because the condition on line 552 was always true1ak
553 next_datafile_controller = message.item and message.item.datafile_controller 1ak
554 if self._editor.datafile_controller == message.item.datafile_controller == next_datafile_controller: 554 ↛ 557line 554 didn't jump to line 557 because the condition on line 554 was never true1ak
555 # print(f"DEBUG: OnTreeSelection Same FILE item type={type(message.item)}\n"
556 # f"value of {self._save_flag=}")
557 self._editor.locate_tree_item(message.item)
558 self.jump = True
559 return
560 if self._editor.dirty and not self._apply_txt_changes_to_model(): 560 ↛ 561line 560 didn't jump to line 561 because the condition on line 560 was never true
561 if self._editor.datafile_controller != next_datafile_controller:
562 self.tree.select_controller_node(self._editor.datafile_controller)
563 self._editor.set_editor_caret_position()
564 return
565 if next_datafile_controller: 565 ↛ 571line 565 didn't jump to line 571 because the condition on line 565 was always true
566 self.jump = True
567 self._open_data_for_controller(next_datafile_controller)
568 # print(f"DEBUG: OnTreeSelection OTHER FILE item type={type(message.item)}\n"
569 # f"value of {self._save_flag=}")
570 wx.CallAfter(self._editor.locate_tree_item, message.item)
571 self._set_read_only(message)
572 self._editor.set_editor_caret_position()
573 else:
574 self._editor.GetFocus(None)
576 def _set_read_only(self, message):
577 if not isinstance(message, bool): 1aD
578 self._editor.source_editor.readonly = not message.item.datafile_controller.is_modifiable()
579 self._editor.source_editor.SetReadOnly(self._editor.source_editor.readonly) 1aD
580 self._editor.source_editor.stylizer.set_styles(self._editor.source_editor.readonly) 1aD
581 self._editor.source_editor.Update() 1aD
583 def _open_tree_selection_in_editor(self):
584 try:
585 datafile_controller = self.tree.get_selected_datafile_controller()
586 if datafile_controller: 586 ↛ 587line 586 didn't jump to line 587 because the condition on line 586 was never true
587 self._editor.language = datafile_controller.language
588 except AttributeError:
589 return
590 if datafile_controller: 590 ↛ 591line 590 didn't jump to line 591 because the condition on line 590 was never true
591 self._editor.language = datafile_controller.language
592 self.global_settings['doc language'] = datafile_controller.language
593 self._editor.open(DataFileWrapper(datafile_controller, self.global_settings, self._editor.language))
594 self._editor.source_editor.readonly = not datafile_controller.is_modifiable()
595 self._editor.set_editor_caret_position()
597 def _open_data_for_controller(self, datafile_controller):
598 self._editor.language = datafile_controller.language
599 self.global_settings['doc language'] = datafile_controller.language
600 self._editor.selected(DataFileWrapper(datafile_controller, self.global_settings, self._editor.language))
601 self._editor.source_editor.readonly = not datafile_controller.is_modifiable()
603 def on_tab_change(self, message):
604 if message.newtab == self.title: 1aD
605 self._open() 1D
606 self._editor.set_editor_caret_position() 1D
607 try: 1D
608 self._set_read_only(self._editor.source_editor.readonly) 1D
609 except Exception as e: # DEBUG: When using only Text Editor exists error in message topic
610 print(e)
611 # wx.CallAfter(self._editor.source_editor.on_style, None) # DEBUG Text Edit, styles were not applied
612 self._editor.source_editor.on_style(None) 1D
613 self._editor.Refresh() 1D
614 elif message.oldtab == self.title: 1aD
615 self._editor.remove_and_store_state() 1D
616 self._apply_txt_changes_to_model() 1D
618 def on_tab_changed(self, event):
619 __ = event
620 self._show_editor()
621 event.Skip()
623 def _apply_txt_changes_to_model(self):
624 if not self.is_focused() and not self._editor.dirty: 624 ↛ anywhereline 624 didn't jump anywhere: it always raised an exception.1D
625 return
626 # self._editor.is_saving = False
627 # self._editor.store_position()
628 # print(f"DEBUG: textedit.py _apply_txt_changes_to_model CALL content_save lang={self._doc_language}"
629 # f" curpos={self._editor._position}")
630 if not self._editor.content_save(lang=self._doc_language):
631 return False
632 self._editor.reset()
633 self._editor.set_editor_caret_position()
634 return True
636 def is_focused(self):
637 try: 1akDcEH
638 return self.notebook.current_page_title == self.title 1akDcEH
639 except AttributeError: 1akDc
640 return self._editor.is_focused() 1akDc
642 def on_config_panel(self):
643 dlg = self.config_panel(self.frame)
644 dlg.Show(True)
646 def config_panel(self, parent):
647 __ = parent
648 _parent = wx.GetTopLevelWindows()
649 dlg = PreferenceEditor(_parent[0], _("RIDE - Preferences"),
650 self.application.preferences, style='single', index=4)
651 dlg.Show(False)
652 return dlg
655class DummyController(WithStepsController):
656 _populator = robotapi.UserKeywordPopulator
657 filename = ""
659 def _init(self, data=None):
660 self.data = data
662 @staticmethod
663 def get_local_variables():
664 return {}
666 def __eq__(self, other):
667 if self is other:
668 return True
669 if other.__class__ != self.__class__:
670 return False
671 return self.data == other.data
673 def __hash__(self):
674 return hash(repr(self))
677class DataValidationHandler(object):
679 def __init__(self, plugin, lang=None):
680 self._plugin = plugin
681 self._last_answer = None
682 self._last_answer_time = 0
683 self._editor = None
684 if lang is not None:
685 self._doc_language = lang
686 else:
687 self._get_shared_doc_lang()
688 file = tempfile.NamedTemporaryFile(prefix="model_saved_from_RIDE_",
689 suffix=".robot", mode="w+", delete=False)
690 self.tempfilename=file.name
692 def _get_shared_doc_lang(self):
693 try:
694 set_lang = shared_memory.ShareableList(name="language")
695 except AttributeError: # Unittests fails here
696 set_lang = []
697 if set_lang is not None: 697 ↛ 700line 697 didn't jump to line 700 because the condition on line 697 was always true
698 self._doc_language = set_lang[0]
699 else:
700 self._doc_language = 'en'
701 return self._doc_language
703 def _set_shared_doc_lang(self, lang='en'):
704 # print(f"DEBUG: textedit.py _set_shared_doc_lang ENTER"
705 # f" params: {lang=}")
706 if isinstance(lang, list):
707 lang = lang[0]
708 # Shared memory to store language definition
709 try:
710 set_lang = shared_memory.ShareableList([lang], name="language")
711 except FileExistsError: # Other instance created file
712 set_lang = shared_memory.ShareableList(name="language")
713 except AttributeError: # Unittests fails here
714 set_lang = []
715 self._doc_language = set_lang[0] = lang
717 def set_editor(self, editor):
718 self._editor = editor
721 def validate_and_update(self, data, text, lang='en'):
723 try:
724 from robot.parsing.parser.parser import get_model # RF > 4.0
725 except ImportError:
726 return self._old_validate_and_update(data, text)
727 return self._new_validate_and_update(data, text, lang)
729 """
730 Backwards compatible code v1.7.4.2
731 """
733 def _old_validate_and_update(self, data, text):
734 m_text = text.decode("utf-8")
735 if not self._old_sanity_check(data, m_text):
736 handled = self._old_handle_sanity_check_failure()
737 if not handled:
738 return False
739 self._editor.reset()
740 # print("DEBUG: updating text") # %s" % (self._editor.GetCurrentPos()))
741 data.update_from(m_text)
742 # print("DEBUG: AFTER updating text")
743 self._editor.set_editor_caret_position()
744 # %s" % (self._editor.GetCurrentPos()))
745 return True
747 def _old_sanity_check(self, data, text):
748 formatted_text = data.format_text(text)
749 # print(f"DEBUG: texteditor old_sanity_check {formatted_text=}")
750 c = self._normalize(formatted_text)
751 e = self._normalize(text)
752 return len(c) == len(e)
754 @staticmethod
755 def _normalize(text):
756 for item in [' ', r'\t', r'\n', r'\r\n', '...', '*']:
757 if item in text:
758 # print("DEBUG: _normaliz item %s txt %s" % (item, text))
759 text = text.replace(item, '')
760 return text
762 def _old_handle_sanity_check_failure(self):
763 if self._last_answer == wx.ID_NO and \
764 time() - self._last_answer_time <= 0.2:
765 self._editor._mark_file_dirty()
766 return False
767 # TODO: use widgets.Dialog
768 id = wx.MessageDialog(self._editor,
769 'ERROR: Data sanity check failed!\n'
770 'Reset changes?',
771 'Can not apply changes from Txt Editor',
772 style=wx.YES | wx.NO).ShowModal()
773 self._last_answer = id
774 self._last_answer_time = time()
775 if id == wx.ID_YES:
776 self._editor._revert()
777 return True
778 else:
779 self._editor._mark_file_dirty()
780 return False
782 """
783 End Backwards compatible code v1.7.4.2
784 """
786 def _new_validate_and_update(self, data, text, lang='en'):
787 from robotide.lib.robot.errors import DataError
788 m_text = text.decode("utf-8")
789 # print(f"DEBUG: textedit.py validate_and_update ENTER"
790 # f" params: {lang=} doc_language={self._doc_language}")
791 initial_lang = lang # if lang is not None else self._doc_language self._doc_language or
792 if LANG_SETTING in m_text:
793 try:
794 self._doc_language = obtain_language(lang, text)
795 except ValueError:
796 self._doc_language = 'en'
797 # print(f"DEBUG: textedit.py validate_and_update Language in doc--> lang={self._doc_language}")
798 else:
799 self._doc_language = lang if lang is not None else 'en'
800 # print(f"DEBUG: textedit.py validate_and_update NO Language in doc--> arg lang={lang} "
801 # f"set to={self._doc_language}")
802 self._editor.language = self._doc_language
804 try:
805 result = self._sanity_check(data, m_text) # First check
806 # print(f"DEBUG: textedit.py validate_and_update Language after sanity_check result={result}")
807 except DataError as err:
808 result = (err.message, err.details)
810 # print(f"DEBUG: textedit.py validate_and_update Language after sanity_check result={result}\n"
811 # f" lang params: {initial_lang=}, {self._doc_language=}")
812 if isinstance(result, tuple):
813 m_text = transform_doc_language(initial_lang, self._doc_language, m_text, node_info=result)
814 __ = self._get_shared_doc_lang()
815 try:
816 result = self._sanity_check(data, m_text) # Check if language changed and is valid content
817 except DataError as err:
818 result = (err.message, err.details)
819 if isinstance(result, tuple):
820 handled = self._handle_sanity_check_failure(result)
821 if not handled:
822 return False
823 # Save language
824 self._set_shared_doc_lang(self._doc_language)
825 if self._editor.reformat:
826 data.update_from(data.format_text(m_text))
827 else:
828 # DEBUG: This is the area where we will implement to not reformat code
829 # print(f"DEBUG: TextEditor.py DataValidationHandler validate_and_update calling update NO REFORMAT"
830 # f" lang={self._doc_language} text={m_text}")
831 data.update_from(m_text)
832 # There is no way to update the model without reformatting
833 # DEBUG: this only updates the editor, but not the model, changes in Text Editor are not reflected
834 # in Grid or when saving
835 # self.source_editor.source_editor.set_text(m_text)
836 self._editor.set_editor_caret_position()
837 return True
839 def _sanity_check(self, data, text):
840 from robotide.lib.compat.parsing import ErrorReporter
841 from robot.parsing.parser.parser import get_model
842 from robotide.lib.robot.errors import DataError
843 result = None
844 rf_lang = get_rf_lang_code(self._doc_language)
845 # print(f"DEBUG: textedit.py _sanity_check data is type={type(data)} lang={self._doc_language},"
846 # f" transformed lang={rf_lang}")
847 from robot.api.parsing import get_tokens
848 for token in get_tokens(text):
849 # print(repr(token))
850 if token.type == token.ERROR:
851 # print("DEBUG: textedit.py _sanity_check TOKEN in ERROR")
852 result = 'ERROR', repr(token)
853 return result
854 if token.type == token.INVALID_HEADER:
855 # print("DEBUG: textedit.py _sanity_check TOKEN in INVALID_HEADER")
856 result = 'INVALID_HEADER', repr(token)
857 return result
859 try:
860 model = get_model(text, lang=rf_lang)
861 except AttributeError:
862 return "Failed validation by Robot Framework", "Please, check if Language setting is valid!"
863 validator = ErrorReporter()
864 try:
865 validator.visit(model)
866 except DataError as err:
867 result = (err.message, err.details)
868 model.save(self.tempfilename)
869 # print(f"DEBUG: textedit.py _sanity_check after calling validator {validator}\n"
870 # f"Save model in /tmp/model_saved_from_RIDE.robot"
871 # f" result={result}")
872 return True if not result else result
874 """ DEBUG
875 formatted_text = data.format_text(text)
876 c = self._normalize(formatted_text)
877 e = self._normalize(text)
878 return len(c) == len(e)
879 """
881 """ DEBUG: This is no longer used
882 @staticmethod
883 def _normalize(text):
884 for item in tuple(string.whitespace) + ('...', '*'):
885 if item in text:
886 text = text.replace(item, '')
887 return text
888 """
890 def _handle_sanity_check_failure(self, message):
891 if isinstance(message[1], str) and message[1].startswith(TOKEN_TXT):
892 c_msg = message[1].replace(TOKEN_TXT, '').replace(')', '').split(',')
893 message = [" ".join(c_msg[4:]), c_msg[2].strip()]
895 if self._last_answer == wx.ID_NO and time() - self._last_answer_time <= 0.2:
896 # self.source_editor._mark_file_dirty(True)
897 return False
898 dlg = RIDEDialog(title=_("Can not apply changes from Text Editor"),
899 message=f"{_('ERROR: Data sanity check failed!')}\n{_('Error at line')}"
900 f" {message[1]}:\n{message[0]}\n\n{_('Reset changes?')}",
901 style=wx.ICON_ERROR | wx.YES_NO)
902 dlg.InheritAttributes()
903 did = dlg.ShowModal()
904 self._last_answer = did
905 self._last_answer_time = time()
906 if did == wx.ID_YES:
907 self._editor.revert()
908 return True
909 # else:
910 # self.source_editor._mark_file_dirty()
911 return False
914class DataFileWrapper(object): # DEBUG: bad class name
916 def __init__(self, data, settings, language=None):
917 self.wrapper_data = data
918 self._settings = settings
919 self._tab_size = self._settings.get(TXT_NUM_SPACES, 2) if self._settings else 2
920 self._reformat = self._settings.get('reformat', False) if self._settings else False
921 if language is not None: 921 ↛ 922line 921 didn't jump to line 922 because the condition on line 921 was never true
922 self._doc_language = language
923 else:
924 self._doc_language = ['en']
926 def __eq__(self, other):
927 if other is None: 927 ↛ 929line 927 didn't jump to line 929 because the condition on line 927 was always true
928 return False
929 return self.wrapper_data == other.wrapper_data
931 def update_from(self, content):
932 self.wrapper_data.execute(SetDataFile(self._create_target_from(content)))
934 def _create_target_from(self, content):
935 src = BytesIO(content.encode("utf-8"))
936 target = self._create_target(content.encode("utf-8"))
937 FromStringIOPopulator(target, lang=self._doc_language).populate(src, self._tab_size)
938 return target
940 def format_text(self, text):
941 return self._txt_data(self._create_target_from(text))
943 def mark_data_dirty(self):
944 if not self.wrapper_data.is_dirty:
945 self.wrapper_data.mark_dirty()
947 def mark_data_pristine(self):
948 if self.wrapper_data.is_dirty: 948 ↛ 949line 948 didn't jump to line 949 because the condition on line 948 was never true1c
949 self.wrapper_data.unmark_dirty()
951 def _create_target(self, content=None):
952 data = self.wrapper_data.data
953 target_class = type(data)
954 self._doc_language = obtain_language(self._doc_language, content=content)
955 # print(f"DEBUG: textedit.py DataFileWrapper _create_target self._doc_language={self._doc_language}"
956 # f"\n target class={target_class}")
957 if isinstance(data, robotapi.TestDataDirectory):
958 target = robotapi.TestDataDirectory(parent=None, source=self.wrapper_data.directory,
959 settings=self._settings, language=self._doc_language)
960 target.initfile = data.initfile
961 return target
962 elif isinstance(data, TestDataDirectoryWithExcludes):
963 target = TestDataDirectoryWithExcludes(parent=None, source=self.wrapper_data.directory,
964 settings=self._settings, language=self._doc_language)
965 target.initfile = data.initfile
966 return target
967 return target_class(source=self.wrapper_data.source)
969 @property
970 def content(self):
971 return self._txt_data(self.wrapper_data.data)
973 def _txt_data(self, data):
974 output = StringIO()
975 data.save(output=output, fformat='txt', txt_separating_spaces=self._settings.get(TXT_NUM_SPACES, 4),
976 language=self._doc_language)
977 text = output.getvalue()
978 """ DEBUG: This is a good place to call Tidy
979 # if self._reformat:
980 # text = self.collapse_blanks(text) # This breaks formatting in Templated tests
981 # print(f"DEBUG: textedit.py DataFileWrapper content _txt_data = {text=} language={self._doc_language}")
982 """
983 return text
985 """ DEBUG: This is no longer used
986 def collapse_blanks(self, content: str) -> str:
987 spaces = self._tab_size * ' '
988 block = []
989 for ln in content.splitlines():
990 block.append(ln.replace(f'\\{spaces}', '').replace(f'\\\\ ', '').split(spaces))
991 # print(f"DEBUG: texteditor.py collapse_blanks block={block}\n")
992 new_text = ''
993 for ln in block:
994 blank_found = 0
995 for sl in ln:
996 if len(ln) == 1 and sl == '':
997 blank_found = 0
998 # sl = '\n'
999 elif len(ln) > 1 and sl == '':
1000 blank_found += 1
1001 elif sl != '':
1002 blank_found = 0
1003 if blank_found < 2:
1004 new_text += sl + spaces
1005 new_text = new_text.strip(' ') + '\n'
1006 # print(f"DEBUG: texteditor.py collapse_blanks new_text={new_text}")
1007 return new_text
1008 """
1011class SourceEditor(wx.Panel):
1013 def __init__(self, plugin, parent, title, data_validator):
1014 wx.Panel.__init__(self, parent)
1015 self.dlg = RIDEDialog()
1016 self.SetBackgroundColour(Colour(self.dlg.color_background))
1017 self.SetForegroundColour(Colour(self.dlg.color_foreground))
1018 self._syntax_colorization_help_exists = False
1019 self._data_validator = data_validator
1020 self._data_validator.set_editor(self)
1021 self.source_editor_parent = parent
1022 self.plugin = plugin
1023 self.datafile = None
1024 self._title = title
1025 self.tab_size = self.source_editor_parent.app.settings.get(TXT_NUM_SPACES, 4)
1026 self.reformat = self.source_editor_parent.app.settings.get('reformat', False)
1027 self._doc_language = None
1028 try:
1029 set_lang = shared_memory.ShareableList(name="language")
1030 except AttributeError: # Unittests fails here
1031 set_lang = []
1032 if not set_lang: 1032 ↛ 1033line 1032 didn't jump to line 1033 because the condition on line 1032 was never true
1033 set_lang[0] = ['en']
1034 self._doc_language = set_lang[0]
1035 self._create_ui(title)
1036 self._data = None
1037 self._position = 0 # Start at 0 if first time access
1038 self.restore_start_pos = self._position
1039 self.restore_end_pos = self._position
1040 self.restore_anchor = self._position
1041 self._showing_list = False
1042 self.autocomp_pos = None
1043 self._tab_open = self._title # When starting standalone this was not being set
1044 self._controller_for_context = None
1045 self._suggestions = None
1046 self.doc_size: int = 0 # Number of lines in document to be used in collecting words
1047 self._words_cache = set() # Actual cache of words to add to suggestions
1048 self._stored_text = None
1049 self._ctrl_action = None
1050 # self.is_saving = False # To avoid double calls to save
1051 self.old_information_popup = None
1052 PUBLISHER.subscribe(self.on_settings_changed, RideSettingsChanged)
1053 PUBLISHER.subscribe(self.on_tab_change, RideNotebookTabChanging)
1055 @property
1056 def general_font_size(self):
1057 fsize = self.source_editor_parent.app.settings.get('General', None)['font size']
1058 return fsize
1060 def is_focused(self):
1061 # DEBUG: original method: foc = wx.Window.FindFocus()
1062 # DEBUG: any(elem == foc for elem in [self]+list(self.GetChildren()))
1063 return self._tab_open == self._title 1aAikDcE
1065 def on_tab_change(self, message):
1066 self._tab_open = message.newtab 1aD
1068 def _create_ui(self, title):
1069 cnt = self.source_editor_parent.GetPageCount()
1070 if cnt >= 0: 1070 ↛ exitline 1070 didn't return from function '_create_ui' because the condition on line 1070 was always true
1071 editor_created = False
1072 while cnt > 0 and not editor_created:
1073 cnt -= 1
1074 editor_created = self.source_editor_parent.GetPageText(cnt) == self._title
1075 # DEBUG: Later we can adjust for several Text Editor tabs
1076 if not editor_created: 1076 ↛ exitline 1076 didn't return from function '_create_ui' because the condition on line 1076 was always true
1077 self.SetSizer(VerticalSizer())
1078 self._create_editor_toolbar()
1079 self._create_editor_text_control(language=self.language)
1080 self.source_editor_parent.add_tab(self, title, allow_closing=False)
1082 def _create_editor_toolbar(self):
1083 # needs extra container, since we might add helper
1084 # text about syntax colorization
1085 self.editor_toolbar = HorizontalSizer()
1086 default_components = HorizontalSizer()
1087 button = ButtonWithHandler(self, _('Apply Changes'), fsize=self.general_font_size,
1088 handler=lambda e: self.plugin._apply_txt_changes_to_model())
1089 button.SetBackgroundColour(Colour(self.dlg.color_secondary_background))
1090 button.SetForegroundColour(Colour(self.dlg.color_secondary_foreground))
1091 config_button = ButtonWithHandler(self, _('Settings'), bitmap='wrench.png', fsize=self.general_font_size,
1092 handler=lambda e: self.plugin.on_config_panel())
1093 config_button.SetBackgroundColour(Colour(self.dlg.color_background))
1094 config_button.SetOwnBackgroundColour(Colour(self.dlg.color_background))
1095 config_button.SetForegroundColour(Colour(self.dlg.color_foreground))
1096 default_components.add_with_padding(button)
1097 self._create_search(default_components)
1098 self.editor_toolbar.add_expanding(default_components)
1099 self.editor_toolbar.add_with_padding(config_button)
1100 self.Sizer.add_expanding(self.editor_toolbar, propotion=0)
1102 def _create_search(self, container_sizer):
1103 container_sizer.AddSpacer(5)
1104 size = wx.Size(200, 32)
1105 self.search_field = SearchField(self, '', size=size, process_enters=True)
1106 self.search_field.SetBackgroundColour(Colour(self.dlg.color_secondary_background))
1107 self.search_field.SetForegroundColour(Colour(self.dlg.color_secondary_foreground))
1108 self.search_field.Bind(wx.EVT_TEXT_ENTER, self.on_find)
1109 self.search_field.Bind(wx.EVT_SEARCHCTRL_SEARCH_BTN, self.on_find)
1110 self.search_field.SetHint(_('Search'))
1111 container_sizer.add_with_padding(self.search_field)
1112 button = ButtonWithHandler(self, _('Search'), fsize=self.general_font_size, handler=self.on_find)
1113 button.SetBackgroundColour(Colour(self.dlg.color_secondary_background))
1114 button.SetForegroundColour(Colour(self.dlg.color_secondary_foreground))
1115 container_sizer.add_with_padding(button)
1116 self._search_field_notification = Label(self, label='')
1117 container_sizer.add_with_padding(self._search_field_notification)
1119 def create_syntax_colorization_help(self):
1120 if self._syntax_colorization_help_exists:
1121 return
1122 label = Label(self, label=_("Syntax colorization disabled due to missing requirements."))
1123 link = HyperlinkCtrl(self, -1, label=_("Get help"), url="")
1124 link.Bind(EVT_HYPERLINK, self.show_help_dialog)
1125 flags = wx.ALIGN_RIGHT
1126 syntax_colorization_help_sizer = wx.BoxSizer(wx.VERTICAL)
1127 syntax_colorization_help_sizer.AddMany([
1128 (label, 0, flags),
1129 (link, 0, flags)
1130 ])
1131 self.editor_toolbar.add_expanding(syntax_colorization_help_sizer)
1132 self.Layout()
1133 self._syntax_colorization_help_exists = True
1135 @staticmethod
1136 def show_help_dialog(event):
1137 __ = event
1138 content = _("""<h1>Syntax colorization</h1>
1139 <p>
1140 Syntax colorization for Text Edit uses <a href='https://pygments.org/'>Pygments</a> syntax highlighter.
1141 </p>
1142 <p>
1143 Install Pygments from command line with:
1144 <pre>
1145 pip install pygments
1146 </pre>
1147 Or:
1148 <pre>
1149 easy_install pygments
1150 </pre>
1151 Then, restart RIDE.
1152 </p>
1153 <p>
1154 If you do not have pip or easy_install,
1155 <a
1156 href='https://pythonhosted.org/an_example_pypi_project/setuptools.html#installing-setuptools-and-easy-install'
1157 >follow these instructions</a>.
1158 </p>
1159 <p>
1160 For more information about installing Pygments, <a href='https://pygments.org/download/'>see the site</a>.
1161 </p>
1162 """)
1163 HtmlDialog(_("Getting syntax colorization"), content).Show()
1165 def store_position(self, force=False):
1166 if self.source_editor: # We don't necessarily need a data controller, was: "and self.datafile_controller:" 1166 ↛ exitline 1166 didn't return from function 'store_position' because the condition on line 1166 was always true1aACxzuyvwstiBkDc
1167 cur_pos = self.source_editor.GetCurrentPos() 1aACxzuyvwstiBkDc
1168 self.restore_start_pos = self.source_editor.GetSelectionStart() 1aACxzuyvwstiBkDc
1169 self.restore_end_pos = self.source_editor.GetSelectionEnd() 1aACxzuyvwstiBkDc
1170 self.restore_anchor = self.source_editor.GetAnchor() 1aACxzuyvwstiBkDc
1171 if cur_pos > 0: # Cheating because it always goes to zero 1aACxzuyvwstiBkDc
1172 self._position = cur_pos 1axzuyvwstkc
1173 if force: 1axzuyvwstkc
1174 self.source_editor.GotoPos(self._position) 1c
1176 def set_editor_caret_position(self):
1177 if not self.is_focused(): # DEBUG was typing text when at Grid Editor 1177 ↛ 1178line 1177 didn't jump to line 1178 because the condition on line 1177 was never true1aAiDc
1178 return
1179 position = self._position 1aAiDc
1180 self.source_editor.SetFocus() 1aAiDc
1181 # print(f"DEBUG: texteditor.py SourceEditor set_editor_caret_position position={position}")
1182 if position: 1aAiDc
1183 self.source_editor.SetCurrentPos(position) 1c
1184 self.source_editor.SetSelection(self.restore_start_pos, self.restore_end_pos) 1c
1185 self.source_editor.SetAnchor(self.restore_anchor) 1c
1186 self.source_editor.GotoPos(position) 1c
1187 self.source_editor.Refresh() 1c
1188 self.source_editor.Update() 1c
1190 @property
1191 def dirty(self):
1192 return self._data.wrapper_data.is_dirty if self._data else False 1aD
1194 @property
1195 def datafile_controller(self):
1196 return self._data.wrapper_data if self._data else None 1ak
1198 @property
1199 def language(self):
1200 return self._doc_language 1ac
1202 @language.setter
1203 def language(self, flanguage):
1204 self._doc_language = flanguage 1ac
1206 def on_find(self, event, forward=True):
1207 if self.source_editor:
1208 if event.GetEventType() == wx.wxEVT_SEARCH:
1209 text = event.GetString()
1210 elif event.GetEventType() != wx.wxEVT_TEXT_ENTER: # Was getting selected item from Tree
1211 text = self.source_editor.GetSelectedText()
1212 else:
1213 text = ''
1214 if (len(text) > 0 and text.lower() != self.search_field.GetValue().lower() and
1215 event.GetEventType() != wx.wxEVT_TOOL):
1216 # if a search string selected in text and CTRL+G is pressed
1217 # put the string into the search_field
1218 self.search_field.SelectAll()
1219 self.search_field.Clear()
1220 self.search_field.Update()
1221 self.search_field.SetValue(text)
1222 self.search_field.SelectAll()
1223 self.search_field.Update()
1224 if forward:
1225 # and set the start position to the beginning of the editor
1226 self.source_editor.SetAnchor(0)
1227 self.source_editor.SetCurrentPos(0)
1228 self.source_editor.Update()
1229 self._find(forward)
1231 def on_find_backwards(self, event):
1232 if self.source_editor:
1233 self.on_find(event, forward=False)
1235 def _find(self, forward=True):
1236 txt = self.search_field.GetValue().encode('utf-8')
1237 position = self._find_text_position(forward, txt)
1238 self._show_search_results(position, txt)
1240 # DEBUG: This must be cleaned up
1241 def _find_text_position(self, forward, txt):
1242 file_end = len(self.source_editor.utf8_text) 1A
1243 search_end = file_end if forward else 0 1A
1244 anchor = self.source_editor.GetAnchor() 1A
1245 anchor += 1 if forward else 0 1A
1246 position = self.source_editor.FindText(anchor, search_end, txt, 0) 1A
1247 if position == -1: 1247 ↛ 1248line 1247 didn't jump to line 1248 because the condition on line 1247 was never true1A
1248 start, end = (0, file_end) if forward else (file_end - 1, 0)
1249 position = self.source_editor.FindText(start, end, txt, 0)
1250 return position 1A
1252 def _show_search_results(self, position, txt):
1253 # if text is found start and end of the found text is returned,
1254 # but we do need just starting position which is the first value
1255 if type(position) is tuple: 1255 ↛ 1258line 1255 didn't jump to line 1258 because the condition on line 1255 was always true1A
1256 position = position[0] 1A
1258 if position != -1: 1258 ↛ 1264line 1258 didn't jump to line 1264 because the condition on line 1258 was always true1A
1259 self.source_editor.SetCurrentPos(position) 1A
1260 self.source_editor.SetSelection(position, position + len(txt)) 1A
1261 self.source_editor.ScrollToLine(self.source_editor.GetCurrentLine()) 1A
1262 self._search_field_notification.SetLabel('') 1A
1263 else:
1264 self._search_field_notification.SetLabel(_('No matches found.'))
1266 def locate_tree_item(self, item):
1267 """ item is object received from message """
1268 if not self.plugin.jump:
1269 self.plugin.jump = True
1270 return
1271 from wx.stc import STC_FIND_REGEXP
1272 search_end = len(self.source_editor.utf8_text)
1273 section_start = 0
1274 name_to_locate = r'^'+item.name+r'.*$'
1275 position = self.source_editor.FindText(section_start, search_end, name_to_locate, STC_FIND_REGEXP)
1276 # print(f"DEBUG: TextEditor locate_tree_item name_to_locate={name_to_locate} position={position}\n"
1277 # f"curpos={self._position} {self.is_saving=}")
1278 if position[0] != -1:
1279 # DEBUG: Make colours configurable?
1280 self.source_editor.SetSelBackground(True, Colour('orange'))
1281 self.source_editor.SetSelForeground(True, Colour('white'))
1282 self.source_editor.SetFocus()
1283 self.source_editor.GotoPos(position[1]+1)
1284 self.source_editor.LineScrollUp()
1285 self.source_editor.SetCurrentPos(position[1])
1286 self.source_editor.SetAnchor(position[0])
1287 self.source_editor.SetSelection(position[0], position[1])
1288 self.source_editor.SetFocusFromKbd()
1289 self.source_editor_parent.SetFocus()
1290 self.source_editor.Update()
1291 else: # Text was not found, so it is the Test Suite name. Go to line zero.
1292 self.source_editor.SetFocus()
1293 self.source_editor.GotoPos(1)
1294 self.source_editor.LineScrollUp()
1295 self.source_editor.SetCurrentPos(1)
1296 self.source_editor.SetAnchor(0)
1297 self.source_editor.SetSelection(0, self.source_editor.GetLineEndPosition(0))
1298 self.source_editor.SetFocusFromKbd()
1299 self.source_editor_parent.SetFocus()
1300 self.source_editor.Update()
1302 def words_cache(self, doc_size: int):
1303 if doc_size != self.doc_size: # DEBUG The initial idea was to not update words list if no changes in doc 1ac
1304 words_list = self.collect_words(self.source_editor.GetText()) 1ac
1305 self._words_cache.update(words_list) 1ac
1306 self.doc_size = doc_size 1ac
1307 return sorted(self._words_cache) 1ac
1309 @staticmethod
1310 def var_strip(txt:str):
1311 for symb in '$&@%{[()]}': 1ac
1312 txt = txt.strip(symb) 1ac
1313 return txt 1ac
1315 def collect_words(self, text: str):
1316 if not text: 1ac
1317 return ['']
1318 words = set() 1ac
1319 words_ = list(text.replace('\r\n', ' ').replace('\n', ' ').split(' ')) 1ac
1320 for w in words_: 1ac
1321 wl = self.var_strip(w) 1ac
1322 if wl and wl[0].isalpha(): 1ac
1323 words.add(w) 1ac
1325 # print(f"DEBUG: texteditor.py SourceEditor collect_words returning {words=}")
1326 return sorted(words) 1ac
1328 def on_content_assist(self, event):
1329 """
1330 Produces an Auto Complete Suggestions list, from Scintilla. Based on selected content or nearby text.
1331 Always add actual imported libraries and resources keywords and BuiltIn.
1332 :param event: Not used
1333 :return:
1334 """
1335 if not (self.is_focused() and self.plugin.is_focused()): # DEBUG was typing text when at Grid Editor 1335 ↛ 1336line 1335 didn't jump to line 1336 because the condition on line 1335 was never true1c
1336 return
1337 __ = event 1c
1338 """ 1c
1339 if self._showing_list:
1340 self._showing_list = False # Avoid double calls
1341 return
1342 """
1343 self.store_position() 1c
1344 selected = self.source_editor.get_selected_or_near_text() 1c
1345 # print(f"DEBUG: texteditor.py SourceEditor SELECTION selected = {selected} is type={type(selected)}")
1346 self.set_editor_caret_position() 1c
1347 # Next is for the unit tests when the did not used open to get data:
1348 if not self._suggestions: 1348 ↛ 1349line 1348 didn't jump to line 1349 because the condition on line 1348 was never true1c
1349 self._controller_for_context = DummyController(self._data.wrapper_data, self._data.wrapper_data)
1350 self._suggestions = SuggestionSource(self.plugin, self._controller_for_context)
1351 self._suggestions.update_from_local(self.words_cache(self.source_editor.GetLineCount()), self.language) 1c
1352 sugs = set() 1c
1353 if selected: 1353 ↛ 1368line 1353 didn't jump to line 1368 because the condition on line 1353 was always true1c
1354 selected = list(selected) 1c
1355 selected = ([selected[0], selected[-1].split(' ')[-1]] if selected[0] != selected[-1].split(' ')[-1] 1c
1356 else [selected[0]])
1357 for start in selected: 1c
1358 found = [] 1c
1359 for s in self._suggestions.get_suggestions(start): 1c
1360 if hasattr(s, 'name'): 1360 ↛ 1361line 1360 didn't jump to line 1361 because the condition on line 1360 was never true1c
1361 found.append(s.name)
1362 else:
1363 found.append(s) 1c
1364 sugs.update(found) 1c
1365 # print(f"DEBUG: texteditor.py SourceEditor on_content_assist FIRST SUGGESTION suggestions = {sugs}\n")
1366 # DEBUG: Here, if sugs is still [], then we can get all words from line and repeat suggestions
1367 # In another evolution, we can use database of words by frequency (considering future by project db)
1368 sel = [s for s in selected] if selected else [''] 1c
1369 entry_word = sel[0].split('.')[-1].strip() if '.' in sel[0] else sel[0] 1c
1370 length_entered = len(entry_word) # Because Libraries prefixed 1c
1371 # print(f"DEBUG: texteditor.py SourceEditor on_content_assist selection = {sel}")
1372 # if sel[0] == '':
1373 # The case when we call Ctl+Space in empty line o always add suggestions
1374 # for start in sel:
1375 # print(f"DEBUG: texteditor.py SourceEditor on_content_assist selection = {sel}")
1376 if sel[0] == '': 1376 ↛ 1377line 1376 didn't jump to line 1377 because the condition on line 1376 was never true1c
1377 found = []
1378 for s in self._suggestions.get_suggestions(''):
1379 if hasattr(s, 'name'):
1380 found.append(s.name)
1381 else:
1382 found.append(s)
1383 sugs.update(found)
1384 if len(sel[0]) >= 2: # Search again with and without variable prefixes 1384 ↛ 1405line 1384 didn't jump to line 1405 because the condition on line 1384 was always true1c
1385 found = [] 1c
1386 for start in sel: 1c
1387 if start[0] in "$&@%{[()]}": 1387 ↛ 1388line 1387 didn't jump to line 1388 because the condition on line 1387 was never true1c
1388 text = self.var_strip(start)
1389 for s in self._suggestions.get_suggestions(text):
1390 if hasattr(s, 'name'):
1391 found.append(s.name)
1392 else:
1393 found.append(s)
1394 else:
1395 for v in ['${', '&{', '@{', '$', '[', '(']: 1c
1396 text = f'{v}{start}' 1c
1397 for s in self._suggestions.get_suggestions(text): 1c
1398 if hasattr(s, 'name'): 1398 ↛ 1399line 1398 didn't jump to line 1399 because the condition on line 1398 was never true1c
1399 found.append(s.name)
1400 else:
1401 found.append(s) 1c
1402 sugs.update(found) 1c
1403 # print(f"DEBUG: texteditor.py SourceEditor on_content_assist VARIABLES SEARCH selection = {sel}\n"
1404 # f"sugs={sugs}")
1405 if len(sugs) > 0: 1405 ↛ 1409line 1405 didn't jump to line 1409 because the condition on line 1405 was always true1c
1406 # sugs = [s for s in sugs if s != '']
1407 if '' in sugs: 1407 ↛ 1408line 1407 didn't jump to line 1408 because the condition on line 1407 was never true1c
1408 sugs.remove('')
1409 suggestions=";".join(sorted(sugs)) 1c
1410 # print(f"DEBUG: texteditor.py SourceEditor on_content_assist BEFORE SHOW LIST suggestions = {suggestions}\n"
1411 # f" size={len(suggestions)} cache size={len(self._words_cache)}")
1412 if len(suggestions) > 0: # Consider using contentassist as in Grid Editor 1412 ↛ 1424line 1412 didn't jump to line 1424 because the condition on line 1412 was always true1c
1413 self.source_editor.AutoCompSetDropRestOfWord(False) 1c
1414 self.source_editor.AutoCompSetFillUps('=') 1c
1415 self.source_editor.AutoCompSetIgnoreCase(True) 1c
1416 self.source_editor.AutoCompSetOrder(0) 1c
1417 self.source_editor.AutoCompSetSeparator(ord(';')) 1c
1418 self.source_editor.AutoCompShow(length_entered, suggestions) 1c
1419 self.autocomp_pos = self.source_editor.AutoCompPosStart() 1c
1420 self._showing_list = True 1c
1421 # DEBUG: self.set_editor_caret_position()
1422 # Needs proper calculation of position to delete already suggestion.
1423 # This will be done when selection is effective at on_key_down
1424 """ 1c
1425 else:
1426 # self.set_editor_caret_position() # Restore selected text and caret
1427 self.source_editor.SetInsertionPoint(self._position) # We should know if list was canceled or value change
1428 """
1430 def open(self, data):
1431 self.reset() 1ac
1432 self._data = data 1ac
1433 if hasattr(self._data, '_doc_language') and self._data._doc_language is not None and len(self._data._doc_language) > 0: 1ac
1434 self.language = self._data._doc_language
1435 elif hasattr(self._data, '_language') and self._data._language is not None and len(self._data._language) > 0: 1435 ↛ 1436line 1435 didn't jump to line 1436 because the condition on line 1435 was never true1ac
1436 self.language = self._data._language
1437 else:
1438 self.language = ['en'] 1ac
1439 # print(f"DEBUG: texteditor.py SourceEditor open ENTER language={self.language} curpos={self._position}")
1440 try: 1ac
1441 if hasattr(self._data, 'wrapper_data'): 1ac
1442 if isinstance(self._data.wrapper_data, ResourceFileController): 1442 ↛ 1443line 1442 didn't jump to line 1443 because the condition on line 1442 was never true
1443 self._controller_for_context = DummyController(self._data.wrapper_data, self._data.wrapper_data)
1444 else:
1445 self._controller_for_context = self._data.wrapper_data.tests[0]
1446 elif isinstance(self._data, ResourceFileController): 1446 ↛ 1447line 1446 didn't jump to line 1447 because the condition on line 1446 was never true1ac
1447 self._controller_for_context = self._data
1448 else:
1449 self._controller_for_context = self._data.tests[0] 1ac
1450 self._suggestions = SuggestionSource(self.plugin, self._controller_for_context) 1ac
1451 except IndexError: # It is a new project, no content yet
1452 self._controller_for_context = DummyController(self._data.wrapper_data, self._data.wrapper_data)
1453 self._suggestions = SuggestionSource(self.plugin, self._controller_for_context)
1454 if hasattr(self.plugin, 'datafile') and self.plugin.datafile: 1454 ↛ 1455line 1454 didn't jump to line 1455 because the condition on line 1454 was never true1ac
1455 self.datafile = self.plugin.datafile
1456 # else:
1457 # print(f"DEBUG: Text Editor open NOT DATAFILE path={self.datafile_controller.source}")
1458 if not self.source_editor: 1458 ↛ 1459line 1458 didn't jump to line 1459 because the condition on line 1458 was never true1ac
1459 self._stored_text = self._data.content
1460 self._create_editor_text_control(text=self._stored_text, language=self.language)
1461 else:
1462 self.source_editor.set_language(self.language) 1ac
1463 if hasattr(self._data, 'content'): # Special case for unit test 1ac
1464 self.source_editor.set_text(self._data.content)
1465 self.set_editor_caret_position() 1ac
1466 wx.CallAfter(self.plugin.statusbar_message, f'{_("Source: ")}{self._controller_for_context.source}', 4000) 1ac
1467 self._words_cache.clear() 1ac
1468 self._suggestions.update_from_local(self.words_cache(self.source_editor.GetLineCount()), self.language) 1ac
1470 def selected(self, data):
1471 if not self.source_editor: 1471 ↛ 1472line 1471 didn't jump to line 1472 because the condition on line 1471 was never true
1472 self._create_editor_text_control(text=self._stored_text, language=self.language)
1473 else:
1474 self.source_editor.set_language(self.language)
1475 if self._data == data: 1475 ↛ 1476line 1475 didn't jump to line 1476 because the condition on line 1475 was never true
1476 return
1477 self.open(data)
1479 def _add_auto_indent(self, line: str):
1480 lenline = len(line)
1481 linenum = self.source_editor.GetCurrentLine()
1482 idx = 0
1483 while idx < lenline and line[idx] == ' ':
1484 idx += 1
1485 tsize = idx // self.tab_size
1486 block_line = line.strip().split(' ')[0]
1487 if idx < lenline and (block_line in INDENTED_START):
1488 tsize += 1
1489 elif linenum > 0 and tsize == 0: # Advance if first task/test case or keyword
1490 prevline = self.source_editor.GetLine(linenum - 1).lower()
1491 if prevline.startswith("**") and not ("variables" in prevline or "settings" in prevline):
1492 tsize = 1
1493 elif line.strip().startswith("END"):
1494 pos = self.source_editor.GetCurrentPos()
1495 self.source_editor.SetCurrentPos(pos)
1496 self.source_editor.SetSelection(pos, pos)
1497 self.source_editor.NewLine()
1498 while tsize > 0:
1499 self.write_ident()
1500 tsize -= 1
1502 def auto_indent(self):
1503 line, _ = self.source_editor.GetCurLine()
1504 lenline = len(line)
1505 if lenline > 0:
1506 self._add_auto_indent(line)
1507 else:
1508 self.source_editor.NewLine()
1509 pos = self.source_editor.GetCurrentLine()
1510 self.source_editor.SetCurrentPos(self.source_editor.GetLineEndPosition(pos))
1511 self.store_position()
1513 def deindent_block(self):
1514 start, end = self.source_editor.GetSelection()
1515 ini_line = self.source_editor.LineFromPosition(start)
1516 end_line = self.source_editor.LineFromPosition(end)
1517 self.source_editor.SelectNone()
1518 line = ini_line
1519 inconsistent = False
1520 self.source_editor.BeginUndoAction()
1521 while line <= end_line:
1522 inconsistent = False
1523 pos = self.source_editor.PositionFromLine(line)
1524 self.source_editor.SetCurrentPos(pos)
1525 self.source_editor.SetSelection(pos, pos)
1526 self.source_editor.SetInsertionPoint(pos)
1527 content = self.source_editor.GetRange(pos, pos + self.tab_size)
1528 if content == (' ' * self.tab_size):
1529 self.source_editor.DeleteRange(pos, self.tab_size)
1530 line += 1
1531 else:
1532 inconsistent = True
1533 break
1534 self.source_editor.EndUndoAction()
1535 if inconsistent:
1536 self.source_editor.Undo()
1537 return
1538 tnew_start = self.source_editor.GetLineEndPosition(ini_line) - len(self.source_editor.GetLine(ini_line)) + 1
1539 tnew_end = self.source_editor.GetLineEndPosition(end_line)
1540 self.source_editor.SetSelection(tnew_start, tnew_end)
1541 self.source_editor.SetCurrentPos(tnew_end)
1542 self.source_editor.SetAnchor(tnew_start)
1544 def _calc_indent_size(self, text: str):
1545 lenline = len(text)
1546 idx = 0
1547 block_line = text.strip().split(' ')[0]
1548 while idx < lenline and text[idx] == ' ':
1549 idx += 1
1550 tsize = idx // self.tab_size
1551 if idx < lenline and (block_line in INDENTED_START):
1552 tsize += 1
1553 elif tsize == 0:
1554 text = text.lower()
1555 if text.startswith("**"):
1556 if not ("variables" in text or "settings" in text):
1557 tsize = 1
1558 return tsize
1560 def deindent_line(self, line):
1561 self.indent_line(line, reverse=True)
1563 def indent_line(self, line, reverse=False):
1564 last_line = self.source_editor.GetLineCount() 1B
1565 if line > 0 and not reverse: 1565 ↛ 1566line 1565 didn't jump to line 1566 because the condition on line 1565 was never true1B
1566 pos = self.source_editor.PositionFromLine(line)
1567 text = self.source_editor.GetLine(line)
1568 lenline = len(text)
1569 if lenline > 0:
1570 self.source_editor.SetCurrentPos(pos)
1571 self.source_editor.SetSelection(pos, pos)
1572 self.source_editor.SetInsertionPoint(pos)
1573 self.source_editor.InsertText(pos, ' ' * self.tab_size)
1574 elif line < last_line and reverse: 1574 ↛ 1575line 1574 didn't jump to line 1575 because the condition on line 1574 was never true1B
1575 pos = self.source_editor.PositionFromLine(line)
1576 text = self.source_editor.GetLine(line)
1577 idx = self.first_non_space(text)
1578 if idx >= self.tab_size:
1579 self.source_editor.DeleteRange(pos, self.tab_size)
1581 def indent_block(self):
1582 # print(f"DEBUG: TextEditor SourceEdior ident_block focus={self.is_focused()}")
1583 start, end = self.source_editor.GetSelection()
1584 ini_line = self.source_editor.LineFromPosition(start)
1585 end_line = self.source_editor.LineFromPosition(end)
1586 self.source_editor.SelectNone()
1587 line = ini_line
1588 while line <= end_line:
1589 pos = self.source_editor.PositionFromLine(line)
1590 self.source_editor.SetCurrentPos(pos)
1591 self.source_editor.SetSelection(pos, pos)
1592 self.source_editor.SetInsertionPoint(pos)
1593 self.write_ident()
1594 # print(f"DEBUG: TextEditor SourceEdior ident_block loop line={line}")
1595 line += 1
1596 tnew_start = self.source_editor.GetLineEndPosition(ini_line) - len(self.source_editor.GetLine(ini_line)) + 1
1597 tnew_end = self.source_editor.GetLineEndPosition(end_line)
1598 self.source_editor.SetSelection(tnew_start, tnew_end)
1599 self.source_editor.SetCurrentPos(tnew_end)
1600 self.source_editor.SetAnchor(tnew_start)
1602 def write_ident(self):
1603 spaces = ' ' * self.tab_size
1604 self.source_editor.WriteText(spaces)
1606 def reset(self):
1607 if self._data and (hasattr(self._data, 'wrapper_data') and not self._data.wrapper_data.is_dirty): 1ac
1608 self.mark_file_dirty(False) 1c
1610 def content_save(self, **args):
1611 self.store_position()
1612 # print(f"DEBUG: TextEditor.py SourceEditor content_save curpos={self._position}")
1613 if self.dirty:
1614 # print(f"DEBUG: TextEditor.py SourceEditor content_save content={self.source_editor.utf8_text}\n"
1615 # f"self.language={self.language} data={self._data}"
1616 # f" calling validate_and_update with lang={args['lang']}")
1617 self.plugin.jump = False
1618 if not self._data_validator.validate_and_update(self._data, self.source_editor.utf8_text,
1619 lang=self.language): # args['lang']
1620 self.plugin.jump = True
1621 return False
1622 return True
1624 """
1625 # DEBUG:
1626 def direct_save(self, text):
1627 print(f"DEBUG: direct_save path={self.datafile_controller.source}")
1628 f = open(self.datafile_controller.source, "wb")
1629 try:
1630 f.write(text)
1631 self._mark_file_dirty(False)
1632 print(f"DEBUG: direct_save Content:\n{text}")
1633 except Exception as e:
1634 raise e
1635 finally:
1636 f.close()
1637 """
1638 # Callbacks taken from __init__.py
1639 def on_undo(self, event):
1640 __ = event
1641 self.undo()
1643 def on_redo(self, event):
1644 __ = event
1645 self.redo()
1647 def on_cut(self, event):
1648 __ = event
1649 self.cut()
1651 def on_copy(self, event):
1652 __ = event
1653 self.copy()
1655 def on_paste(self, event):
1656 __ = event
1657 self.paste()
1659 @staticmethod
1660 def on_insert(event):
1661 __ = event
1662 # print(f"DEBUG: TextEditor called on_insert event={event}\n TO BE IMPLEMENTED")
1663 # self.insert_row()
1665 @staticmethod
1666 def on_delete(self, event=None):
1667 """ Not used """
1669 def on_insert_cells(self, event):
1670 self.insert_cell(event)
1672 def on_delete_cells(self, event):
1673 self.delete_cell(event)
1675 def on_comment_rows(self, event):
1676 self.execute_comment(event)
1678 def on_uncomment_rows(self, event):
1679 self.execute_uncomment(event)
1681 def on_sharp_comment_rows(self, event):
1682 self.execute_sharp_comment(event)
1684 def on_sharp_uncomment_rows(self, event):
1685 self.execute_sharp_uncomment(event)
1687 def on_comment_cells(self, event):
1688 self.execute_sharp_comment(event)
1690 def on_uncomment_cells(self, event):
1691 self.execute_sharp_uncomment(event)
1693 def on_insert_rows(self, event):
1694 self.insert_row(event)
1696 def on_delete_rows(self, event):
1697 wx.CallAfter(self.delete_row, event)
1699 def on_move_rows_up(self, event):
1700 self.move_row_up(event)
1702 def on_move_rows_down(self, event):
1703 self.move_row_down(event)
1705 def on_content_assistance(self, event):
1706 self.on_content_assist(event)
1708 def on_key(self, *args):
1709 """ Intentional override """
1710 pass
1712 def cut(self):
1713 self.source_editor.Cut()
1714 self.mark_file_dirty(self.source_editor.GetModify())
1716 def copy(self):
1717 self.source_editor.Copy()
1719 def paste(self):
1720 focus = wx.Window.FindFocus()
1721 if focus == self.source_editor:
1722 self.source_editor.Paste()
1723 self.mark_file_dirty(self.source_editor.GetModify())
1725 def select_all(self):
1726 self.source_editor.SelectAll()
1728 def undo(self):
1729 self.source_editor.Undo()
1730 self.store_position()
1731 self.mark_file_dirty(self.source_editor.GetModify()) # self._dirty == 1 and
1733 def redo(self):
1734 self.source_editor.Redo()
1735 self.store_position()
1736 self.mark_file_dirty(self.source_editor.GetModify())
1738 def remove_and_store_state(self):
1739 if self.source_editor: 1739 ↛ exitline 1739 didn't return from function 'remove_and_store_state' because the condition on line 1739 was always true1D
1740 self.store_position() 1D
1741 self._stored_text = self.source_editor.GetText() 1D
1743 def _create_editor_text_control(self, text=None, language=None):
1744 self.source_editor = RobotDataEditor(self, language=language)
1745 self.Sizer.add_expanding(self.source_editor)
1746 self.Sizer.Layout()
1747 if text is not None: 1747 ↛ 1748line 1747 didn't jump to line 1748 because the condition on line 1747 was never true
1748 self.source_editor.set_text(text)
1749 self._kw_doc_timer = wx.Timer(self.source_editor)
1750 self.source_editor.Bind(wx.EVT_KEY_DOWN, self.on_key_down)
1751 self.source_editor.Bind(wx.EVT_CHAR, self.on_char)
1752 self.source_editor.Bind(wx.EVT_KEY_UP, self.on_editor_key)
1753 self.source_editor.Bind(wx.EVT_MOTION, self.on_mouse_motion)
1754 self.source_editor.Bind(wx.EVT_KILL_FOCUS, self.LeaveFocus)
1755 self.source_editor.Bind(wx.EVT_SET_FOCUS, self.GetFocus)
1756 self.source_editor.Bind(wx.EVT_MENU, self.on_menu)
1757 # DEBUG: Add here binding for keyword help
1759 def on_menu(self, event):
1760 m_id=event.GetId()
1761 if m_id in (12, 14): # Cut and Paste
1762 self.mark_file_dirty(True) # DEBUG: Forcing dirty, even if it may not be
1763 event.Skip()
1765 def LeaveFocus(self, event):
1766 __ = event 1ak
1767 self.source_editor.hide_kw_doc() 1ak
1768 self.source_editor.AcceptsFocusFromKeyboard() 1ak
1769 self.store_position() 1ak
1770 self.source_editor.SetCaretPeriod(0) 1ak
1772 def GetFocus(self, event):
1773 self.source_editor.SetFocus()
1774 self.source_editor.AcceptsFocusFromKeyboard()
1775 self.source_editor.SetCaretPeriod(500)
1776 if self._position: 1776 ↛ 1777line 1776 didn't jump to line 1777 because the condition on line 1776 was never true
1777 self.set_editor_caret_position()
1778 if event: 1778 ↛ exitline 1778 didn't return from function 'GetFocus' because the condition on line 1778 was always true
1779 event.Skip()
1781 def revert(self):
1782 self.reset()
1783 self.source_editor.Undo()
1784 # self.source_editor.set_text(self._data.content)
1786 def on_editor_key(self, event):
1787 # print(f"DEBUG: TextEditor on_editor_key event={event} focus={self.is_focused()}")
1788 if not self.is_focused():
1789 event.Skip()
1790 return
1791 keycode = event.GetKeyCode()
1792 keyvalue = event.GetUnicodeKey()
1793 if keycode in [wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER]:
1794 self.mark_file_dirty(self.source_editor.GetModify())
1795 return
1796 if keyvalue == wx.WXK_NONE and keycode in [wx.WXK_CONTROL, wx.WXK_RAW_CONTROL]:
1797 self.source_editor.hide_kw_doc()
1798 elif keycode == wx.WXK_DELETE or (keyvalue != wx.WXK_NONE and keycode > keyvalue):
1799 # DEBUG on Windows we only get here, single Text Editor
1800 selected = self.source_editor.GetSelection()
1801 if selected[0] == selected[1]:
1802 pos = self.source_editor.GetInsertionPoint()
1803 if pos != self.source_editor.GetLastPosition():
1804 self.source_editor.DeleteRange(selected[0], 1)
1805 else:
1806 self.source_editor.DeleteRange(selected[0], selected[1] - selected[0])
1807 if self.is_focused(): # DEBUG and keycode != wx.WXK_CONTROL and keyvalue >= ord(' ') and self.dirty:
1808 self.mark_file_dirty(self.source_editor.GetModify())
1809 event.Skip()
1811 def on_key_down(self, event):
1812 """
1813 Some events are also in registered actions, because they are caught by kweditor before reaching here.
1814 When we use on the Text Editor (Grid Editor plugin disables), we need to catch these keys. This is the
1815 case of Ctrl-(Shift)-3 and Ctrl-(Shift)-4.
1817 :param event:
1818 :return:
1819 """
1820 # print(f"DEBUG: TextEditor on_key_down event={event} focus={self.is_focused()}")
1821 if not self.is_focused():
1822 event.Skip()
1823 return
1824 keycode = event.GetUnicodeKey()
1825 raw_key = event.GetKeyCode()
1826 # print(f"DEBUG: TextEditor on_key_down event={event} raw_key={raw_key} wx.WXK_C ={wx.WXK_CONTROL}")
1827 if event.GetKeyCode() == wx.WXK_DELETE:
1828 self.mark_file_dirty(self.source_editor.GetModify())
1829 return
1830 if raw_key != wx.WXK_CONTROL: # We need to clear doc as soon as possible
1831 self.source_editor.hide_kw_doc()
1832 if event.GetKeyCode() == wx.WXK_TAB and not event.ControlDown() and not event.ShiftDown():
1833 if self._showing_list: # Allows to use Tab for keyword selection
1834 self._showing_list = False
1835 # wx.CallAfter(self.write_ident) # DEBUG: Make this configurable?
1836 event.Skip()
1837 self.mark_file_dirty(self.source_editor.GetModify())
1838 return
1839 selected = self.source_editor.GetSelection()
1840 if selected[0] == selected[1]:
1841 self.write_ident()
1842 else:
1843 self.indent_block()
1844 self.mark_file_dirty(self.source_editor.GetModify())
1845 return
1846 elif event.GetKeyCode() == wx.WXK_TAB and event.ShiftDown():
1847 selected = self.source_editor.GetSelection()
1848 if selected[0] == selected[1]:
1849 pos = self.source_editor.GetCurrentPos()
1850 self.source_editor.SetCurrentPos(max(0, pos - self.tab_size))
1851 self.store_position()
1852 if not event.ControlDown(): # No text selection
1853 pos = self.source_editor.GetCurrentPos()
1854 self.source_editor.SetSelection(pos, pos)
1855 else:
1856 self.deindent_block()
1857 self.mark_file_dirty(self.source_editor.GetModify())
1858 return
1859 elif event.GetKeyCode() in [wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER]:
1860 if not self._showing_list:
1861 self.auto_indent()
1862 else:
1863 self._showing_list = False
1864 # wx.CallAfter(self.write_ident) # DEBUG: Make this configurable?
1865 event.Skip()
1866 self.mark_file_dirty(self.source_editor.GetModify())
1867 return
1868 elif keycode in (ord('1'), ord('2'), ord('5')) and event.ControlDown():
1869 self.execute_variable_creator(list_variable=(keycode == ord('2')),
1870 dict_variable=(keycode == ord('5')))
1871 self.store_position()
1872 self.mark_file_dirty(self.source_editor.GetModify())
1873 elif ((not IS_WINDOWS and not IS_MAC and keycode in (ord('v'), ord('V'))
1874 or keycode in (ord('d'), ord('D'))) and event.ControlDown() and not event.ShiftDown()):
1875 # We need to ignore this in Linux, because it does double-action
1876 # We need to ignore Ctl-D because Scintilla does Duplicate line
1877 self.mark_file_dirty(self.source_editor.GetModify())
1878 return
1879 elif keycode in (ord('g'), ord('G')) and event.ControlDown():
1880 if event.ShiftDown():
1881 wx.CallAfter(self.on_find_backwards, event)
1882 else:
1883 wx.CallAfter(self.on_find, event)
1884 return
1885 elif event.ControlDown() and raw_key == wx.WXK_CONTROL:
1886 # This must be the last branch to activate actions before doc
1887 # DEBUG: coords = self._get_screen_coordinates()
1888 self.source_editor.show_kw_doc()
1889 self.mark_file_dirty(self.source_editor.GetModify())
1890 event.Skip()
1892 # These commands are duplicated by global actions
1893 """ DEBUG
1894 else:
1895 self.source_editor.hide_kw_doc()
1896 elif keycode == ord('D') and event.ControlDown():
1897 if event.ShiftDown():
1898 self.delete_cell(event)
1899 else:
1900 self.delete_row(event)
1901 elif keycode == ord('3') and event.ControlDown():
1902 if event.ShiftDown():
1903 self.execute_sharp_comment(event)
1904 else:
1905 self.execute_comment(event)
1906 elif keycode == ord('4') and event.ControlDown():
1907 if event.ShiftDown():
1908 self.execute_sharp_uncomment(event)
1909 else:
1910 self.execute_uncomment(event)
1911 """
1913 @staticmethod
1914 def _get_screen_coordinates():
1915 point = wx.GetMousePosition()
1916 point.x += 25
1917 point.y += 25
1918 return point
1920 def on_char(self, event):
1921 if not self.is_focused():
1922 self.GetFocus(None)
1923 keycode = event.GetUnicodeKey()
1924 if chr(keycode) in ['[', '{', '(', "'", '\"', '`']:
1925 self.execute_enclose_text(chr(keycode))
1926 self.store_position()
1927 else:
1928 event.Skip()
1930 def on_mouse_motion(self, event):
1931 if event.CmdDown(): 1931 ↛ 1932line 1931 didn't jump to line 1932 because the condition on line 1931 was never true1kG
1932 self._kw_doc_timer.Stop()
1933 # self.source_editor.show_kw_doc()
1934 else:
1935 if self.old_information_popup != self.source_editor._information_popup: 1935 ↛ 1936line 1935 didn't jump to line 1936 because the condition on line 1935 was never true1kG
1936 self.source_editor.hide_kw_doc()
1937 self.old_information_popup = self.source_editor._information_popup
1938 self._start_kw_doc_timer()
1939 event.Skip() 1kG
1941 def _start_kw_doc_timer(self):
1942 self._kw_doc_timer.Start(1000, True)
1944 def execute_variable_creator(self, list_variable=False, dict_variable=False):
1945 from_, to_ = self.source_editor.GetSelection()
1946 text = self.source_editor.GetSelectedText()
1947 size = len(bytes(text, encoding='utf-8'))
1948 to_ = from_ + size
1949 if list_variable:
1950 symbol = '@'
1951 elif dict_variable:
1952 symbol = '&'
1953 else:
1954 symbol = '$'
1955 if size == 0:
1956 self.source_editor.SetInsertionPoint(to_)
1957 self.source_editor.InsertText(from_, self._variable_creator_value(symbol))
1958 self.source_editor.SetSelection(from_ + 2, from_ + 2,)
1959 self.source_editor.SetInsertionPoint(from_ + 2)
1960 else:
1961 self.source_editor.DeleteRange(from_, size)
1962 self.source_editor.SetInsertionPoint(from_)
1963 self.source_editor.ReplaceSelection(self._variable_creator_value(symbol, text))
1964 self.source_editor.SetSelection(from_ + size + 2, from_ + size + 2)
1965 self.source_editor.SetInsertionPoint(from_ + size + 2)
1967 @staticmethod
1968 def _variable_creator_value(symbol, value=''):
1969 return symbol + '{' + value + '}'
1971 def execute_enclose_text(self, keycode):
1972 from_, to_ = self.source_editor.GetSelection()
1973 text = self.source_editor.GetSelectedText()
1974 size = len(bytes(text, encoding='utf-8'))
1975 to_ = from_ + size
1976 if size == 0:
1977 self.source_editor.SetInsertionPoint(to_)
1978 self.source_editor.InsertText(from_, self._enclose_text(keycode))
1979 pos = self.source_editor.GetCurrentPos()
1980 self.source_editor.SetSelection(pos + 1, pos + 1)
1981 else:
1982 self.source_editor.DeleteRange(from_, size)
1983 self.source_editor.SetInsertionPoint(from_)
1984 self.source_editor.ReplaceSelection(self._enclose_text(keycode, text))
1985 self.source_editor.SetSelection(from_ + 1, from_ + size + 1)
1987 @staticmethod
1988 def _enclose_text(open_symbol, value=''):
1989 if open_symbol == '[':
1990 close_symbol = ']'
1991 elif open_symbol == '{':
1992 close_symbol = '}'
1993 elif open_symbol == '(':
1994 close_symbol = ')'
1995 else:
1996 close_symbol = open_symbol
1997 return open_symbol + value + close_symbol
1999 def _prepare_selection(self, start, end):
2000 new_end_line = end_line = self.source_editor.LineFromPosition(end) 1q
2001 last_line = self.source_editor.GetLineCount() 1q
2002 # get the next row content
2003 if end_line + 1 < last_line - 3: 2003 ↛ 2004line 2003 didn't jump to line 2004 because the condition on line 2003 was never true1q
2004 rowbelow = self.source_editor.GetLine(end_line + 1)
2005 # exception if moving block is long assignemnt or arguments with continuation markers extend selection
2006 is_marker = self.first_non_space_content(rowbelow)
2007 if is_marker == '...':
2008 new_end_line = self.find_initial_keyword(end_line, up=False)
2009 if new_end_line != end_line: # Extend selection 2009 ↛ 2010line 2009 didn't jump to line 2010 because the condition on line 2009 was never true1q
2010 new_end_pos = self.source_editor.GetLineEndPosition(new_end_line - 1)
2011 self.source_editor.SetSelection(start, new_end_pos - 1)
2013 def _above_row_selection(self, new_ini_line, ini_line):
2014 # get the previous row content
2015 rowabove = self.source_editor.GetLine(ini_line - 1) 1q
2016 is_marker = self.first_non_space_content(rowabove) 1q
2017 if is_marker == '...': 2017 ↛ 2018line 2017 didn't jump to line 2018 because the condition on line 2017 was never true1q
2018 new_ini_line = max(1, self.find_initial_keyword(ini_line))
2019 self.source_editor.BeginUndoAction() 1q
2020 if new_ini_line != ini_line: 2020 ↛ 2022line 2020 didn't jump to line 2022 because the condition on line 2020 was never true1q
2021 # Move block to up new_ini_line
2022 delta = ini_line - new_ini_line
2023 for _ in range(0, delta):
2024 self.source_editor.MoveSelectedLinesUp()
2025 else:
2026 self.source_editor.MoveSelectedLinesUp() 1q
2028 def move_row_up(self, event):
2029 __ = event 1q
2030 start, end = self.source_editor.GetSelection() 1q
2031 new_ini_line = ini_line = self.source_editor.LineFromPosition(start) 1q
2032 if ini_line == 0: 2032 ↛ 2033line 2032 didn't jump to line 2033 because the condition on line 2032 was never true1q
2033 return
2034 self._prepare_selection(start, end) 1q
2035 # exception if is long assignemnt or arguments with continuation markers get top line
2036 self._above_row_selection(new_ini_line, ini_line) 1q
2037 self.source_editor.EndUndoAction() 1q
2038 # New selection
2039 start, end = self.source_editor.GetSelection() 1q
2040 new_end_line = self.source_editor.LineFromPosition(end - 1) # One char before 1q
2041 nendpos = self.source_editor.GetLineEndPosition(new_end_line) 1q
2042 self.source_editor.SetAnchor(nendpos) 1q
2043 new_ini_line = self.source_editor.LineFromPosition(start) 1q
2044 # indentation after move
2045 new_top = self.source_editor.GetLine(new_ini_line) 1q
2046 rowbelow = self.source_editor.GetLine(new_end_line + 1) 1q
2047 old_start = self.first_non_space(rowbelow) 1q
2048 new_start = self.first_non_space(new_top) 1q
2049 was_end_old = rowbelow[old_start:old_start+3] == 'END' 1q
2050 was_end_new = new_top[new_start:new_start+3] == 'END' 1q
2051 if new_start == old_start and was_end_old and was_end_new: 2051 ↛ 2052line 2051 didn't jump to line 2052 because the condition on line 2051 was never true1q
2052 self.deindent_line(new_end_line + 1)
2053 self.indent_block()
2054 elif new_start == old_start and was_end_old: 2054 ↛ 2055line 2054 didn't jump to line 2055 because the condition on line 2054 was never true1q
2055 self.indent_block()
2056 start, end = self.source_editor.GetSelection() 1q
2057 if old_start > new_start and was_end_new: 2057 ↛ 2058line 2057 didn't jump to line 2058 because the condition on line 2057 was never true1q
2058 self.deindent_line(new_end_line + 1)
2059 if was_end_old:
2060 self.indent_block()
2061 if new_start < old_start and not was_end_new: 2061 ↛ 2062line 2061 didn't jump to line 2062 because the condition on line 2061 was never true1q
2062 self.indent_block()
2063 elif new_start > old_start: 2063 ↛ 2064line 2063 didn't jump to line 2064 because the condition on line 2063 was never true1q
2064 self.deindent_block()
2066 def _find_up_kw(self, start:int) -> int:
2067 if start <= 3:
2068 return start
2069 text = self.source_editor.GetLine(start)
2070 is_marker = self.first_non_space_content(text) # check if selection starts with marker
2071 if is_marker == '...':
2072 return start
2073 is_marker = '...'
2074 found = False
2075 line = start - 2
2076 while is_marker == '...' and line > 2:
2077 row = self.source_editor.GetLine(line)
2078 is_marker = self.first_non_space_content(row)
2079 if is_marker != '...':
2080 found = True
2081 break
2082 line -= 1
2083 return line if found else start - 1
2085 def _find_down_kw(self, start:int) -> int:
2086 last_line = self.source_editor.GetLineCount()
2087 if start > last_line - 3:
2088 return start
2089 text = self.source_editor.GetLine(start)
2090 is_marker = self.first_non_space_content(text) # check if selection starts with marker
2091 if is_marker == '...':
2092 return start
2093 is_marker = '...'
2094 found = False
2095 line = start + 2
2096 while is_marker == '...' and line < last_line - 2:
2097 row = self.source_editor.GetLine(line)
2098 is_marker = self.first_non_space_content(row)
2099 if is_marker != '...':
2100 found = True
2101 break
2102 line += 1
2103 return line if found else start + 1
2105 def find_initial_keyword(self, start: int, up=True):
2106 if up:
2107 return self._find_up_kw(start)
2108 return self._find_down_kw(start)
2110 def first_non_space_content(self, text):
2111 start = self.first_non_space(text) 1lq
2112 end = self.last_non_space(text[start:]) 1lq
2113 return text[start:start+end] 1lq
2115 @staticmethod
2116 def first_non_space(text):
2117 idx = 0 1lq
2118 for idx in range(0, len(text)): 1lq
2119 if text[idx] != ' ': 2119 ↛ 2118line 2119 didn't jump to line 2118 because the condition on line 2119 was always true1lq
2120 break 1lq
2121 return idx 1lq
2123 @staticmethod
2124 def last_non_space(text):
2125 idx = 0 1lq
2126 for idx in range(0, len(text)): 1lq
2127 if text[idx] == ' ': 1lq
2128 break 1lq
2129 return idx 1lq
2131 def move_row_down(self, event):
2132 __ = event 1l
2133 start, end = self.source_editor.GetSelection() 1l
2134 new_ini_line = ini_line = self.source_editor.LineFromPosition(start) 1l
2135 new_end_line = end_line = self.source_editor.LineFromPosition(end) 1l
2136 last_line = self.source_editor.GetLineCount() 1l
2137 # If line to move starts with ... move block without changes
2138 top_content = self.source_editor.GetLine(ini_line) 1l
2139 # exception if is long assignement or arguments with continuation markers get below line
2140 is_marker = self.first_non_space_content(top_content) 1l
2141 if is_marker != '...': 2141 ↛ 2143line 2141 didn't jump to line 2143 because the condition on line 2141 was always true1l
2142 new_ini_line = self._set_pos_by_marker(new_ini_line, new_end_line, end_line, last_line, start) 1l
2143 self.source_editor.BeginUndoAction() 1l
2144 if new_ini_line != ini_line: 2144 ↛ 2146line 2144 didn't jump to line 2146 because the condition on line 2144 was never true1l
2145 # Move block to down new_ini_line
2146 delta = new_ini_line - ini_line - 1
2147 for _ in range(0, delta):
2148 self.source_editor.MoveSelectedLinesDown()
2149 else:
2150 self.source_editor.MoveSelectedLinesDown() 1l
2151 self.source_editor.EndUndoAction() 1l
2152 self.source_editor.EnsureCaretVisible() 1l
2153 # New selection
2154 start, end = self.source_editor.GetSelection() 1l
2155 new_end_line = self.source_editor.LineFromPosition(end - 1) # One char before 1l
2156 new_ini_line = self.source_editor.LineFromPosition(start) 1l
2157 rowtop = self.source_editor.GetLine(new_ini_line - 1) # content before block 1l
2158 nendpos = self.source_editor.GetLineEndPosition(new_end_line) 1l
2159 self.source_editor.SetAnchor(nendpos) 1l
2160 # indentation after move
2161 newstart = self.source_editor.GetLine(new_ini_line) 1l
2162 new_start = self.first_non_space(newstart) 1l
2163 old_start = self.first_non_space(rowtop) 1l
2164 was_end_old = rowtop[old_start:old_start + 3] == 'END' 1l
2165 old_size = self.last_non_space(rowtop[old_start:]) 1l
2166 was_indent_old = rowtop[old_start:old_start+old_size] in INDENTED_START 1l
2167 was_end_new = newstart[new_start:new_start + 3] == 'END' 1l
2168 if new_start == old_start and was_end_new: 2168 ↛ 2169line 2168 didn't jump to line 2169 because the condition on line 2168 was never true1l
2169 self.indent_line(new_ini_line - 1)
2170 if new_start == old_start and was_indent_old: 2170 ↛ 2171line 2170 didn't jump to line 2171 because the condition on line 2170 was never true1l
2171 self.indent_block()
2172 if new_start > old_start and not was_indent_old: 2172 ↛ 2173line 2172 didn't jump to line 2173 because the condition on line 2172 was never true1l
2173 self.deindent_block()
2174 elif new_start < old_start and not was_end_new: 2174 ↛ 2175line 2174 didn't jump to line 2175 because the condition on line 2174 was never true1l
2175 self.indent_block()
2176 if new_start > old_start and was_end_new and was_end_old: 2176 ↛ 2177line 2176 didn't jump to line 2177 because the condition on line 2176 was never true1l
2177 self.indent_line(new_ini_line - 1)
2178 # New selection
2179 self._set_new_selection(new_ini_line, new_end_line) 1l
2181 def _set_pos_by_marker(self, new_ini_line:int, new_end_line:int, end_line:int, last_line:int, start:int) -> int:
2182 # get the next row content and length
2183 rowbelow = self.source_editor.GetLine(end_line + 1) 1l
2184 if end_line + 2 < last_line - 1: 2184 ↛ 2186line 2184 didn't jump to line 2186 because the condition on line 2184 was never true1l
2185 # exception if is long assignement or arguments with continuation markers get below line
2186 is_marker = self.first_non_space_content(rowbelow)
2187 if is_marker == '...':
2188 new_end_line = self.find_initial_keyword(end_line, up=False)
2189 if new_end_line != end_line: # Extend selection 2189 ↛ 2190line 2189 didn't jump to line 2190 because the condition on line 2189 was never true1l
2190 new_end_pos = self.source_editor.GetLineEndPosition(new_end_line - 1)
2191 self._select_anchor(start, new_end_pos - 1)
2192 # exception if target is long assignemnt or arguments with continuation markers get end line
2193 # get the after below row content
2194 rowafterbelow = self.source_editor.GetLine(new_end_line + 2) # Checking if is continuation arguments 1l
2195 is_marker = self.first_non_space_content(rowafterbelow) 1l
2196 if is_marker == '...': 2196 ↛ 2197line 2196 didn't jump to line 2197 because the condition on line 2196 was never true1l
2197 new_ini_line = self.find_initial_keyword(new_end_line + 1, up=False)
2198 return new_ini_line 1l
2200 def _set_new_selection(self, new_ini_line: int, new_end_line: int) -> None:
2201 nstartpos = self.source_editor.PositionFromLine(new_ini_line) 1l
2202 nendpos = self.source_editor.GetLineEndPosition(new_end_line) 1l
2203 self._select_anchor(nstartpos, nendpos) 1l
2205 def _select_anchor(self, start:int, end:int) -> None:
2206 self.source_editor.SetSelection(start, end) 1l
2207 self.source_editor.SetAnchor(start) 1l
2209 def delete_row(self, event):
2210 __ = event 1C
2211 start, end = self.source_editor.GetSelection() 1C
2212 ini_line = self.source_editor.LineFromPosition(start) 1C
2213 self.source_editor.SelectNone() 1C
2214 if start == end: 2214 ↛ 2217line 2214 didn't jump to line 2217 because the condition on line 2214 was always true1C
2215 end_line = ini_line 1C
2216 else:
2217 end_line = self.source_editor.LineFromPosition(end)
2218 for _ in range(ini_line, end_line + 1): 1C
2219 self.source_editor.GotoLine(ini_line) 1C
2220 self.source_editor.LineDelete() 1C
2221 self.store_position() 1C
2223 def insert_row(self, event):
2224 __ = event 1B
2225 start, end = self.source_editor.GetSelection() 1B
2226 ini_line = self.source_editor.LineFromPosition(start) 1B
2227 end_line = self.source_editor.LineFromPosition(end) 1B
2228 delta = end_line - ini_line 1B
2229 positionfromline = self.source_editor.PositionFromLine(ini_line) 1B
2230 self.source_editor.SelectNone() 1B
2231 self.source_editor.InsertText(positionfromline, '\n') 1B
2232 for nl in range(delta): 2232 ↛ 2233line 2232 didn't jump to line 2233 because the loop on line 2232 never started1B
2233 self.source_editor.InsertText(positionfromline + nl, '\n')
2234 self.source_editor.SetCurrentPos(positionfromline) 1B
2235 self.source_editor.SetAnchor(positionfromline) 1B
2236 self.source_editor.GotoLine(ini_line) 1B
2237 self.indent_line(ini_line) 1B
2238 self.store_position() 1B
2240 def execute_comment(self, event):
2241 __ = event 1x
2242 start, end = self.source_editor.GetSelection() 1x
2243 cursor = self.source_editor.GetCurrentPos() 1x
2244 ini_line = self.source_editor.LineFromPosition(start) 1x
2245 end_line = self.source_editor.LineFromPosition(end) 1x
2246 spaces = ' ' * self.tab_size 1x
2247 comment = 'Comment' + spaces 1x
2248 count = 0 1x
2249 self.source_editor.SelectNone() 1x
2250 row = ini_line 1x
2251 while row <= end_line: 1x
2252 pos = self.source_editor.PositionFromLine(row) 1x
2253 self.source_editor.SetCurrentPos(pos) 1x
2254 self.source_editor.SetSelection(pos, pos) 1x
2255 self.source_editor.SetInsertionPoint(pos) 1x
2256 line = self.source_editor.GetLine(row) 1x
2257 lenline = len(line) 1x
2258 if lenline > 0: 2258 ↛ 2263line 2258 didn't jump to line 2263 because the condition on line 2258 was always true1x
2259 idx = 0 1x
2260 while idx < lenline and line[idx] == ' ': 2260 ↛ 2261line 2260 didn't jump to line 2261 because the condition on line 2260 was never true1x
2261 idx += 1
2262 self.source_editor.InsertText(pos + idx, comment) 1x
2263 count += 1 1x
2264 row += 1 1x
2265 new_start = start 1x
2266 new_end = end + (count * len(comment)) 1x
2267 if cursor == start: 2267 ↛ 2268line 2267 didn't jump to line 2268 because the condition on line 2267 was never true1x
2268 ini = new_start
2269 fini = new_end
2270 else:
2271 ini = new_end 1x
2272 fini = new_start 1x
2273 self.source_editor.SetSelection(new_start, new_end) 1x
2274 self.source_editor.SetCurrentPos(ini) 1x
2275 self.source_editor.SetAnchor(fini) 1x
2276 self.store_position() 1x
2278 def execute_uncomment(self, event):
2279 __ = event 1t
2280 start, end = self.source_editor.GetSelection() 1t
2281 cursor = self.source_editor.GetCurrentPos() 1t
2282 ini_line = self.source_editor.LineFromPosition(start) 1t
2283 end_line = self.source_editor.LineFromPosition(end) 1t
2284 spaces = ' ' * self.tab_size 1t
2285 comment = 'Comment' + spaces 1t
2286 commentlong = 'BuiltIn.Comment' + spaces 1t
2287 self.source_editor.SelectNone() 1t
2288 count = 0 1t
2289 row = ini_line 1t
2290 while row <= end_line: 1t
2291 pos = self.source_editor.PositionFromLine(row) 1t
2292 self.source_editor.SetCurrentPos(pos) 1t
2293 self.source_editor.SetSelection(pos, pos) 1t
2294 self.source_editor.SetInsertionPoint(pos) 1t
2295 line = self.source_editor.GetLine(row) 1t
2296 lenline = len(line) 1t
2297 if lenline > 0: 2297 ↛ 2305line 2297 didn't jump to line 2305 because the condition on line 2297 was always true1t
2298 idx = 0 1t
2299 while idx < lenline and line[idx] == ' ': 2299 ↛ 2300line 2299 didn't jump to line 2300 because the condition on line 2299 was never true1t
2300 idx += 1
2301 if (line[idx:len(comment) + idx]).lower() == comment.lower(): 2301 ↛ 2303line 2301 didn't jump to line 2303 because the condition on line 2301 was always true1t
2302 self.source_editor.DeleteRange(pos + idx, len(comment)) 1t
2303 if (line[idx:len(commentlong) + idx]).lower() == commentlong.lower(): 2303 ↛ 2304line 2303 didn't jump to line 2304 because the condition on line 2303 was never true1t
2304 self.source_editor.DeleteRange(pos + idx, len(commentlong))
2305 count += 1 1t
2306 row += 1 1t
2307 new_start = start 1t
2308 new_end = end - (count * len(comment)) 1t
2309 if cursor == start: 2309 ↛ 2310line 2309 didn't jump to line 2310 because the condition on line 2309 was never true1t
2310 ini = new_start
2311 fini = new_end
2312 else:
2313 ini = new_end 1t
2314 fini = new_start 1t
2315 self.source_editor.SetSelection(new_start, new_end) 1t
2316 self.source_editor.SetCurrentPos(ini) 1t
2317 self.source_editor.SetAnchor(fini) 1t
2318 self.store_position() 1t
2320 def insert_cell(self, event):
2321 __ = event 1nmo
2322 start, end = self.source_editor.GetSelection() 1nmo
2323 ini_line = self.source_editor.LineFromPosition(start) 1nmo
2324 end_line = self.source_editor.LineFromPosition(end) 1nmo
2325 begpos = self.source_editor.PositionFromLine(ini_line) 1nmo
2326 endpos = self.source_editor.PositionFromLine(end_line + 1) 1nmo
2327 cell_no_beg = self._get_cell_no(begpos, endpos, start) 1nmo
2328 cell_pos_beg = self._get_position_of_cell(begpos, endpos, cell_no_beg) 1nmo
2329 # if there is a selection subtract 1 from endpos to circumvent cursor being on end of cell
2330 # --> otherwise no will be next cell no
2331 if start != end: 1nmo
2332 cell_no_end = self._get_cell_no(begpos, endpos, end - 1) 1mo
2333 else:
2334 cell_no_end = cell_no_beg 1n
2335 # print(f"DEBUG: cell range to handle beg={cell_no_beg} end={cell_no_end}")
2336 celltab = ' ' * self.tab_size 1nmo
2337 # If the selection spans more than one line:
2338 if ini_line < end_line: # DEBUG: do inserts in such a way that they can be undone in 1 undo 1nmo
2339 new_start = cell_pos_beg 1m
2340 for line in range(ini_line, end_line + 1): 1m
2341 begthis = self.source_editor.PositionFromLine(line) 1m
2342 endthis = self.source_editor.PositionFromLine(line + 1) 1m
2343 cell_pos_beg = self._get_position_of_cell(begthis, endthis, cell_no_beg) 1m
2344 self.source_editor.InsertText(cell_pos_beg, celltab) 1m
2345 new_end = cell_pos_beg + (len(celltab.encode('utf-8'))) 1m
2346 elif start == end: # On a single row, no selection 1no
2347 self.source_editor.InsertText(cell_pos_beg, celltab) 1n
2348 new_start = cell_pos_beg 1n
2349 new_end = cell_pos_beg + len(celltab.encode('utf-8')) 1n
2350 else: # On a single row, with selection
2351 cells_to_insert = cell_no_end - cell_no_beg + 1 1o
2352 # insert at once so undo handles it correct
2353 self.source_editor.InsertText(cell_pos_beg, celltab * cells_to_insert) 1o
2354 new_start = cell_pos_beg 1o
2355 new_end = cell_pos_beg + (len(celltab.encode('utf-8')) * cells_to_insert) 1o
2356 # SetSelection and SetCurrentPos + Store_position overrule each other so only use one of them
2357 self.source_editor.SetSelection(new_start, new_end) 1nmo
2358 # @Helio: SetAnchor overrules the SetSelection if it specifies a different start than
2359 # SetSelection (but I left your code for now)
2360 self.source_editor.SetAnchor(new_end) 1nmo
2362 def delete_cell(self, event):
2363 __ = event 1p
2364 start, end = self.source_editor.GetSelection() 1p
2365 ini_line = self.source_editor.LineFromPosition(start) 1p
2366 end_line = self.source_editor.LineFromPosition(end) 1p
2367 begpos = self.source_editor.PositionFromLine(ini_line) 1p
2368 endpos = self.source_editor.PositionFromLine(end_line + 1) 1p
2369 cell_no_beg = self._get_cell_no(begpos, endpos, start) 1p
2370 cell_pos_beg = self._get_position_of_cell(begpos, endpos, cell_no_beg) 1p
2371 # if there is a selection subtract 1 from endpos to circumvent cursor being on end of cell
2372 # --> otherwise no will be next cell no
2373 if start != end: 2373 ↛ 2376line 2373 didn't jump to line 2376 because the condition on line 2373 was always true1p
2374 cell_no_end = self._get_cell_no(begpos, endpos, end - 1) 1p
2375 else:
2376 cell_no_end = cell_no_beg
2377 cell_pos_end = self._get_position_of_cell(begpos, endpos, cell_no_end + 1) 1p
2378 self.source_editor.Remove(cell_pos_beg, cell_pos_end) 1p
2379 new_start = cell_pos_beg 1p
2380 new_end = new_start + (end - start) 1p
2381 # SetSelection and SetCurrentPos + Store_position overrule each other so only use one of them
2382 self.source_editor.SetSelection(new_start, new_end) 1p
2383 # @Helio: SetAnchor overrules the SetSelection if it specifies a different start than SetSelection
2384 # I am not sure what any selection should be after deleting big ranges
2385 self.source_editor.SetAnchor(new_start) 1p
2387 def _get_cell_no(self, begpos, endpos, findpos):
2388 # get cell number from range begpos-endpos using findpos
2389 cell_no = 0 1pnmo
2390 celltot = self._get_number_of_cells(begpos, endpos) 1pnmo
2391 while cell_no < celltot: 1pnmo
2392 cell_no += 1 1pnmo
2393 cellpos = self._get_position_of_cell(begpos, endpos, cell_no) 1pnmo
2394 if cellpos > findpos: 1pnmo
2395 cell_no -= 1 1pnm
2396 break 1pnm
2397 return cell_no 1pnmo
2399 def _get_number_of_cells(self, begpos, endpos):
2400 # get number of cells in range begpos-endpos
2401 # Warning! GetStringSelection does not work properly if there are diacritics in the content above (!)
2402 # the selected range
2403 the_content = self.source_editor.GetTextRange(begpos, endpos) 1pnmo
2404 celltab = ' ' * self.tab_size 1pnmo
2405 return the_content.count(celltab) 1pnmo
2407 def calc_cellpos(self, begpos, endpos, cell_no):
2408 _cellpos = 0 1pnmo
2409 celltab = ' ' * self.tab_size 1pnmo
2410 cellencode = celltab.encode('utf-8') 1pnmo
2411 # Warning! GetStringSelection does not work properly if there are diacritics in
2412 # the content above (!) the selected range
2413 textrange = self.source_editor.GetTextRange(begpos, endpos) 1pnmo
2414 textencode = textrange.encode('utf-8') 1pnmo
2415 fndcnt = 1 # begpos is always in a cell 1pnmo
2416 fndidx = 0 1pnmo
2417 while fndidx != -1: 2417 ↛ 2427line 2417 didn't jump to line 2427 because the condition on line 2417 was always true1pnmo
2418 fndidx = textencode.find(cellencode, fndidx) 1pnmo
2419 if fndidx != -1: 2419 ↛ 2417line 2419 didn't jump to line 2417 because the condition on line 2419 was always true1pnmo
2420 if fndcnt == 1 and fndidx == 0: # check if begpos is at the beginning of a cell 1pnmo
2421 fndcnt -= 1 1pnmo
2422 fndcnt += 1 1pnmo
2423 if cell_no == fndcnt: 1pnmo
2424 _cellpos = begpos + fndidx 1pnmo
2425 break 1pnmo
2426 fndidx += 1 # for next search 1pnmo
2427 return _cellpos 1pnmo
2429 def _get_position_of_cell(self, begpos, endpos, cell_no):
2430 # get position of cell number within range begpos-endpos
2431 # DEBUG: this does not work correctly if first cell within the range is totally empty (so not as \ sanitized)
2432 cellcnt = self._get_number_of_cells(begpos, endpos) 1pnmo
2433 if cell_no <= cellcnt: # encode is needed for finding correct position when there are special characters 2433 ↛ 2437line 2433 didn't jump to line 2437 because the condition on line 2433 was always true1pnmo
2434 # in the content
2435 cellpos = self.calc_cellpos(begpos, endpos, cell_no) 1pnmo
2436 else: # cell_no does not exist -- return endpos-1
2437 cellpos = endpos - 1
2438 return cellpos 1pnmo
2440 def execute_sharp_comment(self, event):
2441 __ = event 1zuy
2442 start, end = self.source_editor.GetSelection() 1zuy
2443 cursor = self.source_editor.GetCurrentPos() 1zuy
2444 ini_line = self.source_editor.LineFromPosition(start) 1zuy
2445 end_line = self.source_editor.LineFromPosition(end) 1zuy
2446 spaces = ' ' * self.tab_size 1zuy
2447 count = 0 1zuy
2448 maxsize = self.source_editor.GetLineCount() 1zuy
2449 # If the selection spans on more than one line:
2450 if ini_line < end_line: 1zuy
2451 for line in range(ini_line, end_line + 1): 1u
2452 count += 1 1u
2453 if line < maxsize: 2453 ↛ 2456line 2453 didn't jump to line 2456 because the condition on line 2453 was always true1u
2454 self.source_editor.GotoLine(line) 1u
2455 else:
2456 self.source_editor.GotoLine(maxsize)
2457 pos = self.source_editor.PositionFromLine(line) 1u
2458 self.source_editor.SetCurrentPos(pos) 1u
2459 self.source_editor.SetSelection(pos, pos) 1u
2460 self.source_editor.SetInsertionPoint(pos) 1u
2461 row = self.source_editor.GetLine(line) 1u
2462 lenline = len(row) 1u
2463 if lenline > 0: 2463 ↛ 2451line 2463 didn't jump to line 2451 because the condition on line 2463 was always true1u
2464 idx = 0 1u
2465 while idx < lenline and row[idx] == ' ': 1u
2466 idx += 1 1u
2467 self.source_editor.InsertText(pos + idx, '# ') 1u
2468 elif start == end: # On a single row, no selection 1zy
2469 count += 1 1z
2470 pos = self.source_editor.PositionFromLine(ini_line) 1z
2471 row = self.source_editor.GetLine(ini_line) 1z
2472 lenline = len(row) 1z
2473 if lenline > 0: 2473 ↛ 2492line 2473 didn't jump to line 2492 because the condition on line 2473 was always true1z
2474 idx = 0 1z
2475 while idx < lenline and row[idx] == ' ': 1z
2476 idx += 1 1z
2477 self.source_editor.InsertText(pos + idx, '# ') 1z
2478 else: # On a single row, with selection
2479 count += 1 1y
2480 pos = self.source_editor.PositionFromLine(ini_line) 1y
2481 row = self.source_editor.GetLine(ini_line) 1y
2482 if cursor > pos: 2482 ↛ 2492line 2482 didn't jump to line 2492 because the condition on line 2482 was always true1y
2483 idx = cursor - pos 1y
2484 while idx >= len(spaces): 2484 ↛ 2489line 2484 didn't jump to line 2489 because the condition on line 2484 was always true1y
2485 if row[idx - len(spaces):idx] != spaces: 1y
2486 idx -= 1 1y
2487 else:
2488 break 1y
2489 if idx < len(spaces): 2489 ↛ 2490line 2489 didn't jump to line 2490 because the condition on line 2489 was never true1y
2490 idx = 0
2491 self.source_editor.InsertText(pos + idx, '# ') 1y
2492 new_start = start 1zuy
2493 new_end = end + (count * 2) 1zuy
2494 if cursor == start: 1zuy
2495 ini = new_start 1z
2496 fini = new_end 1z
2497 else:
2498 ini = new_end 1uy
2499 fini = new_start 1uy
2500 self.source_editor.SetSelection(new_start, new_end) # DEBUG: For some reason the selection is not restored! 1zuy
2501 self.source_editor.SetCurrentPos(ini) 1zuy
2502 self.source_editor.SetAnchor(fini) 1zuy
2503 self.source_editor.SetCurrentPos(cursor + count * 2) 1zuy
2504 self.store_position() 1zuy
2506 def execute_sharp_uncomment(self, event):
2507 __ = event 1vws
2508 start, end = self.source_editor.GetSelection() 1vws
2509 cursor = self.source_editor.GetCurrentPos() 1vws
2510 ini_line = self.source_editor.LineFromPosition(start) 1vws
2511 end_line = self.source_editor.LineFromPosition(end) 1vws
2512 spaces = ' ' * self.tab_size 1vws
2513 # self.source_editor.SelectNone()
2514 count = 0 1vws
2515 # maxsize = self.source_editor.GetLineCount()
2516 # If the selection spans on more than one line:
2517 if ini_line < end_line: 1vws
2518 for line in range(ini_line, end_line + 1): 1w
2519 pos = self.source_editor.PositionFromLine(line) 1w
2520 row = self.source_editor.GetLine(line) 1w
2521 lenline = len(row) 1w
2522 if lenline > 0: 2522 ↛ 2518line 2522 didn't jump to line 2518 because the condition on line 2522 was always true1w
2523 idx = 0 1w
2524 while idx < lenline and row[idx] == ' ': 1w
2525 idx += 1 1w
2526 size = 1 1w
2527 if idx + 1 < lenline and row[idx:idx + 1] == '#': 2527 ↛ 2518line 2527 didn't jump to line 2518 because the condition on line 2527 was always true1w
2528 if idx + 2 < lenline and row[idx + 1:idx + 2] == ' ': 2528 ↛ 2531line 2528 didn't jump to line 2531 because the condition on line 2528 was always true1w
2529 size = 2 1w
2530 # Here we clean up escaped spaces from Apply
2531 if idx + size < lenline: 2531 ↛ 2537line 2531 didn't jump to line 2537 because the condition on line 2531 was always true1w
2532 newrow = row[idx + size:] 1w
2533 newrow = newrow.replace('\\ ', ' ') 1w
2534 size += len(row[idx:]) - len(newrow) - size 1w
2535 self.source_editor.DeleteRange(pos + idx, len(newrow) + size) 1w
2536 self.source_editor.InsertText(pos + idx, newrow) 1w
2537 count += size 1w
2538 elif start == end: # On a single row, no selection 1vs
2539 pos = self.source_editor.PositionFromLine(ini_line) 1v
2540 row = self.source_editor.GetLine(ini_line) 1v
2541 lenline = len(row) 1v
2542 if lenline > 0: 2542 ↛ 2589line 2542 didn't jump to line 2589 because the condition on line 2542 was always true1v
2543 idx = 0 1v
2544 while idx < lenline and row[idx] == ' ': 1v
2545 idx += 1 1v
2546 while count == 0 and idx < lenline: 1v
2547 size = 1 1v
2548 if idx + 1 < lenline and row[idx:idx + 1] == '#': 2548 ↛ 2560line 2548 didn't jump to line 2560 because the condition on line 2548 was always true1v
2549 if idx + 2 < lenline and row[idx + 1:idx + 2] == ' ': 2549 ↛ 2552line 2549 didn't jump to line 2552 because the condition on line 2549 was always true1v
2550 size = 2 1v
2551 # Here we clean up escaped spaces from Apply
2552 if idx + size < lenline: 2552 ↛ 2558line 2552 didn't jump to line 2558 because the condition on line 2552 was always true1v
2553 newrow = row[idx + size:] 1v
2554 newrow = newrow.replace('\\ ', ' ') 1v
2555 size += len(row[idx:]) - len(newrow) - size 1v
2556 self.source_editor.DeleteRange(pos + idx, len(newrow) + size) 1v
2557 self.source_editor.InsertText(pos + idx, newrow) 1v
2558 count += size 1v
2559 else:
2560 idx += 1
2561 else: # On a single row, with selection
2562 pos = self.source_editor.PositionFromLine(ini_line) 1s
2563 row = self.source_editor.GetLine(ini_line) 1s
2564 lenline = len(row) 1s
2565 if cursor > pos: 2565 ↛ 2589line 2565 didn't jump to line 2589 because the condition on line 2565 was always true1s
2566 idx = cursor - pos 1s
2567 while idx >= len(spaces): 2567 ↛ 2572line 2567 didn't jump to line 2572 because the condition on line 2567 was always true1s
2568 if row[idx - len(spaces):idx] != spaces: 1s
2569 idx -= 1 1s
2570 else:
2571 break 1s
2572 if idx < len(spaces): 2572 ↛ 2573line 2572 didn't jump to line 2573 because the condition on line 2572 was never true1s
2573 idx = 0
2574 while count == 0 and idx > 0: 1s
2575 size = 1 1s
2576 if idx + 1 < lenline and row[idx:idx + 1] == '#': 2576 ↛ 2588line 2576 didn't jump to line 2588 because the condition on line 2576 was always true1s
2577 if idx + 2 < lenline and row[idx + 1:idx + 2] == ' ': 2577 ↛ 2580line 2577 didn't jump to line 2580 because the condition on line 2577 was always true1s
2578 size = 2 1s
2579 # Here we clean up escaped spaces from Apply
2580 if idx + size < lenline: 2580 ↛ 2586line 2580 didn't jump to line 2586 because the condition on line 2580 was always true1s
2581 newrow = row[idx + size:] 1s
2582 newrow = newrow.replace('\\ ', ' ') 1s
2583 size += len(row[idx:]) - len(newrow) - size 1s
2584 self.source_editor.DeleteRange(pos + idx, len(newrow) + size) 1s
2585 self.source_editor.InsertText(pos + idx, newrow) 1s
2586 count += size 1s
2587 else:
2588 idx -= 1
2589 if count == 0: 2589 ↛ 2590line 2589 didn't jump to line 2590 because the condition on line 2589 was never true1vws
2590 return
2591 new_start = start 1vws
2592 new_end = end - count 1vws
2593 self.source_editor.SetSelection(new_start, new_end) # DEBUG: For some reason the selection is not restored! 1vws
2594 self.source_editor.SetCurrentPos(cursor - count) 1vws
2595 self.store_position() 1vws
2597 def on_settings_changed(self, message):
2598 """Update tab size if txt spaces size setting is modified"""
2599 _, setting = message.keys
2600 if setting == TXT_NUM_SPACES: 2600 ↛ 2601line 2600 didn't jump to line 2601 because the condition on line 2600 was never true
2601 self.tab_size = self.source_editor_parent.app.settings.get(TXT_NUM_SPACES, 4)
2602 if setting == 'reformat': 2602 ↛ 2603line 2602 didn't jump to line 2603 because the condition on line 2602 was never true
2603 self.reformat = self.source_editor_parent.app.settings.get('reformat', False)
2605 def mark_file_dirty(self, dirty=True):
2606 if not self.is_focused(): # DEBUG: Was marking file clean from Grid Editor 2606 ↛ 2607line 2606 didn't jump to line 2607 because the condition on line 2606 was never true1c
2607 return
2608 if self._data: # and self._dirty == 1: 2608 ↛ exitline 2608 didn't return from function 'mark_file_dirty' because the condition on line 2608 was always true1c
2609 if dirty: 2609 ↛ 2610line 2609 didn't jump to line 2610 because the condition on line 2609 was never true1c
2610 self._data.mark_data_dirty()
2611 else:
2612 self._data.mark_data_pristine() 1c
2615class RobotDataEditor(PythonSTC):
2616 margin = 1
2618 def __init__(self, parent, readonly=False, language=None, style=wx.BORDER_NONE):
2619 # stc.StyledTextCtrl.__init__(self, parent)
2620 self.parent = parent
2621 self.language = language
2622 self._plugin = parent.plugin
2623 self._settings = parent.source_editor_parent.app.settings
2624 self.tab_markers = self._settings[PLUGIN_NAME].get('tab markers', True)
2625 self.fold_symbols = self._settings[PLUGIN_NAME].get('fold symbols', 2)
2626 PythonSTC.__init__(self, parent, -1, options={'tab markers':self.tab_markers, 'fold symbols':self.fold_symbols},
2627 style=style)
2628 self._information_popup = None
2629 self._old_details = None
2630 self.readonly = readonly
2631 # self.SetMarginType(self.margin, stc.STC_MARGIN_NUMBER)
2632 self.SetLexer(stc.STC_LEX_CONTAINER)
2633 self.SetReadOnly(True)
2634 self.SetUseTabs(False)
2635 caret_colour = self._settings[PLUGIN_NAME].get('setting', 'black')
2636 self._background = self._settings[PLUGIN_NAME].get('background', 'white')
2637 caret_colour = self.get_visible_color(caret_colour)
2638 caret_style = self._settings[PLUGIN_NAME].get('caret style', 'block')
2639 caret_style = stc.STC_CARETSTYLE_BLOCK if caret_style.lower() == 'block' else stc.STC_CARETSTYLE_LINE
2640 self.SetCaretStyle(caret_style)
2641 margin_background = self._settings['General'].get_without_default('secondary background')
2642 margin_foreground = self._settings['General'].get_without_default('secondary foreground')
2643 self.SetUpEditor(tab_size=parent.tab_size, tab_markers=self.tab_markers,
2644 m_bg=margin_background, m_fg=margin_foreground, caret_fg=caret_colour)
2645 self.Bind(stc.EVT_STC_STYLENEEDED, self.on_style)
2646 self.Bind(stc.EVT_STC_ZOOM, self.on_zoom)
2647 self.Bind(stc.EVT_STC_UPDATEUI, self.on_update_ui)
2648 self.Bind(stc.EVT_STC_MARGINCLICK, self.on_margin_click)
2649 # DEBUG:
2650 self.Bind(wx.EVT_KEY_DOWN, self.on_key_pressed)
2651 # Only set, after language: self.stylizer = RobotStylizer(self, self._settings, self.readonly)
2652 self.stylizer = None
2653 self.key_trigger = 0
2654 self.autocomplete = self._settings[PLUGIN_NAME].get(AUTO_SUGGESTIONS, False)
2655 # register some images for use in the AutoComplete box.
2656 # self.RegisterImage(1, Smiles.GetBitmap()) # DEBUG was images.
2657 self.RegisterImage(1, wx.ArtProvider.GetBitmap(wx.ART_FLOPPY, size=(16, 16)))
2658 self.RegisterImage(2, wx.ArtProvider.GetBitmap(wx.ART_NEW, size=(16, 16)))
2659 self.RegisterImage(3, wx.ArtProvider.GetBitmap(wx.ART_COPY, size=(16, 16)))
2661 def show_kw_doc(self, coords=None):
2662 # print(f"DEBUG: TextEditor RobotDataEditor show_kw_doc ENTER self.AutoCompActive()=={self.AutoCompActive()}")
2663 if self.AutoCompActive():
2664 selected = [self.AutoCompGetCurrentText()]
2665 else:
2666 selected = self.get_selected_or_near_text(keep_cursor_pos=True)
2667 # print(f"DEBUG: TextEditor RobotDataEditor show_kw_doc selected=={selected}")
2668 if selected:
2669 for kw in selected:
2670 self._show_keyword_details(kw, coords)
2672 def hide_kw_doc(self):
2673 list_of_popups = self.parent.GetChildren() 1ak
2674 for popup in list_of_popups: 1ak
2675 if isinstance(popup, HtmlPopupWindow): 2675 ↛ 2676line 2675 didn't jump to line 2676 because the condition on line 2675 was never true1ak
2676 popup.hide()
2677 self._old_details = None
2679 def on_key_pressed(self, event):
2680 if self.CallTipActive():
2681 self.CallTipCancel()
2682 key = event.GetKeyCode()
2683 if key == 32 and event.ControlDown():
2684 # Tips
2685 if event.ShiftDown():
2686 self.show_kw_doc()
2687 # Code completion
2688 else:
2689 self.parent.on_content_assist(event)
2690 self.key_trigger = 0
2691 else:
2692 # print(f"DEBUG: texteditor.py RobotDataEditor on_key_pressed calling try_autocomplete key={key}")
2693 self._try_autocomplete(key, event)
2694 event.Skip()
2696 def _try_autocomplete(self, key: int, event: wx.KeyEvent) -> None:
2697 if self.autocomplete and not event.ControlDown():
2698 if 32 < key < 309 and self.key_trigger > -1:
2699 if self.key_trigger < 2:
2700 self.key_trigger += 1
2701 else:
2702 self.key_trigger = -1
2703 self.parent.on_content_assist(event)
2704 else:
2705 self.key_trigger = 0
2707 def set_text(self, text):
2708 self.SetReadOnly(False) 1aApCxzuyvwstinmoBklqrcbjefdgh
2709 self.SetText(text) 1aApCxzuyvwstinmoBklqrcbjefdgh
2710 self.set_language(self.language) 1aApCxzuyvwstinmoBklqrcbjefdgh
2711 self.stylizer.stylize() 1aApCxzuyvwstinmoBklqrcbjefdgh
2712 self.EmptyUndoBuffer() 1aApCxzuyvwstinmoBklqrcbjefdgh
2713 self.SetMarginWidth(self.margin, self.calc_margin_width()) 1aApCxzuyvwstinmoBklqrcbjefdgh
2714 self.SetMarginWidth(2, self.TextWidth(stc.STC_STYLE_DEFAULT, "MM")) 1aApCxzuyvwstinmoBklqrcbjefdgh
2715 self.Update() 1aApCxzuyvwstinmoBklqrcbjefdgh
2717 def set_language(self, dlanguage):
2718 content = self.GetTextRaw() 1aApCxzuyvwstinmoBklqrcbjefdgh
2719 # print(f"DEBUG: set_language content={content}\nset_language={dlanguage}")
2720 if content and b"Language: " in content: # We need to recheck the language setting 1aApCxzuyvwstinmoBklqrcbjefdgh
2721 try: 1rcbjefdgh
2722 self.language = obtain_language(dlanguage, content) 1rcbjefdgh
2723 except ValueError: 1rcb
2724 self.language = 'English' 1rcb
2725 else:
2726 self.language = 'English' 1aApCxzuyvwstinmoBklqrcjd
2727 self.stylizer = RobotStylizer(self, self._settings, self.readonly, self.language) 1aApCxzuyvwstinmoBklqrcbjefdgh
2729 @property
2730 def utf8_text(self):
2731 return self.GetText().encode('UTF-8') 1A
2733 def on_style(self, event):
2734 __ = event 1D
2735 self.stylizer.stylize() 1D
2737 def on_zoom(self, event):
2738 __ = event 1aApCxzuyvwstinmoBklqDrcbjefdgh
2739 self.SetMarginWidth(self.margin, self.calc_margin_width()) 1aApCxzuyvwstinmoBklqDrcbjefdgh
2740 self.SetMarginWidth(2, self.TextWidth(stc.STC_STYLE_FOLDDISPLAYTEXT, "MM")) 1aApCxzuyvwstinmoBklqDrcbjefdgh
2741 self._set_zoom() 1aApCxzuyvwstinmoBklqDrcbjefdgh
2743 def _set_zoom(self):
2744 new = self.GetZoom() 1aApCxzuyvwstinmoBklqDrcbjefdgh
2745 old = self._settings[PLUGIN_NAME].get(ZOOM_FACTOR, 0) 1aApCxzuyvwstinmoBklqDrcbjefdgh
2746 if new != old: 2746 ↛ 2747line 2746 didn't jump to line 2747 because the condition on line 2746 was never true1aApCxzuyvwstinmoBklqDrcbjefdgh
2747 self._settings[PLUGIN_NAME].set(ZOOM_FACTOR, new)
2749 def calc_margin_width(self):
2750 style = stc.STC_STYLE_LINENUMBER 1aApCxzuyvwstinmoBklqDrcbjefdgh
2751 width = self.TextWidth(style, str(self.GetLineCount())) 1aApCxzuyvwstinmoBklqDrcbjefdgh
2752 return width + self.TextWidth(style, "M") 1aApCxzuyvwstinmoBklqDrcbjefdgh
2754 def get_selected_or_near_text(self, keep_cursor_pos=False):
2755 content = set() 1ic
2756 if keep_cursor_pos: 1ic
2757 restore_cursor_pos = self.GetInsertionPoint() 1i
2758 else:
2759 restore_cursor_pos = None 1ic
2760 # First get selected text
2761 selected = self.GetSelectedText() 1ic
2762 restore_start_pos = self.GetSelectionStart() 1ic
2763 restore_end_pos = self.GetSelectionEnd() 1ic
2764 # print(f"DEBUG: TextEditor RobotDataEditor get_selected_or_near_text, restore_cursor={restore_cursor_pos} "
2765 # f"restore_start_pos={restore_start_pos} restore_end_pos={restore_end_pos} anchor={self.GetAnchor()}")
2766 if selected: 1ic
2767 start_pos = self.GetSelectionStart() 1i
2768 if selected.endswith('.'): # Special cases for libraries prefix 2768 ↛ 2769line 2768 didn't jump to line 2769 because the condition on line 2768 was never true1i
2769 if restore_cursor_pos:
2770 self.SetInsertionPoint(restore_cursor_pos)
2771 else:
2772 self.SetInsertionPoint(start_pos + len(selected))
2773 elif len(selected.split('.')) > 1: 2773 ↛ 2774line 2773 didn't jump to line 2774 because the condition on line 2773 was never true1i
2774 parts = selected.split('.')
2775 self.SetSelectionStart(start_pos + len(parts[0]) + 1)
2776 self.SetSelectionEnd(start_pos + len(selected))
2777 if restore_cursor_pos:
2778 self.SetInsertionPoint(restore_cursor_pos)
2779 else:
2780 self.SetInsertionPoint(start_pos + len(parts[0]) + 1)
2781 else:
2782 self.SetSelectionStart(start_pos) 1i
2783 self.SetSelectionEnd(start_pos + len(selected)) 1i
2784 if restore_cursor_pos: 2784 ↛ 2785line 2784 didn't jump to line 2785 because the condition on line 2784 was never true1i
2785 self.SetInsertionPoint(restore_cursor_pos)
2786 else:
2787 self.SetInsertionPoint(start_pos + len(selected)) 1i
2788 content.add(selected.strip()) 1i
2789 # Next get text on the left
2790 text = self.GetCurLine()[0] 1ic
2791 start_pos = self.GetInsertionPoint() 1ic
2792 line = self.GetCurrentLine() 1ic
2793 line_end = self.GetLineEndPosition(line) 1ic
2794 size = self.GetLineLength(line) 1ic
2795 star = self.GetLineRaw(line) 1ic
2796 sz_star = len(star) - 1 1ic
2797 min_pos = line_end - sz_star 1ic
2798 pos_in_line = start_pos - min_pos 1ic
2799 # print(f"DEBUG: line={text}\nstar={star}\nmin_pos={min_pos} start_pos={start_pos}"
2800 # f" line_end={line_end}\nline={line}"
2801 # f" pos_in_line={pos_in_line} size={size} sz_star={sz_star} lentext={len(text)}")
2802 # if pos_in_line > 0:
2803 start_chr = end_chr = None 1ic
2804 try: 1ic
2805 for i in range(max(1, min(size, pos_in_line-1)), 1, -1): 1ic
2806 if text[i] == ' ' and text[i-1] == ' ': 1ic
2807 start_chr = i + 1 1ic
2808 break 1ic
2809 if pos_in_line >= 0: 1ic
2810 for i in range(pos_in_line, size): 1ic
2811 if text[i] == ' ' and text[i+1] == ' ': 1ic
2812 end_chr = i 1i
2813 break 1i
2814 except IndexError:
2815 pass
2816 # print(f"DEBUG: TextEditor RobotDataEditor get_selected_or_near_text, get text on the left {start_chr=}"
2817 # f" {end_chr=} {pos_in_line=}")
2818 value = None 1ic
2819 if start_chr is not None: 1ic
2820 if end_chr is not None: 1ic
2821 value = text[start_chr:end_chr] 1i
2822 else:
2823 value = text[start_chr:].strip() 1ic
2824 elif end_chr is not None: 1i
2825 value = text[pos_in_line:end_chr] 1i
2826 if value: 1ic
2827 # print(f"DEBUG: TextEditor RobotDataEditor get_selected_or_near_text, get text on the left {value=} ")
2828 if start_chr: 2828 ↛ 2831line 2828 didn't jump to line 2831 because the condition on line 2828 was always true1ic
2829 start_pos = min_pos + start_chr 1ic
2830 else:
2831 start_pos = min_pos + pos_in_line
2832 if value.endswith('.'): # Special cases for libraries prefix 2832 ↛ 2833line 2832 didn't jump to line 2833 because the condition on line 2832 was never true1ic
2833 if restore_cursor_pos:
2834 self.SetInsertionPoint(restore_cursor_pos)
2835 else:
2836 self.SetInsertionPoint(start_pos + len(value))
2837 elif len(value.split('.')) > 1: 2837 ↛ 2838line 2837 didn't jump to line 2838 because the condition on line 2837 was never true1ic
2838 if restore_cursor_pos:
2839 self.SetInsertionPoint(restore_cursor_pos)
2840 else:
2841 parts = value.split('.')
2842 self.SetSelectionStart(start_pos + len(parts[0]) + 1)
2843 self.SetSelectionEnd(start_pos + len(value))
2844 self.SetInsertionPoint(start_pos + len(parts[0]) + 1)
2845 else:
2846 if restore_cursor_pos: 1ic
2847 self.SetInsertionPoint(restore_cursor_pos) 1i
2848 else:
2849 self.SetSelectionStart(start_pos) 1ic
2850 self.SetSelectionEnd(start_pos + len(value)) 1ic
2851 self.SetInsertionPoint(start_pos) 1ic
2852 content.add(value) 1ic
2853 else:
2854 for bit in text.strip().strip('*').split(' '): 1i
2855 content.add(bit) 1i
2856 try: 1i
2857 content.remove('') 1i
2858 except KeyError:
2859 pass
2860 # print(f"DEBUG: TextEditor RobotDataEditor get_selected_or_near_text, content={content} ")
2861 if restore_cursor_pos: 1ic
2862 self.SetAnchor(restore_cursor_pos) 1i
2863 self.SetInsertionPoint(restore_cursor_pos) 1i
2864 if restore_start_pos: 1ic
2865 self.SetSelection(restore_start_pos, restore_end_pos) 1ic
2866 return content if content else [''] 1ic
2868 def on_update_ui(self, evt):
2869 _ = evt 1k
2870 # check for matching braces
2871 brace_at_caret = -1 1k
2872 brace_opposite = -1 1k
2873 char_before = None 1k
2874 caret_pos = self.GetCurrentPos() 1k
2876 if caret_pos > 0: 2876 ↛ 2880line 2876 didn't jump to line 2880 because the condition on line 2876 was always true1k
2877 char_before = self.GetCharAt(caret_pos - 1) 1k
2879 # check before
2880 if char_before and chr(char_before) in "[]{}()": 2880 ↛ 2881line 2880 didn't jump to line 2881 because the condition on line 2880 was never true1k
2881 brace_at_caret = caret_pos - 1
2883 # check after
2884 if brace_at_caret < 0: 2884 ↛ 2890line 2884 didn't jump to line 2890 because the condition on line 2884 was always true1k
2885 char_after = self.GetCharAt(caret_pos) 1k
2887 if char_after and chr(char_after) in "[]{}()": 2887 ↛ 2888line 2887 didn't jump to line 2888 because the condition on line 2887 was never true1k
2888 brace_at_caret = caret_pos
2890 if brace_at_caret >= 0: 2890 ↛ 2891line 2890 didn't jump to line 2891 because the condition on line 2890 was never true1k
2891 brace_opposite = self.BraceMatch(brace_at_caret)
2893 if brace_at_caret != -1 and brace_opposite == -1: 2893 ↛ 2894line 2893 didn't jump to line 2894 because the condition on line 2893 was never true1k
2894 self.BraceBadLight(brace_at_caret)
2895 else:
2896 self.BraceHighlight(brace_at_caret, brace_opposite) 1k
2897 self.stylizer.stylize() 1k
2899 def _show_keyword_details(self, value, coords=None):
2900 """
2901 Shows the keyword documentation in value at coordinates, coords.
2902 :param value: The content to show in detacheable window
2903 :param coords: If None they will be mouse pointer coordinates
2904 """
2905 details = self._plugin.get_keyword_details(value)
2906 if details and details != self._old_details: # This is because on Windows keys are sent in repeat
2907 if not coords:
2908 position = wx.GetMousePosition()
2909 else:
2910 position = coords
2911 self._information_popup = HtmlPopupWindow(self.parent, (450, 300))
2912 self._information_popup.set_content(details, value)
2913 self._information_popup.show_at(position)
2914 self._old_details = details
2916 def get_visible_color(self, colour):
2917 color_diff1 = abs(Colour.GetRGBA(Colour(colour)) - Colour.GetRGBA(Colour(self._background)))
2918 color_diff2 = abs(Colour.GetRGBA(Colour('gray')) - Colour.GetRGBA(Colour(self._background)))
2919 color_diff3 = abs(Colour.GetRGBA(Colour(colour)) - Colour.GetRGBA(Colour('gray')))
2920 if color_diff1 > color_diff2:
2921 return Colour(colour)
2922 elif color_diff2 > color_diff3: 2922 ↛ 2924line 2922 didn't jump to line 2924 because the condition on line 2922 was always true
2923 return Colour('gray')
2924 elif color_diff1 < color_diff3:
2925 return Colour(colour)
2926 return Colour('black')
2928 def SetUpEditor(self, tab_size=4, tab_markers=True, m_bg='', m_fg='', caret_fg: Colour = 'BLUE'):
2929 """
2930 This method carries out the work of setting up the Code editor.
2931 It's seperate so as not to clutter up the init code.
2932 """
2933 import keyword
2935 self.SetLexer(stc.STC_LEX_PYTHON)
2936 self.SetKeyWords(0, " ".join(keyword.kwlist))
2938 # Enable folding
2939 self.SetProperty("fold", "1")
2941 # Highlight tab/space mixing (shouldn't be any)
2942 self.SetProperty("tab.timmy.whinge.level", "1")
2944 # Set left and right margins
2945 self.SetMargins(2, 2)
2947 self.SetMarginBackground(1, m_bg)
2948 # Set up the numbers in the margin for margin #1
2949 self.SetMarginType(1, wx.stc.STC_MARGIN_NUMBER)
2950 # Reasonable value for, say, 4-5 digits using a mono font (40 pix)
2951 # self.SetMarginWidth(1, 40)
2953 # Indentation and tab stuff
2954 self.SetIndent(tab_size) # Proscribed indent size for wx
2955 self.SetIndentationGuides(tab_markers) # Show indent guides
2956 self.SetBackSpaceUnIndents(True) # Backspace unindents rather than delete 1 space
2957 self.SetTabIndents(True) # Tab key indents
2958 self.SetTabWidth(tab_size) # Proscribed tab size for wx
2959 self.SetUseTabs(False) # Use spaces rather than tabs, or TabTimmy will complain!
2960 # White space
2961 self.SetViewWhiteSpace(False) # Don't view white space
2963 # EOL: Since we are loading/saving ourselves, and the
2964 # strings will always have \n's in them, set the STC to
2965 # edit them that way.
2966 self.SetEOLMode(wx.stc.STC_EOL_LF)
2967 self.SetViewEOL(False)
2969 # No right-edge mode indicator
2970 self.SetEdgeMode(stc.STC_EDGE_NONE)
2972 # Set up a margin to hold fold markers
2973 self.SetMarginType(2, stc.STC_MARGIN_SYMBOL)
2974 self.SetMarginMask(2, stc.STC_MASK_FOLDERS)
2975 self.SetMarginSensitive(2, True)
2976 self.SetMarginBackground(2, m_bg)
2977 self.SetFoldMarginColour(True, m_bg)
2978 self.SetFoldMarginHiColour(True, m_bg)
2979 self.SetMarginWidth(2, self.TextWidth(stc.STC_STYLE_FOLDDISPLAYTEXT, "MM"))
2981 # Global default style
2982 if wx.Platform == '__WXMSW__': 2982 ↛ 2983line 2982 didn't jump to line 2983 because the condition on line 2982 was never true
2983 self.StyleSetSpec(stc.STC_STYLE_DEFAULT, 'fore:#000000,back:#FFFFFF,face:Space Mono') # Courier New
2984 elif wx.Platform == '__WXMAC__': 2984 ↛ 2991line 2984 didn't jump to line 2991 because the condition on line 2984 was always true
2985 # DEBUG: if this looks fine on Linux too, remove the Mac-specific case
2986 # and use this whenever OS != MSW.
2987 self.StyleSetSpec(stc.STC_STYLE_DEFAULT,
2988 'fore:#000000,back:#FFFFFF,face:Monaco')
2989 else:
2990 # print("DEBUG: Setup on Linux")
2991 defsize = wx.SystemSettings.GetFont(wx.SYS_ANSI_FIXED_FONT).GetPointSize()
2992 # Courier, Space Mono, Source Pro Mono,
2993 self.StyleSetSpec(stc.STC_STYLE_DEFAULT, 'fore:#000000,back:#FFFFFF,face:Hack,size:%d' % defsize)
2994 """
2995 self.StyleSetBackground(stc.STC_STYLE_DEFAULT, Colour(200, 222, 40))
2996 self.StyleSetForeground(stc.STC_STYLE_DEFAULT, Colour(7, 0, 70))
2997 """
2998 # Clear styles and revert to default.
2999 self.StyleClearAll()
3001 # Following style specs only indicate differences from default.
3002 # The rest remains unchanged.
3004 # Line numbers in margin
3005 self.StyleSetSpec(wx.stc.STC_STYLE_LINENUMBER, f'fore:{m_fg},back:{m_bg}')
3006 # Highlighted brace
3007 self.StyleSetSpec(wx.stc.STC_STYLE_BRACELIGHT, 'fore:#00009D,back:#FFFF00')
3008 # Unmatched brace
3009 self.StyleSetSpec(wx.stc.STC_STYLE_BRACEBAD, 'fore:#00009D,back:#FF0000')
3010 # Indentation guide
3011 if tab_markers: 3011 ↛ 3015line 3011 didn't jump to line 3015 because the condition on line 3011 was always true
3012 self.StyleSetSpec(wx.stc.STC_STYLE_INDENTGUIDE, "fore:#CDCDCD")
3014 # Caret color
3015 self.SetCaretForeground(Colour(caret_fg))
3016 # Selection background
3017 # self.SetSelBackground(1, '#66CCFF')
3018 """
3019 self.SetBackgroundColour(Colour(200, 222, 40))
3020 self.SetForegroundColour(Colour(7, 0, 70))
3021 """
3023 self.SetSelBackground(True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHT))
3024 self.SetSelForeground(True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHTTEXT))
3027class FromStringIOPopulator(robotapi.populators.FromFilePopulator):
3029 def populate(self, content: [str, BytesIO], tab_size: int):
3030 try:
3031 if not self._language:
3032 set_lang = shared_memory.ShareableList(name="language")
3033 language = [set_lang[0]]
3034 else:
3035 language = self._language
3036 except AttributeError:
3037 language = ['en']
3038 robotapi.RobotReader(spaces=tab_size, lang=language).read(content, self)
3041class RobotStylizer(object):
3042 def __init__(self, editor, settings, readonly=False, language=None):
3043 self.tokens = {} 1aApCxzuyvwstinmoBklqrcbjefdgh
3044 self.editor = editor 1aApCxzuyvwstinmoBklqrcbjefdgh
3045 self.lexer = None 1aApCxzuyvwstinmoBklqrcbjefdgh
3046 self.settings = settings 1aApCxzuyvwstinmoBklqrcbjefdgh
3047 self._readonly = readonly 1aApCxzuyvwstinmoBklqrcbjefdgh
3048 self._ensure_default_font_is_valid() 1aApCxzuyvwstinmoBklqrcbjefdgh
3049 if language: 3049 ↛ 3055line 3049 didn't jump to line 3055 because the condition on line 3049 was always true1aApCxzuyvwstinmoBklqrcbjefdgh
3050 if isinstance(language, list): 1aApCxzuyvwstinmoBklqrcbjefdgh
3051 self.language = language[0] 1rcbjefdgh
3052 else:
3053 self.language = [language] 1aApCxzuyvwstinmoBklqrcbjd
3054 else:
3055 self.language = ['En']
3056 options = {'language': self.language} 1aApCxzuyvwstinmoBklqrcbjefdgh
3057 # print(f"DEBUG: texteditor.py RobotStylizer _init_ language={self.language}\n")
3058 if robotframeworklexer: 3058 ↛ 3061line 3058 didn't jump to line 3061 because the condition on line 3058 was always true1aApCxzuyvwstinmoBklqrcbjefdgh
3059 self.lexer = robotframeworklexer.RobotFrameworkLexer(**options) 1aApCxzuyvwstinmoBklqrcbjefdgh
3060 else:
3061 self.editor.GetParent().create_syntax_colorization_help()
3062 self.set_styles(self._readonly) 1aApCxzuyvwstinmoBklqrcbjefdgh
3063 PUBLISHER.subscribe(self.on_settings_changed, RideSettingsChanged) 1aApCxzuyvwstinmoBklqrcbjefdgh
3065 def on_settings_changed(self, message):
3066 """Redraw the colors if the color settings are modified"""
3067 section, _ = message.keys
3068 if section == PLUGIN_NAME:
3069 self.set_styles(self._readonly) # DEBUG: When on read-only file changing background color ignores flag
3070 self.editor.autocomplete = self.settings[PLUGIN_NAME].get(AUTO_SUGGESTIONS, False)
3071 caret_colour = self.settings[PLUGIN_NAME].get('setting', 'black')
3072 caret_colour = self.editor.get_visible_color(caret_colour)
3073 self.editor.SetCaretForeground(Colour(caret_colour))
3074 caret_style = self.settings[PLUGIN_NAME].get('caret style', 'block')
3075 caret_style = stc.STC_CARETSTYLE_BLOCK if caret_style.lower() == 'block' else stc.STC_CARETSTYLE_LINE
3076 self.editor.SetCaretStyle(caret_style)
3078 def _font_size(self):
3079 return self.settings[PLUGIN_NAME].get('font size', 10) 1aApCxzuyvwstinmoBklqDrcbjefdgh
3081 def _font_face(self):
3082 return self.settings[PLUGIN_NAME].get('font face', 'Courier New') 1aApCxzuyvwstinmoBklqDrcbjefdgh
3084 def _zoom_factor(self):
3085 return self.settings[PLUGIN_NAME].get(ZOOM_FACTOR, 0) 1aApCxzuyvwstinmoBklqDrcbjefdgh
3087 def set_styles(self, readonly=False):
3088 color_settings = self.settings.get_without_default(PLUGIN_NAME) 1aApCxzuyvwstinmoBklqDrcbjefdgh
3089 background = color_settings.get('background', '#FFFFFF') 1aApCxzuyvwstinmoBklqDrcbjefdgh
3090 if readonly: 1aApCxzuyvwstinmoBklqDrcbjefdgh
3091 h = background.lstrip('#') 1k
3092 if h.upper() == background.upper(): 3092 ↛ 3098line 3092 didn't jump to line 3098 because the condition on line 3092 was always true1k
3093 from wx import ColourDatabase 1k
3094 cdb = ColourDatabase() 1k
3095 bkng = cdb.Find(h.upper()) 1k
3096 bkg = (bkng[0], bkng[1], bkng[2]) 1k
3097 else:
3098 bkg = tuple(int(h[i:i + 2], 16) for i in (0, 2, 4))
3099 if bkg >= (180, 180, 180): 3099 ↛ 3103line 3099 didn't jump to line 3103 because the condition on line 3099 was always true1k
3100 bkg = (max(160, bkg[0] - 80), max(160, bkg[1] - 80), 1k
3101 max(160, bkg[2] - 80))
3102 else:
3103 bkg = (min(255, bkg[0] + 180), min(255, bkg[1] + 180),
3104 min(255, bkg[2] + 180))
3105 background = '#%02X%02X%02X' % bkg 1k
3106 if robotframeworklexer: 3106 ↛ 3154line 3106 didn't jump to line 3154 because the condition on line 3106 was always true1aApCxzuyvwstinmoBklqDrcbjefdgh
3107 styles = { 1aApCxzuyvwstinmoBklqDrcbjefdgh
3108 robotframeworklexer.ARGUMENT: {
3109 'fore': color_settings.get('argument', '#bb8844')
3110 },
3111 robotframeworklexer.COMMENT: {
3112 'fore': color_settings.get('comment', 'black')
3113 },
3114 robotframeworklexer.ERROR: {
3115 'fore': color_settings.get('error', 'black')
3116 },
3117 robotframeworklexer.GHERKIN: {
3118 'fore': color_settings.get('gherkin', 'black')
3119 },
3120 robotframeworklexer.HEADING: {
3121 'fore': color_settings.get('heading', '#999999'),
3122 'bold': 'true'
3123 },
3124 robotframeworklexer.IMPORT: {
3125 'fore': color_settings.get('import', '#555555')
3126 },
3127 robotframeworklexer.KEYWORD: {
3128 'fore': color_settings.get('keyword', '#990000'),
3129 'bold': 'true'
3130 },
3131 robotframeworklexer.SEPARATOR: {
3132 'fore': color_settings.get('separator', 'black')
3133 },
3134 robotframeworklexer.SETTING: {
3135 'fore': color_settings.get('setting', 'black'),
3136 'bold': 'true'
3137 },
3138 robotframeworklexer.SYNTAX: {
3139 'fore': color_settings.get('syntax', 'black')
3140 },
3141 robotframeworklexer.TC_KW_NAME: {
3142 'fore': color_settings.get('tc_kw_name', '#aaaaaa')
3143 },
3144 robotframeworklexer.VARIABLE: {
3145 'fore': color_settings.get('variable', '#008080')
3146 }
3147 }
3148 for index, token in enumerate(styles): 1aApCxzuyvwstinmoBklqDrcbjefdgh
3149 self.tokens[token] = index 1aApCxzuyvwstinmoBklqDrcbjefdgh
3150 self.editor.StyleSetSpec(index, 1aApCxzuyvwstinmoBklqDrcbjefdgh
3151 self._get_style_string(back=background,
3152 **styles[token]))
3153 else:
3154 foreground = color_settings.get('setting', 'black')
3155 self.editor.StyleSetSpec(0, self._get_style_string(back=background,
3156 fore=foreground))
3157 self.editor.StyleSetBackground(wx.stc.STC_STYLE_DEFAULT, background) 1aApCxzuyvwstinmoBklqDrcbjefdgh
3158 self.editor.SetZoom(self._zoom_factor()) 1aApCxzuyvwstinmoBklqDrcbjefdgh
3159 self.editor.Refresh() 1aApCxzuyvwstinmoBklqDrcbjefdgh
3161 def _get_word_and_length(self, current_position):
3162 word = self.editor.GetTextRange(current_position,
3163 self.editor.WordEndPosition(
3164 current_position,
3165 False))
3166 return word, len(word)
3168 def _get_style_string(self, back='#FFFFFF', fore='#000000', bold='', underline=''):
3169 settings = locals() 1aApCxzuyvwstinmoBklqDrcbjefdgh
3170 settings.update(size=self._font_size()) 1aApCxzuyvwstinmoBklqDrcbjefdgh
3171 settings.update(face=self._font_face()) 1aApCxzuyvwstinmoBklqDrcbjefdgh
3172 return ','.join('%s:%s' % (name, value) 1aApCxzuyvwstinmoBklqDrcbjefdgh
3173 for name, value in settings.items() if value)
3175 def _ensure_default_font_is_valid(self):
3176 """Checks if default font is installed"""
3177 default_font = self._font_face() 1aApCxzuyvwstinmoBklqrcbjefdgh
3178 if default_font not in read_fonts(): 3178 ↛ 3179line 3178 didn't jump to line 3179 because the condition on line 3178 was never true1aApCxzuyvwstinmoBklqrcbjefdgh
3179 sys_font = wx.SystemSettings.GetFont(wx.SYS_ANSI_FIXED_FONT)
3180 self.settings[PLUGIN_NAME]['font face'] = sys_font.GetFaceName()
3182 def stylize(self):
3183 # print(f"DEBUG: texteditor.py RobotStylizer stylize ENTER lexer={self.lexer}")
3184 if not self.lexer: 3184 ↛ 3185line 3184 didn't jump to line 3185 because the condition on line 3184 was never true1aApCxzuyvwstinmoBklqDrcbjefdgh
3185 return
3186 self.editor.ConvertEOLs(2) 1aApCxzuyvwstinmoBklqDrcbjefdgh
3187 shift = 0 1aApCxzuyvwstinmoBklqDrcbjefdgh
3188 for position, token, value in self.lexer.get_tokens_unprocessed(self.editor.GetText()): 1aApCxzuyvwstinmoBklqDrcbjefdgh
3189 # print(f"DEBUG: texteditor.py RobotStylizer stylize token={token} value={value}")
3190 if wx.VERSION < (4, 1, 0): 3190 ↛ 3191line 3190 didn't jump to line 3191 because the condition on line 3190 was never true1aApCxzuyvwstinmoBklqrcbjefdgh
3191 self.editor.StartStyling(position + shift, 31)
3192 else:
3193 self.editor.StartStyling(position + shift) 1aApCxzuyvwstinmoBklqrcbjefdgh
3194 try: 1aApCxzuyvwstinmoBklqrcbjefdgh
3195 self.editor.SetStyling(len(value.encode('utf-8')), self.tokens[token]) 1aApCxzuyvwstinmoBklqrcbjefdgh
3196 shift += len(value.encode('utf-8')) - len(value) 1aApCxzuyvwstinmoBklqrcbjefdgh
3197 except UnicodeEncodeError:
3198 self.editor.SetStyling(len(value), self.tokens[token])
3199 shift += len(value) - len(value)