]> Untitled Git - wolf-seeking-sheep.git/blob - addons/dialogic/Editor/TimelineEditor/TextEditor/CodeCompletionHelper.gd
Updated export config options
[wolf-seeking-sheep.git] / addons / dialogic / Editor / TimelineEditor / TextEditor / CodeCompletionHelper.gd
1 @tool
2 extends Node
3
4 enum Modes {TEXT_EVENT_ONLY, FULL_HIGHLIGHTING}
5
6 var syntax_highlighter: SyntaxHighlighter = load("res://addons/dialogic/Editor/TimelineEditor/TextEditor/syntax_highlighter.gd").new()
7 var text_syntax_highlighter: SyntaxHighlighter = load("res://addons/dialogic/Editor/TimelineEditor/TextEditor/syntax_highlighter.gd").new()
8
9
10 # These RegEx's are used to deduce information from the current line for auto-completion
11
12 # To find the currently typed word and the symbol before
13 var completion_word_regex := RegEx.new()
14 # To find the shortcode of the current shortcode event (basically the type)
15 var completion_shortcode_getter_regex := RegEx.new()
16 # To find the parameter name of the current if typing a value
17 var completion_shortcode_param_getter_regex := RegEx.new()
18 # To find the value of a paramater that is being typed
19 var completion_shortcode_value_regex := RegEx.new()
20
21 # Stores references to all shortcode events for parameter and value suggestions
22 var shortcode_events := {}
23 var custom_syntax_events := []
24 var text_event: DialogicTextEvent = null
25
26 func _ready() -> void:
27         # Compile RegEx's
28         completion_word_regex.compile("(?<s>(\\W)|^)(?<word>\\w*)\\x{FFFF}")
29         completion_shortcode_getter_regex.compile("\\[(?<code>\\w*)")
30         completion_shortcode_param_getter_regex.compile("(?<param>\\w*)\\W*=\\s*\"?(\\w|\\s)*"+String.chr(0xFFFF))
31         completion_shortcode_value_regex.compile(r'(\[|\s)[^\[\s=]*="(?<value>[^"$]*)'+String.chr(0xFFFF))
32
33         text_syntax_highlighter.mode = text_syntax_highlighter.Modes.TEXT_EVENT_ONLY
34
35 #region AUTO COMPLETION
36 ################################################################################
37
38 # Helper that gets the current line with a special character where the caret is
39 func get_code_completion_line(text:CodeEdit) -> String:
40         return text.get_line(text.get_caret_line()).insert(text.get_caret_column(), String.chr(0xFFFF)).strip_edges()
41
42
43 # Helper that gets the currently typed word
44 func get_code_completion_word(text:CodeEdit) -> String:
45         var result := completion_word_regex.search(get_code_completion_line(text))
46         return result.get_string('word') if result else ""
47
48 # Helper that gets the currently typed parameter
49 func get_code_completion_parameter_value(text:CodeEdit) -> String:
50         var result := completion_shortcode_value_regex.search(get_code_completion_line(text))
51         return result.get_string('value') if result else ""
52
53
54 # Helper that gets the symbol before the current word
55 func get_code_completion_prev_symbol(text:CodeEdit) -> String:
56         var result := completion_word_regex.search(get_code_completion_line(text))
57         return result.get_string('s') if result else ""
58
59
60 func get_line_untill_caret(line:String) -> String:
61         return line.substr(0, line.find(String.chr(0xFFFF)))
62
63
64 # Called if something was typed
65 # Adds all kinds of options depending on the
66 #   content of the current line, the last word and the symbol that came before
67 # Triggers opening of the popup
68 func request_code_completion(force:bool, text:CodeEdit, mode:=Modes.FULL_HIGHLIGHTING) -> void:
69         ## TODO remove this once https://github.com/godotengine/godot/issues/38560 is fixed
70         if mode != Modes.FULL_HIGHLIGHTING:
71                 return
72
73         # make sure shortcode event references are loaded
74         if mode == Modes.FULL_HIGHLIGHTING:
75                 var hidden_events: Array = DialogicUtil.get_editor_setting('hidden_event_buttons', [])
76                 if shortcode_events.is_empty():
77                         for event in DialogicResourceUtil.get_event_cache():
78                                 if event.get_shortcode() != 'default_shortcode':
79                                         shortcode_events[event.get_shortcode()] = event
80
81                                 else:
82                                         custom_syntax_events.append(event)
83                                 if event.event_name in hidden_events:
84                                         event.set_meta('hidden', true)
85                                 if event is DialogicTextEvent:
86                                         text_event = event
87                                         # this is done to force-load the text effects regex which is used below
88                                         event.load_text_effects()
89
90         # fill helpers
91         var line := get_code_completion_line(text)
92         var word := get_code_completion_word(text)
93         var symbol := get_code_completion_prev_symbol(text)
94         var line_part := get_line_untill_caret(line)
95
96         ## Note on use of KIND types for options.
97         # These types are mostly useless for us.
98         # However I decidede to assign some special cases for them:
99         # - KIND_PLAIN_TEXT is only shown if the beginnging of the option is already typed
100                         # !word.is_empty() and option.begins_with(word)
101         # - KIND_CLASS is only shown if anything from the options is already typed
102                         # !word.is_empty() and word in option
103         # - KIND_CONSTANT is shown and checked against the beginning
104                         # option.begins_with(word)
105         # - KIND_MEMBER is shown and searched completely
106                         # word in option
107
108         ## Note on VALUE key
109         # The value key is used to store a potential closing string for the completion.
110         # The completion will check if the string is already present and add it otherwise.
111
112         # Shortcode event suggestions
113         if mode == Modes.FULL_HIGHLIGHTING and syntax_highlighter.line_is_shortcode_event(text.get_caret_line()):
114                 if symbol == '[':
115                         # suggest shortcodes if a shortcode event has just begun
116                         var shortcodes := shortcode_events.keys()
117                         shortcodes.sort()
118                         for shortcode in shortcodes:
119                                 if shortcode_events[shortcode].get_meta('hidden', false):
120                                         continue
121                                 if shortcode_events[shortcode].get_shortcode_parameters().is_empty():
122                                         text.add_code_completion_option(CodeEdit.KIND_MEMBER, shortcode, shortcode, shortcode_events[shortcode].event_color.lerp(syntax_highlighter.normal_color, 0.3), shortcode_events[shortcode]._get_icon())
123                                 else:
124                                         text.add_code_completion_option(CodeEdit.KIND_MEMBER, shortcode, shortcode+" ", shortcode_events[shortcode].event_color.lerp(syntax_highlighter.normal_color, 0.3), shortcode_events[shortcode]._get_icon())
125                 else:
126                         var full_event_text: String = syntax_highlighter.get_full_event(text.get_caret_line())
127                         var current_shortcode := completion_shortcode_getter_regex.search(full_event_text)
128                         if !current_shortcode:
129                                 text.update_code_completion_options(false)
130                                 return
131
132                         var code := current_shortcode.get_string('code')
133                         if !code in shortcode_events.keys():
134                                 text.update_code_completion_options(false)
135                                 return
136
137                         # suggest parameters
138                         if symbol == ' ' and line.count('"')%2 == 0:
139                                 var parameters: Array = shortcode_events[code].get_shortcode_parameters().keys()
140                                 for param in parameters:
141                                         if !param+'=' in full_event_text:
142                                                 text.add_code_completion_option(CodeEdit.KIND_MEMBER, param, param+'="' , shortcode_events[code].event_color.lerp(syntax_highlighter.normal_color, 0.3), text.get_theme_icon("MemberProperty", "EditorIcons"))
143
144                         # suggest values
145                         elif symbol == '=' or symbol == '"':
146                                 var current_parameter_gex := completion_shortcode_param_getter_regex.search(line)
147                                 if !current_parameter_gex:
148                                         text.update_code_completion_options(false)
149                                         return
150
151                                 var current_parameter := current_parameter_gex.get_string('param')
152                                 if !shortcode_events[code].get_shortcode_parameters().has(current_parameter):
153                                         text.update_code_completion_options(false)
154                                         return
155                                 if !shortcode_events[code].get_shortcode_parameters()[current_parameter].has('suggestions'):
156                                         if typeof(shortcode_events[code].get_shortcode_parameters()[current_parameter].default) == TYPE_BOOL:
157                                                 suggest_bool(text, shortcode_events[code].event_color.lerp(syntax_highlighter.normal_color, 0.3))
158                                         elif len(word) > 0:
159                                                 text.add_code_completion_option(CodeEdit.KIND_VARIABLE, word, word, shortcode_events[code].event_color.lerp(syntax_highlighter.normal_color, 0.3), text.get_theme_icon("GuiScrollArrowRight", "EditorIcons"), '" ')
160                                         text.update_code_completion_options(true)
161                                         return
162
163                                 var suggestions: Dictionary = shortcode_events[code].get_shortcode_parameters()[current_parameter]['suggestions'].call()
164                                 suggest_custom_suggestions(suggestions, text, shortcode_events[code].event_color.lerp(syntax_highlighter.normal_color, 0.3))
165
166                 # Force update and showing of the popup
167                 text.update_code_completion_options(true)
168                 return
169
170
171         for event in custom_syntax_events:
172                 if mode == Modes.TEXT_EVENT_ONLY and !event is DialogicTextEvent:
173                         continue
174
175                 if ! ' ' in line_part:
176                         event._get_start_code_completion(self, text)
177
178                 if event.is_valid_event(line):
179                         event._get_code_completion(self, text, line, word, symbol)
180                         break
181
182         # Force update and showing of the popup
183         text.update_code_completion_options(true)
184
185
186
187 # Helper that adds all characters as options
188 func suggest_characters(text:CodeEdit, type := CodeEdit.KIND_MEMBER, text_event_start:=false) -> void:
189         for character in DialogicResourceUtil.get_character_directory():
190                 var result: String = character
191                 if " " in character:
192                         result = '"'+character+'"'
193                 if text_event_start and load(DialogicResourceUtil.get_character_directory()[character]).portraits.is_empty():
194                         result += ':'
195                 text.add_code_completion_option(type, character, result, syntax_highlighter.character_name_color, load("res://addons/dialogic/Editor/Images/Resources/character.svg"))
196
197
198 # Helper that adds all timelines as options
199 func suggest_timelines(text:CodeEdit, type := CodeEdit.KIND_MEMBER, color:=Color()) -> void:
200         for timeline in DialogicResourceUtil.get_timeline_directory():
201                 text.add_code_completion_option(type, timeline, timeline+'/', color, text.get_theme_icon("TripleBar", "EditorIcons"))
202
203
204 func suggest_labels(text:CodeEdit, timeline:String='', end:='', color:=Color()) -> void:
205         if timeline in DialogicResourceUtil.get_label_cache():
206                 for i in DialogicResourceUtil.get_label_cache()[timeline]:
207                         text.add_code_completion_option(CodeEdit.KIND_MEMBER, i, i+end, color, load("res://addons/dialogic/Modules/Jump/icon_label.png"))
208
209
210 # Helper that adds all portraits of a given character as options
211 func suggest_portraits(text:CodeEdit, character_name:String, end_check:=')') -> void:
212         if !character_name in DialogicResourceUtil.get_character_directory():
213                 return
214         var character_resource: DialogicCharacter = load(DialogicResourceUtil.get_character_directory()[character_name])
215         for portrait in character_resource.portraits:
216                 text.add_code_completion_option(CodeEdit.KIND_MEMBER, portrait, portrait, syntax_highlighter.character_portrait_color, load("res://addons/dialogic/Editor/Images/Resources/character.svg"), end_check)
217         if character_resource.portraits.is_empty():
218                 text.add_code_completion_option(CodeEdit.KIND_MEMBER, 'Has no portraits!', '', syntax_highlighter.character_portrait_color, load("res://addons/dialogic/Editor/Images/Pieces/warning.svg"))
219
220
221 # Helper that adds all variable paths as options
222 func suggest_variables(text:CodeEdit):
223         for variable in DialogicUtil.list_variables(ProjectSettings.get_setting('dialogic/variables')):
224                 text.add_code_completion_option(CodeEdit.KIND_MEMBER, variable, variable, syntax_highlighter.variable_color, text.get_theme_icon("MemberProperty", "EditorIcons"), '}')
225
226
227 # Helper that adds true and false as options
228 func suggest_bool(text:CodeEdit, color:Color):
229         text.add_code_completion_option(CodeEdit.KIND_VARIABLE, 'true', 'true', color, text.get_theme_icon("GuiChecked", "EditorIcons"), '" ')
230         text.add_code_completion_option(CodeEdit.KIND_VARIABLE, 'false', 'false', color, text.get_theme_icon("GuiUnchecked", "EditorIcons"), '" ')
231
232
233 func suggest_custom_suggestions(suggestions:Dictionary, text:CodeEdit, color:Color) -> void:
234         for key in suggestions.keys():
235                 if suggestions[key].has('text_alt'):
236                         text.add_code_completion_option(CodeEdit.KIND_VARIABLE, key, suggestions[key].text_alt[0], color, suggestions[key].get('icon', null), '" ')
237                 else:
238                         text.add_code_completion_option(CodeEdit.KIND_VARIABLE, key, str(suggestions[key].value), color, suggestions[key].get('icon', null), '" ')
239
240
241 # Filters the list of all possible options, depending on what was typed
242 # Purpose of the different Kinds is explained in [_request_code_completion]
243 func filter_code_completion_candidates(candidates:Array, text:CodeEdit) -> Array:
244         var valid_candidates := []
245         var current_word := get_code_completion_word(text)
246         for candidate in candidates:
247                 if candidate.kind == text.KIND_PLAIN_TEXT:
248                         if !current_word.is_empty() and candidate.insert_text.begins_with(current_word):
249                                 valid_candidates.append(candidate)
250                 elif candidate.kind == text.KIND_MEMBER:
251                         if current_word.is_empty() or current_word.to_lower() in candidate.insert_text.to_lower():
252                                 valid_candidates.append(candidate)
253                 elif candidate.kind == text.KIND_VARIABLE:
254                         var current_param_value := get_code_completion_parameter_value(text)
255                         if current_param_value.is_empty() or current_param_value.to_lower() in candidate.insert_text.to_lower():
256                                 valid_candidates.append(candidate)
257                 elif candidate.kind == text.KIND_CONSTANT:
258                         if current_word.is_empty() or candidate.insert_text.begins_with(current_word):
259                                 valid_candidates.append(candidate)
260                 elif candidate.kind == text.KIND_CLASS:
261                         if !current_word.is_empty() and current_word.to_lower() in candidate.insert_text.to_lower():
262                                 valid_candidates.append(candidate)
263         return valid_candidates
264
265
266 # Called when code completion was activated
267 # Inserts the selected item
268 func confirm_code_completion(replace:bool, text:CodeEdit) -> void:
269         # Note: I decided to ALWAYS use replace mode, as dialogic is supposed to be beginner friendly
270
271         var code_completion := text.get_code_completion_option(text.get_code_completion_selected_index())
272
273         var word := get_code_completion_word(text)
274         if code_completion.kind == CodeEdit.KIND_VARIABLE:
275                 word = get_code_completion_parameter_value(text)
276
277         text.remove_text(text.get_caret_line(), text.get_caret_column()-len(word), text.get_caret_line(), text.get_caret_column())
278
279         # Something has changed between 4.2 and 4.3
280         # Probably about how carets are reset when text is removed or idk.
281         # To keep compatibility with 4.2 for at least a while this should do the trick:
282         # TODO: Remove once compatibility for 4.2 is dropped.
283         if Engine.get_version_info().hex >= 0x040300:
284                 text.set_caret_column(text.get_caret_column())
285         else:
286                 text.set_caret_column(text.get_caret_column()-len(word))
287
288         text.insert_text_at_caret(code_completion.insert_text)
289
290         if code_completion.has('default_value') and typeof(code_completion['default_value']) == TYPE_STRING:
291                 var next_letter := text.get_line(text.get_caret_line()).substr(text.get_caret_column(), len(code_completion['default_value']))
292                 if next_letter == code_completion['default_value'] or next_letter[0] == code_completion['default_value'][0]:
293                         text.set_caret_column(text.get_caret_column()+1)
294                 else:
295                         text.insert_text_at_caret(code_completion['default_value'])
296
297
298 #endregion
299
300 #region SYMBOL CLICKING
301 ################################################################################
302
303 # Performs an action (like opening a link) when a valid symbol was clicked
304 func symbol_lookup(symbol:String, line:int, column:int) -> void:
305         if symbol in shortcode_events.keys():
306                 if !shortcode_events[symbol].help_page_path.is_empty():
307                         OS.shell_open(shortcode_events[symbol].help_page_path)
308         if symbol in DialogicResourceUtil.get_character_directory():
309                 EditorInterface.edit_resource(DialogicResourceUtil.get_resource_from_identifier(symbol, 'dch'))
310         if symbol in DialogicResourceUtil.get_timeline_directory():
311                 EditorInterface.edit_resource(DialogicResourceUtil.get_resource_from_identifier(symbol, 'dtl'))
312
313
314 # Called to test if a symbol can be clicked
315 func symbol_validate(symbol:String, text:CodeEdit) -> void:
316         if symbol in shortcode_events.keys():
317                 if !shortcode_events[symbol].help_page_path.is_empty():
318                         text.set_symbol_lookup_word_as_valid(true)
319         if symbol in DialogicResourceUtil.get_character_directory():
320                 text.set_symbol_lookup_word_as_valid(true)
321         if symbol in DialogicResourceUtil.get_timeline_directory():
322                 text.set_symbol_lookup_word_as_valid(true)
323
324 #endregion