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