fix own regression in 2.66 [#34610] Text editor: Syntax highlighting freezes
[blender.git] / source / blender / editors / space_text / text_format_py.c
1 /*
2  * ***** BEGIN GPL LICENSE BLOCK *****
3  *
4  * This program is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU General Public License
6  * as published by the Free Software Foundation; either version 2
7  * of the License, or (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * GNU General Public License for more details.
12  *
13  * You should have received a copy of the GNU General Public License
14  * along with this program; if not, write to the Free Software Foundation,
15  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16  *
17  * The Original Code is Copyright (C) 2001-2002 by NaN Holding BV.
18  * All rights reserved.
19  *
20  * The Original Code is: all of this file.
21  *
22  * Contributor(s): none yet.
23  *
24  * ***** END GPL LICENSE BLOCK *****
25  */
26
27 /** \file blender/editors/space_text/text_format_py.c
28  *  \ingroup sptext
29  */
30
31 #include <string.h>
32
33 #include "BLI_blenlib.h"
34
35 #include "DNA_text_types.h"
36 #include "DNA_space_types.h"
37
38 #include "BKE_text.h"
39
40 #include "text_format.h"
41
42 /* *** Local Functions (for format_line) *** */
43
44 /* Checks the specified source string for a Python built-in function name. This
45  * name must start at the beginning of the source string and must be followed by
46  * a non-identifier (see text_check_identifier(char)) or null character.
47  *
48  * If a built-in function is found, the length of the matching name is returned.
49  * Otherwise, -1 is returned.
50  *
51  * See:
52  * http://docs.python.org/py3k/reference/lexical_analysis.html#keywords
53  */
54
55 static int txtfmt_py_find_builtinfunc(const char *string)
56 {
57         int i, len;
58         /* list is from...
59          * ", ".join(['"%s"' % kw
60          *            for kw in  __import__("keyword").kwlist
61          *            if kw not in {"False", "None", "True", "def", "class"}])
62          *
63          * ... and for this code:
64          * print("\n".join(['else if (STR_LITERAL_STARTSWITH(string, "%s", len)) i = len;' % kw
65          *                  for kw in  __import__("keyword").kwlist
66          *                  if kw not in {"False", "None", "True", "def", "class"}]))
67          */
68
69         if      (STR_LITERAL_STARTSWITH(string, "and",      len)) i = len;
70         else if (STR_LITERAL_STARTSWITH(string, "as",       len)) i = len;
71         else if (STR_LITERAL_STARTSWITH(string, "assert",   len)) i = len;
72         else if (STR_LITERAL_STARTSWITH(string, "break",    len)) i = len;
73         else if (STR_LITERAL_STARTSWITH(string, "continue", len)) i = len;
74         else if (STR_LITERAL_STARTSWITH(string, "del",      len)) i = len;
75         else if (STR_LITERAL_STARTSWITH(string, "elif",     len)) i = len;
76         else if (STR_LITERAL_STARTSWITH(string, "else",     len)) i = len;
77         else if (STR_LITERAL_STARTSWITH(string, "except",   len)) i = len;
78         else if (STR_LITERAL_STARTSWITH(string, "finally",  len)) i = len;
79         else if (STR_LITERAL_STARTSWITH(string, "for",      len)) i = len;
80         else if (STR_LITERAL_STARTSWITH(string, "from",     len)) i = len;
81         else if (STR_LITERAL_STARTSWITH(string, "global",   len)) i = len;
82         else if (STR_LITERAL_STARTSWITH(string, "if",       len)) i = len;
83         else if (STR_LITERAL_STARTSWITH(string, "import",   len)) i = len;
84         else if (STR_LITERAL_STARTSWITH(string, "in",       len)) i = len;
85         else if (STR_LITERAL_STARTSWITH(string, "is",       len)) i = len;
86         else if (STR_LITERAL_STARTSWITH(string, "lambda",   len)) i = len;
87         else if (STR_LITERAL_STARTSWITH(string, "nonlocal", len)) i = len;
88         else if (STR_LITERAL_STARTSWITH(string, "not",      len)) i = len;
89         else if (STR_LITERAL_STARTSWITH(string, "or",       len)) i = len;
90         else if (STR_LITERAL_STARTSWITH(string, "pass",     len)) i = len;
91         else if (STR_LITERAL_STARTSWITH(string, "raise",    len)) i = len;
92         else if (STR_LITERAL_STARTSWITH(string, "return",   len)) i = len;
93         else if (STR_LITERAL_STARTSWITH(string, "try",      len)) i = len;
94         else if (STR_LITERAL_STARTSWITH(string, "while",    len)) i = len;
95         else if (STR_LITERAL_STARTSWITH(string, "with",     len)) i = len;
96         else if (STR_LITERAL_STARTSWITH(string, "yield",    len)) i = len;
97         else                                                      i = 0;
98
99         /* If next source char is an identifier (eg. 'i' in "definate") no match */
100         if (i == 0 || text_check_identifier(string[i]))
101                 return -1;
102         return i;
103 }
104
105 /* Checks the specified source string for a Python special name. This name must
106  * start at the beginning of the source string and must be followed by a non-
107  * identifier (see text_check_identifier(char)) or null character.
108  *
109  * If a special name is found, the length of the matching name is returned.
110  * Otherwise, -1 is returned. */
111
112 static int txtfmt_py_find_specialvar(const char *string)
113 {
114         int i, len;
115
116         if      (STR_LITERAL_STARTSWITH(string, "def", len))   i = len;
117         else if (STR_LITERAL_STARTSWITH(string, "class", len)) i = len;
118         else                                                   i = 0;
119
120         /* If next source char is an identifier (eg. 'i' in "definate") no match */
121         if (i == 0 || text_check_identifier(string[i]))
122                 return -1;
123         return i;
124 }
125
126 static int txtfmt_py_find_decorator(const char *string)
127 {
128         if (string[0] == '@') {
129                 int i = 1;
130                 /* Whitespace is ok '@  foo' */
131                 while (text_check_whitespace(string[i])) {
132                         i++;
133                 }
134                 while (text_check_identifier(string[i])) {
135                         i++;
136                 }
137                 return i;
138         }
139         return -1;
140 }
141
142 static int txtfmt_py_find_bool(const char *string)
143 {
144         int i, len;
145
146         if      (STR_LITERAL_STARTSWITH(string, "None",  len))  i = len;
147         else if (STR_LITERAL_STARTSWITH(string, "True",  len))  i = len;
148         else if (STR_LITERAL_STARTSWITH(string, "False", len))  i = len;
149         else                                                    i = 0;
150
151         /* If next source char is an identifier (eg. 'i' in "Nonetheless") no match */
152         if (i == 0 || text_check_identifier(string[i]))
153                 return -1;
154         return i;
155 }
156
157 static char txtfmt_py_format_identifier(const char *str)
158 {
159         char fmt;
160         if      ((txtfmt_py_find_specialvar(str))   != -1) fmt = FMT_TYPE_SPECIAL;
161         else if ((txtfmt_py_find_builtinfunc(str))  != -1) fmt = FMT_TYPE_KEYWORD;
162         else if ((txtfmt_py_find_decorator(str))    != -1) fmt = FMT_TYPE_RESERVED;
163         else                                               fmt = FMT_TYPE_DEFAULT;
164         return fmt;
165 }
166
167 static void txtfmt_py_format_line(SpaceText *st, TextLine *line, const int do_next)
168 {
169         FlattenString fs;
170         const char *str;
171         char *fmt;
172         char cont_orig, cont, find, prev = ' ';
173         int len, i;
174
175         /* Get continuation from previous line */
176         if (line->prev && line->prev->format != NULL) {
177                 fmt = line->prev->format;
178                 cont = fmt[strlen(fmt) + 1]; /* Just after the null-terminator */
179                 BLI_assert((FMT_CONT_ALL & cont) == cont);
180         }
181         else {
182                 cont = FMT_CONT_NOP;
183         }
184
185         /* Get original continuation from this line */
186         if (line->format != NULL) {
187                 fmt = line->format;
188                 cont_orig = fmt[strlen(fmt) + 1]; /* Just after the null-terminator */
189                 BLI_assert((FMT_CONT_ALL & cont_orig) == cont_orig);
190         }
191         else {
192                 cont_orig = 0xFF;
193         }
194
195         len = flatten_string(st, &fs, line->line);
196         str = fs.buf;
197         if (!text_check_format_len(line, len)) {
198                 flatten_string_free(&fs);
199                 return;
200         }
201         fmt = line->format;
202
203         while (*str) {
204                 /* Handle escape sequences by skipping both \ and next char */
205                 if (*str == '\\') {
206                         *fmt = prev; fmt++; str++;
207                         if (*str == '\0') break;
208                         *fmt = prev; fmt++; str += BLI_str_utf8_size_safe(str);
209                         continue;
210                 }
211                 /* Handle continuations */
212                 else if (cont) {
213                         /* Triple strings ("""...""" or '''...''') */
214                         if (cont & FMT_CONT_TRIPLE) {
215                                 find = (cont & FMT_CONT_QUOTEDOUBLE) ? '"' : '\'';
216                                 if (*str == find && *(str + 1) == find && *(str + 2) == find) {
217                                         *fmt = FMT_TYPE_STRING; fmt++; str++;
218                                         *fmt = FMT_TYPE_STRING; fmt++; str++;
219                                         cont = FMT_CONT_NOP;
220                                 }
221                                 /* Handle other strings */
222                         }
223                         else {
224                                 find = (cont & FMT_CONT_QUOTEDOUBLE) ? '"' : '\'';
225                                 if (*str == find) cont = FMT_CONT_NOP;
226                         }
227
228                         *fmt = FMT_TYPE_STRING;
229                         str += BLI_str_utf8_size_safe(str) - 1;
230                 }
231                 /* Not in a string... */
232                 else {
233                         /* Deal with comments first */
234                         if (*str == '#') {
235                                 /* fill the remaining line */
236                                 text_format_fill(&str, &fmt, FMT_TYPE_COMMENT, len - (int)(fmt - line->format));
237                         }
238                         else if (*str == '"' || *str == '\'') {
239                                 /* Strings */
240                                 find = *str;
241                                 cont = (*str == '"') ? FMT_CONT_QUOTEDOUBLE : FMT_CONT_QUOTESINGLE;
242                                 if (*(str + 1) == find && *(str + 2) == find) {
243                                         *fmt = FMT_TYPE_STRING; fmt++; str++;
244                                         *fmt = FMT_TYPE_STRING; fmt++; str++;
245                                         cont |= FMT_CONT_TRIPLE;
246                                 }
247                                 *fmt = FMT_TYPE_STRING;
248                         }
249                         /* Whitespace (all ws. has been converted to spaces) */
250                         else if (*str == ' ') {
251                                 *fmt = FMT_TYPE_WHITESPACE;
252                         }
253                         /* Numbers (digits not part of an identifier and periods followed by digits) */
254                         else if ((prev != FMT_TYPE_DEFAULT && text_check_digit(*str)) ||
255                                  (*str == '.' && text_check_digit(*(str + 1))))
256                         {
257                                 *fmt = FMT_TYPE_NUMERAL;
258                         }
259                         /* Booleans */
260                         else if (prev != FMT_TYPE_DEFAULT && (i = txtfmt_py_find_bool(str)) != -1) {
261                                 if (i > 0) {
262                                         text_format_fill_ascii(&str, &fmt, FMT_TYPE_NUMERAL, i);
263                                 }
264                                 else {
265                                         str += BLI_str_utf8_size_safe(str) - 1;
266                                         *fmt = FMT_TYPE_DEFAULT;
267                                 }
268                         }
269                         /* Punctuation */
270                         else if ((*str != '@') && text_check_delim(*str)) {
271                                 *fmt = FMT_TYPE_SYMBOL;
272                         }
273                         /* Identifiers and other text (no previous ws. or delims. so text continues) */
274                         else if (prev == FMT_TYPE_DEFAULT) {
275                                 str += BLI_str_utf8_size_safe(str) - 1;
276                                 *fmt = FMT_TYPE_DEFAULT;
277                         }
278                         /* Not ws, a digit, punct, or continuing text. Must be new, check for special words */
279                         else {
280                                 /* Special vars(v) or built-in keywords(b) */
281                                 /* keep in sync with 'txtfmt_py_format_identifier()' */
282                                 if      ((i = txtfmt_py_find_specialvar(str))   != -1) prev = FMT_TYPE_SPECIAL;
283                                 else if ((i = txtfmt_py_find_builtinfunc(str))  != -1) prev = FMT_TYPE_KEYWORD;
284                                 else if ((i = txtfmt_py_find_decorator(str))    != -1) prev = FMT_TYPE_DIRECTIVE;
285
286                                 if (i > 0) {
287                                         if (prev == FMT_TYPE_DIRECTIVE) {  /* can contain utf8 */
288                                                 text_format_fill(&str, &fmt, prev, i);
289                                         }
290                                         else {
291                                                 text_format_fill_ascii(&str, &fmt, prev, i);
292                                         }
293                                 }
294                                 else {
295                                         str += BLI_str_utf8_size_safe(str) - 1;
296                                         *fmt = FMT_TYPE_DEFAULT;
297                                 }
298                         }
299                 }
300                 prev = *fmt; fmt++; str++;
301         }
302
303         /* Terminate and add continuation char */
304         *fmt = '\0'; fmt++;
305         *fmt = cont;
306
307         /* If continuation has changed and we're allowed, process the next line */
308         if (cont != cont_orig && do_next && line->next) {
309                 txtfmt_py_format_line(st, line->next, do_next);
310         }
311
312         flatten_string_free(&fs);
313 }
314
315 void ED_text_format_register_py(void)
316 {
317         static TextFormatType tft = {0};
318         static const char *ext[] = {"py", NULL};
319
320         tft.format_identifier = txtfmt_py_format_identifier;
321         tft.format_line       = txtfmt_py_format_line;
322         tft.ext = ext;
323
324         ED_text_format_register(&tft);
325 }