1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25
26 /**
27 * @brief The Highlighter class pre-highlights Python source using simple scanner.
28 *
29 * Highlighter doesn't highlight user types (classes and enumerations), syntax
30 * and semantic errors, unnecessary code, etc. It's implements only
31 * basic highlight mechanism.
32 *
33 * Main highlight procedure is highlightBlock().
34 */
35
36 #include "pythonhighlighter.h"
37 #include "pythonscanner.h"
38
39 #include <texteditor/textdocument.h>
40 #include <texteditor/textdocumentlayout.h>
41 #include <texteditor/texteditorconstants.h>
42 #include <utils/qtcassert.h>
43
44 namespace Python {
45 namespace Internal {
46
47 /**
48 * @class PythonEditor::Internal::PythonHighlighter
49 * @brief Handles incremental lexical highlighting, but not semantic
50 *
51 * Incremental lexical highlighting works every time when any character typed
52 * or some text inserted (i.e. copied & pasted).
53 * Each line keeps associated scanner state - integer number. This state is the
54 * scanner context for next line. For example, 3 quotes begin a multiline
55 * string, and each line up to next 3 quotes has state 'MultiLineString'.
56 *
57 * @code
58 * def __init__: # Normal
59 * self.__doc__ = """ # MultiLineString (next line is inside)
60 * banana # MultiLineString
61 * """ # Normal
62 * @endcode
63 */
64
styleForFormat(int format)65 static TextEditor::TextStyle styleForFormat(int format)
66 {
67 using namespace TextEditor;
68 const auto f = Format(format);
69 switch (f) {
70 case Format_Number: return C_NUMBER;
71 case Format_String: return C_STRING;
72 case Format_Keyword: return C_KEYWORD;
73 case Format_Type: return C_TYPE;
74 case Format_ClassField: return C_FIELD;
75 case Format_MagicAttr: return C_JS_SCOPE_VAR;
76 case Format_Operator: return C_OPERATOR;
77 case Format_Comment: return C_COMMENT;
78 case Format_Doxygen: return C_DOXYGEN_COMMENT;
79 case Format_Identifier: return C_TEXT;
80 case Format_Whitespace: return C_VISUAL_WHITESPACE;
81 case Format_ImportedModule: return C_STRING;
82 case Format_LParen: return C_OPERATOR;
83 case Format_RParen: return C_OPERATOR;
84 case Format_FormatsAmount:
85 QTC_CHECK(false); // should never get here
86 return C_TEXT;
87 }
88 QTC_CHECK(false); // should never get here
89 return C_TEXT;
90 }
91
PythonHighlighter()92 PythonHighlighter::PythonHighlighter()
93 {
94 setTextFormatCategories(Format_FormatsAmount, styleForFormat);
95 }
96
97 /**
98 * @brief PythonHighlighter::highlightBlock highlights single line of Python code
99 * @param text is single line without EOLN symbol. Access to all block data
100 * can be obtained through inherited currentBlock() function.
101 *
102 * This function receives state (int number) from previously highlighted block,
103 * scans block using received state and sets initial highlighting for current
104 * block. At the end, it saves internal state in current block.
105 */
highlightBlock(const QString & text)106 void PythonHighlighter::highlightBlock(const QString &text)
107 {
108 int initialState = previousBlockState();
109 if (initialState == -1)
110 initialState = 0;
111 setCurrentBlockState(highlightLine(text, initialState));
112 }
113
114 /**
115 * @return True if this keyword is acceptable at start of import line
116 */
isImportKeyword(const QString & keyword)117 static bool isImportKeyword(const QString &keyword)
118 {
119 return keyword == "import" || keyword == "from";
120 }
121
indent(const QString & line)122 static int indent(const QString &line)
123 {
124 for (int i = 0, size = line.size(); i < size; ++i) {
125 if (!line.at(i).isSpace())
126 return i;
127 }
128 return -1;
129 }
130
setFoldingIndent(const QTextBlock & block,int indent)131 static void setFoldingIndent(const QTextBlock &block, int indent)
132 {
133 if (TextEditor::TextBlockUserData *userData = TextEditor::TextDocumentLayout::userData(block)) {
134 userData->setFoldingIndent(indent);
135 userData->setFoldingStartIncluded(false);
136 userData->setFoldingEndIncluded(false);
137 }
138 }
139
140 /**
141 * @brief Highlight line of code, returns new block state
142 * @param text Source code to highlight
143 * @param initialState Initial state of scanner, retrieved from previous block
144 * @return Final state of scanner, should be saved with current block
145 */
highlightLine(const QString & text,int initialState)146 int PythonHighlighter::highlightLine(const QString &text, int initialState)
147 {
148 Scanner scanner(text.constData(), text.size());
149 scanner.setState(initialState);
150
151 const int pos = indent(text);
152 if (pos < 0) {
153 // Empty lines do not change folding indent
154 setFoldingIndent(currentBlock(), m_lastIndent);
155 } else {
156 m_lastIndent = pos;
157 if (pos == 0 && text.startsWith('#') && !text.startsWith("#!")) {
158 // A comment block at indentation 0. Fold on first line.
159 setFoldingIndent(currentBlock(), withinLicenseHeader ? 1 : 0);
160 withinLicenseHeader = true;
161 } else {
162 // Normal Python code. Line indentation can be used as folding indent.
163 setFoldingIndent(currentBlock(), m_lastIndent);
164 withinLicenseHeader = false;
165 }
166 }
167
168 FormatToken tk;
169 TextEditor::Parentheses parentheses;
170 bool hasOnlyWhitespace = true;
171 while (!(tk = scanner.read()).isEndOfBlock()) {
172 Format format = tk.format();
173 if (format == Format_Keyword && isImportKeyword(scanner.value(tk)) && hasOnlyWhitespace) {
174 setFormat(tk.begin(), tk.length(), formatForCategory(format));
175 highlightImport(scanner);
176 } else if (format == Format_Comment
177 || format == Format_String
178 || format == Format_Doxygen) {
179 setFormatWithSpaces(text, tk.begin(), tk.length(), formatForCategory(format));
180 } else {
181 if (format == Format_LParen) {
182 parentheses.append(TextEditor::Parenthesis(TextEditor::Parenthesis::Opened,
183 text.at(tk.begin()), tk.begin()));
184 } else if (format == Format_RParen) {
185 parentheses.append(TextEditor::Parenthesis(TextEditor::Parenthesis::Closed,
186 text.at(tk.begin()), tk.begin()));
187 }
188 setFormat(tk.begin(), tk.length(), formatForCategory(format));
189 }
190
191 if (format != Format_Whitespace)
192 hasOnlyWhitespace = false;
193 }
194 TextEditor::TextDocumentLayout::setParentheses(currentBlock(), parentheses);
195 return scanner.state();
196 }
197
198 /**
199 * @brief Highlights rest of line as import directive
200 */
highlightImport(Scanner & scanner)201 void PythonHighlighter::highlightImport(Scanner &scanner)
202 {
203 FormatToken tk;
204 while (!(tk = scanner.read()).isEndOfBlock()) {
205 Format format = tk.format();
206 if (tk.format() == Format_Identifier)
207 format = Format_ImportedModule;
208 setFormat(tk.begin(), tk.length(), formatForCategory(format));
209 }
210 }
211
212 } // namespace Internal
213 } // namespace Python
214