1 | /* | |
2 | * This file is part of the programmer editor demo | |
3 | * Copyright (C) 2001-2005 Stephen Ostermiller | |
4 | * http://ostermiller.org/contact.pl?regarding=Syntax+Highlighting | |
5 | * | |
6 | * This program is free software; you can redistribute it and/or modify | |
7 | * it under the terms of the GNU General Public License as published by | |
8 | * the Free Software Foundation; either version 2 of the License, or | |
9 | * (at your option) any later version. | |
10 | * | |
11 | * This program is distributed in the hope that it will be useful, | |
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
14 | * GNU General Public License for more details. | |
15 | * | |
16 | * See COPYING.TXT for details. | |
17 | */ | |
18 | package com.jsql.view.swing.sql.lexer; | |
19 | ||
20 | import com.jsql.util.LogLevelUtil; | |
21 | import com.jsql.view.swing.sql.lexer.syntax.Lexer; | |
22 | import com.jsql.view.swing.sql.lexer.syntax.Token; | |
23 | import org.apache.logging.log4j.LogManager; | |
24 | import org.apache.logging.log4j.Logger; | |
25 | ||
26 | import javax.swing.text.AttributeSet; | |
27 | import java.io.IOException; | |
28 | import java.lang.ref.WeakReference; | |
29 | import java.util.*; | |
30 | ||
31 | /** | |
32 | * Run the Syntax Highlighting as a separate thread. Things that need to be | |
33 | * colored are messaged to the thread and put in a list. | |
34 | */ | |
35 | class Colorer extends Thread { | |
36 | | |
37 | /** | |
38 | * Log4j logger sent to view. | |
39 | */ | |
40 | private static final Logger LOGGER = LogManager.getRootLogger(); | |
41 | | |
42 | /** | |
43 | * A simple wrapper representing something that needs to be colored. Placed | |
44 | * into an object so that it can be stored in a Vector. | |
45 | */ | |
46 | private static class RecolorEvent { | |
47 | | |
48 | private int position; | |
49 | | |
50 | private int adjustment; | |
51 | ||
52 | public RecolorEvent(int position, int adjustment) { | |
53 | ||
54 | this.position = position; | |
55 | this.adjustment = adjustment; | |
56 | } | |
57 | ||
58 | public int getPosition() { | |
59 |
1
1. getPosition : replaced int return with 0 for com/jsql/view/swing/sql/lexer/Colorer$RecolorEvent::getPosition → NO_COVERAGE |
return this.position; |
60 | } | |
61 | ||
62 | public void setPosition(int position) { | |
63 | this.position = position; | |
64 | } | |
65 | ||
66 | public int getAdjustment() { | |
67 |
1
1. getAdjustment : replaced int return with 0 for com/jsql/view/swing/sql/lexer/Colorer$RecolorEvent::getAdjustment → NO_COVERAGE |
return this.adjustment; |
68 | } | |
69 | ||
70 | public void setAdjustment(int adjustment) { | |
71 | this.adjustment = adjustment; | |
72 | } | |
73 | } | |
74 | | |
75 | /** | |
76 | * Stores the document we are coloring. We use a WeakReference | |
77 | * so that the document is eligible for garbage collection when | |
78 | * it is no longer being used. At that point, this thread will | |
79 | * shut down itself. | |
80 | */ | |
81 | private final WeakReference<HighlightedDocument> document; | |
82 | ||
83 | /** | |
84 | * Keep a list of places in the file that it is safe to restart the | |
85 | * highlighting. This happens whenever the lexer reports that it has | |
86 | * returned to its initial state. Since this list needs to be sorted, and | |
87 | * we need to be able to retrieve ranges from it, it is stored in a | |
88 | * balanced tree. | |
89 | */ | |
90 | private final TreeSet<DocPosition> iniPositions = new TreeSet<>(DocPositionComparator.instance); | |
91 | ||
92 | /** | |
93 | * As we go through and remove invalid positions we will also be finding | |
94 | * new valid positions. Since the position list cannot be deleted from | |
95 | * and written to at the same time, we will keep a list of the new | |
96 | * positions and simply add it to the list of positions once all the old | |
97 | * positions have been removed. | |
98 | */ | |
99 | private final HashSet<DocPosition> newPositions = new HashSet<>(); | |
100 | ||
101 | /** | |
102 | * Vector that stores the communication between the two threads. | |
103 | */ | |
104 | private final LinkedList<RecolorEvent> events = new LinkedList<>(); | |
105 | ||
106 | /** | |
107 | * When accessing the linked list, we need to create a critical section. | |
108 | * we will synchronize on this object to ensure that we don't get unsafe | |
109 | * thread behavior. | |
110 | */ | |
111 | private final Object eventsLock = new Object(); | |
112 | ||
113 | /** | |
114 | * The amount of change that has occurred before the place in the | |
115 | * document that we are currently highlighting (lastPosition). | |
116 | */ | |
117 | private volatile int change = 0; | |
118 | ||
119 | /** | |
120 | * The last position colored | |
121 | */ | |
122 | private volatile int lastPosition = -1; | |
123 | ||
124 | /** | |
125 | * Creates the coloring thread for the given document. | |
126 | * | |
127 | * @param document The document to be colored. | |
128 | */ | |
129 | public Colorer(HighlightedDocument document) { | |
130 | ||
131 | super("ThreadColorer"); | |
132 | this.document = new WeakReference<>(document); | |
133 | } | |
134 | ||
135 | /** | |
136 | * Tell the Syntax Highlighting thread to take another look at this | |
137 | * section of the document. It will process this as a FIFO. This method | |
138 | * should be done inside a docLock. | |
139 | */ | |
140 | public void color(int position, int adjustment) { | |
141 | | |
142 | // figure out if this adjustment effects the current run. | |
143 | // if it does, then adjust the place in the document | |
144 | // that gets highlighted. | |
145 |
2
1. color : changed conditional boundary → NO_COVERAGE 2. color : negated conditional → NO_COVERAGE |
if (position < this.lastPosition) { |
146 |
3
1. color : negated conditional → NO_COVERAGE 2. color : Replaced integer subtraction with addition → NO_COVERAGE 3. color : changed conditional boundary → NO_COVERAGE |
if (this.lastPosition < position - adjustment) { |
147 |
2
1. color : Replaced integer subtraction with addition → NO_COVERAGE 2. color : Replaced integer subtraction with addition → NO_COVERAGE |
this.change -= this.lastPosition - position; |
148 | } else { | |
149 |
1
1. color : Replaced integer addition with subtraction → NO_COVERAGE |
this.change += adjustment; |
150 | } | |
151 | } | |
152 | | |
153 | synchronized (this.eventsLock) { | |
154 | | |
155 |
1
1. color : negated conditional → NO_COVERAGE |
if (!this.events.isEmpty()) { |
156 | | |
157 | // check whether to coalesce with current last element | |
158 | RecolorEvent curLast = this.events.getLast(); | |
159 | | |
160 |
4
1. color : changed conditional boundary → NO_COVERAGE 2. color : negated conditional → NO_COVERAGE 3. color : changed conditional boundary → NO_COVERAGE 4. color : negated conditional → NO_COVERAGE |
if (adjustment < 0 && curLast.getAdjustment() < 0) { |
161 | // both are removals | |
162 |
1
1. color : negated conditional → NO_COVERAGE |
if (position == curLast.getPosition()) { |
163 | | |
164 |
2
1. color : Replaced integer addition with subtraction → NO_COVERAGE 2. color : removed call to com/jsql/view/swing/sql/lexer/Colorer$RecolorEvent::setAdjustment → NO_COVERAGE |
curLast.setAdjustment(curLast.getAdjustment() + adjustment); |
165 | return; | |
166 | } | |
167 |
4
1. color : changed conditional boundary → NO_COVERAGE 2. color : negated conditional → NO_COVERAGE 3. color : negated conditional → NO_COVERAGE 4. color : changed conditional boundary → NO_COVERAGE |
} else if (adjustment >= 0 && curLast.getAdjustment() >= 0) { |
168 | // both are insertions | |
169 |
2
1. color : Replaced integer addition with subtraction → NO_COVERAGE 2. color : negated conditional → NO_COVERAGE |
if (position == curLast.getPosition() + curLast.getAdjustment()) { |
170 | | |
171 |
2
1. color : Replaced integer addition with subtraction → NO_COVERAGE 2. color : removed call to com/jsql/view/swing/sql/lexer/Colorer$RecolorEvent::setAdjustment → NO_COVERAGE |
curLast.setAdjustment(curLast.getAdjustment() + adjustment); |
172 | return; | |
173 | | |
174 |
2
1. color : Replaced integer addition with subtraction → NO_COVERAGE 2. color : negated conditional → NO_COVERAGE |
} else if (curLast.getPosition() == position + adjustment) { |
175 | | |
176 |
1
1. color : removed call to com/jsql/view/swing/sql/lexer/Colorer$RecolorEvent::setPosition → NO_COVERAGE |
curLast.setPosition(position); |
177 |
2
1. color : Replaced integer addition with subtraction → NO_COVERAGE 2. color : removed call to com/jsql/view/swing/sql/lexer/Colorer$RecolorEvent::setAdjustment → NO_COVERAGE |
curLast.setAdjustment(curLast.getAdjustment() + adjustment); |
178 | | |
179 | return; | |
180 | } | |
181 | } | |
182 | } | |
183 | | |
184 | this.events.add(new RecolorEvent(position, adjustment)); | |
185 |
1
1. color : removed call to java/lang/Object::notifyAll → NO_COVERAGE |
this.eventsLock.notifyAll(); |
186 | } | |
187 | } | |
188 | ||
189 | /** | |
190 | * The colorer runs forever and may sleep for long periods of time. It | |
191 | * should be interrupted every time there is something for it to do. | |
192 | */ | |
193 | @Override | |
194 | public void run() { | |
195 |
1
1. run : negated conditional → NO_COVERAGE |
while (this.document.get() != null) { |
196 | try { | |
197 | RecolorEvent re = new RecolorEvent(0, 0); | |
198 | synchronized (this.eventsLock) { | |
199 | | |
200 | // get the next event to process - stalling until the | |
201 | // event becomes available | |
202 |
2
1. run : negated conditional → NO_COVERAGE 2. run : negated conditional → NO_COVERAGE |
while(this.events.isEmpty() && this.document.get() != null) { |
203 | // stop waiting after a second in case document | |
204 | // has been cleared. | |
205 |
1
1. run : removed call to java/lang/Object::wait → NO_COVERAGE |
this.eventsLock.wait(1000); |
206 | } | |
207 | | |
208 |
1
1. run : negated conditional → NO_COVERAGE |
if (!this.events.isEmpty()) { |
209 | re = this.events.removeFirst(); | |
210 | } | |
211 | } | |
212 |
1
1. run : removed call to com/jsql/view/swing/sql/lexer/Colorer::processEvent → NO_COVERAGE |
this.processEvent(re.getPosition(), re.getAdjustment()); |
213 |
1
1. run : removed call to java/lang/Thread::sleep → NO_COVERAGE |
Thread.sleep(100); |
214 | | |
215 | } catch(InterruptedException e) { | |
216 | | |
217 | LOGGER.log(LogLevelUtil.IGNORE, e, e); | |
218 |
1
1. run : removed call to java/lang/Thread::interrupt → NO_COVERAGE |
Thread.currentThread().interrupt(); |
219 | } | |
220 | } | |
221 | } | |
222 | | |
223 | private void processEvent(int position, int adjustment) { | |
224 | | |
225 | HighlightedDocument doc = this.document.get(); | |
226 |
1
1. processEvent : negated conditional → NO_COVERAGE |
if (doc == null) { |
227 | return; | |
228 | } | |
229 | | |
230 | // slurp everything up into local variables in case another | |
231 | // thread changes them during coloring process | |
232 | AttributeSet globalStyle = doc.getGlobalStyle(); | |
233 | Lexer syntaxLexer = doc.getSyntaxLexer(); | |
234 | DocumentReader documentReader = doc.getDocumentReader(); | |
235 | Object docLock = doc.getDocumentLock(); | |
236 | ||
237 |
1
1. processEvent : negated conditional → NO_COVERAGE |
if (globalStyle != null) { |
238 | | |
239 |
1
1. processEvent : Replaced integer addition with subtraction → NO_COVERAGE |
int start = Math.min(position, position + adjustment); |
240 |
1
1. processEvent : Replaced integer addition with subtraction → NO_COVERAGE |
int stop = Math.max(position, position + adjustment); |
241 | | |
242 | synchronized (docLock) { | |
243 |
2
1. processEvent : removed call to com/jsql/view/swing/sql/lexer/HighlightedDocument::setCharacterAttributes → NO_COVERAGE 2. processEvent : Replaced integer subtraction with addition → NO_COVERAGE |
doc.setCharacterAttributes(start, stop - start, globalStyle, true); |
244 | } | |
245 | | |
246 | return; | |
247 | } | |
248 | | |
249 | SortedSet<DocPosition> workingSet; | |
250 | Iterator<DocPosition> workingIt; | |
251 | DocPosition startRequest = new DocPosition(position); | |
252 |
1
1. processEvent : Replaced integer addition with subtraction → NO_COVERAGE |
DocPosition endRequest = new DocPosition(position + Math.abs(adjustment)); |
253 | DocPosition dp; | |
254 | DocPosition dpStart = null; | |
255 | DocPosition dpEnd; | |
256 | ||
257 | // find the starting position. We must start at least one | |
258 | // token before the current position | |
259 | try { | |
260 | // all the good positions before | |
261 | workingSet = this.iniPositions.headSet(startRequest); | |
262 | // the last of the stuff before | |
263 | dpStart = workingSet.last(); | |
264 | | |
265 | } catch (NoSuchElementException e) { | |
266 | | |
267 | // if there were no good positions before the requested | |
268 | // start, | |
269 | // we can always start at the very beginning. | |
270 | dpStart = new DocPosition(0); | |
271 | | |
272 | LOGGER.log(LogLevelUtil.IGNORE, e); | |
273 | } | |
274 | ||
275 | // if stuff was removed, take any removed positions off the | |
276 | // list. | |
277 |
2
1. processEvent : negated conditional → NO_COVERAGE 2. processEvent : changed conditional boundary → NO_COVERAGE |
if (adjustment < 0) { |
278 | | |
279 | workingSet = this.iniPositions.subSet(startRequest, endRequest); | |
280 | workingIt = workingSet.iterator(); | |
281 | | |
282 |
1
1. processEvent : negated conditional → NO_COVERAGE |
while (workingIt.hasNext()) { |
283 | | |
284 | workingIt.next(); | |
285 |
1
1. processEvent : removed call to java/util/Iterator::remove → NO_COVERAGE |
workingIt.remove(); |
286 | } | |
287 | } | |
288 | ||
289 | // adjust the positions of everything after the | |
290 | // insertion/removal. | |
291 | workingSet = this.iniPositions.tailSet(startRequest); | |
292 | workingIt = workingSet.iterator(); | |
293 |
1
1. processEvent : negated conditional → NO_COVERAGE |
while (workingIt.hasNext()) { |
294 |
1
1. processEvent : removed call to com/jsql/view/swing/sql/lexer/DocPosition::adjustPosition → NO_COVERAGE |
workingIt.next().adjustPosition(adjustment); |
295 | } | |
296 | ||
297 | // now go through and highlight as much as needed | |
298 | workingSet = this.iniPositions.tailSet(dpStart); | |
299 | workingIt = workingSet.iterator(); | |
300 | dp = null; | |
301 | | |
302 |
1
1. processEvent : negated conditional → NO_COVERAGE |
if (workingIt.hasNext()) { |
303 | dp = workingIt.next(); | |
304 | } | |
305 | | |
306 | try { | |
307 | Token t; | |
308 | boolean done = false; | |
309 | dpEnd = dpStart; | |
310 | | |
311 | synchronized (docLock) { | |
312 | | |
313 | // we are playing some games with the lexer for | |
314 | // efficiency. | |
315 | // we could just create a new lexer each time here, | |
316 | // but instead, | |
317 | // we will just reset it so that it thinks it is | |
318 | // starting at the | |
319 | // beginning of the document but reporting a funny | |
320 | // start position. | |
321 | // Reseting the lexer causes the close() method on | |
322 | // the reader | |
323 | // to be called but because the close() method has | |
324 | // no effect on the | |
325 | // DocumentReader, we can do this. | |
326 |
1
1. processEvent : removed call to com/jsql/view/swing/sql/lexer/syntax/Lexer::reset → NO_COVERAGE |
syntaxLexer.reset(documentReader, 0, dpStart |
327 | .getPosition(), 0); | |
328 | // After the lexer has been set up, scroll the | |
329 | // reader so that it | |
330 | // is in the correct spot as well. | |
331 |
1
1. processEvent : removed call to com/jsql/view/swing/sql/lexer/DocumentReader::seek → NO_COVERAGE |
documentReader.seek(dpStart.getPosition()); |
332 | // we will highlight tokens until we reach a good | |
333 | // stopping place. | |
334 | // the first obvious stopping place is the end of | |
335 | // the document. | |
336 | // the lexer will return null at the end of the | |
337 | // document and wee | |
338 | // need to stop there. | |
339 | t = syntaxLexer.getNextToken(); | |
340 | } | |
341 | | |
342 | this.newPositions.add(dpStart); | |
343 | | |
344 |
2
1. processEvent : negated conditional → NO_COVERAGE 2. processEvent : negated conditional → NO_COVERAGE |
while (!done && t != null) { |
345 | // this is the actual command that colors the stuff. | |
346 | // Color stuff with the description of the styles | |
347 | // stored in tokenStyles. | |
348 |
2
1. processEvent : negated conditional → NO_COVERAGE 2. processEvent : changed conditional boundary → NO_COVERAGE |
if (t.getCharEnd() <= doc.getLength()) { |
349 | | |
350 |
1
1. processEvent : removed call to com/jsql/view/swing/sql/lexer/HighlightedDocument::setCharacterAttributes → NO_COVERAGE |
doc.setCharacterAttributes( |
351 |
1
1. processEvent : Replaced integer addition with subtraction → NO_COVERAGE |
t.getCharBegin() + this.change, |
352 |
1
1. processEvent : Replaced integer subtraction with addition → NO_COVERAGE |
t.getCharEnd() - t.getCharBegin(), |
353 | TokenStyles.getStyle(t.getDescription()), | |
354 | true | |
355 | ); | |
356 | // record the position of the last bit of | |
357 | // text that we colored | |
358 | dpEnd = new DocPosition(t.getCharEnd()); | |
359 | } | |
360 | | |
361 |
1
1. processEvent : Replaced integer addition with subtraction → NO_COVERAGE |
this.lastPosition = t.getCharEnd() + this.change; |
362 | | |
363 | // The other more complicated reason for doing no | |
364 | // more highlighting | |
365 | // is that all the colors are the same from here on | |
366 | // out anyway. | |
367 | // We can detect this by seeing if the place that | |
368 | // the lexer returned | |
369 | // to the initial state last time we highlighted is | |
370 | // the same as the | |
371 | // place that returned to the initial state this | |
372 | // time. | |
373 | // As long as that place is after the last changed | |
374 | // text, everything | |
375 | // from there on is fine already. | |
376 |
1
1. processEvent : negated conditional → NO_COVERAGE |
if (t.getState() == Token.INITIAL_STATE) { |
377 | | |
378 | // look at all the positions from last time that | |
379 | // are less than or | |
380 | // equal to the current position | |
381 |
3
1. processEvent : negated conditional → NO_COVERAGE 2. processEvent : negated conditional → NO_COVERAGE 3. processEvent : changed conditional boundary → NO_COVERAGE |
while (dp != null && dp.getPosition() <= t.getCharEnd()) { |
382 | | |
383 |
3
1. processEvent : negated conditional → NO_COVERAGE 2. processEvent : negated conditional → NO_COVERAGE 3. processEvent : changed conditional boundary → NO_COVERAGE |
if (dp.getPosition() == t.getCharEnd() && dp.getPosition() >= endRequest.getPosition()) { |
384 | | |
385 | // we have found a state that is the | |
386 | // same | |
387 | done = true; | |
388 | dp = null; | |
389 | | |
390 |
1
1. processEvent : negated conditional → NO_COVERAGE |
} else if (workingIt.hasNext()) { |
391 | | |
392 | // didn't find it, try again. | |
393 | dp = workingIt.next(); | |
394 | | |
395 | } else { | |
396 | | |
397 | // didn't find it, and there is no more | |
398 | // info from last | |
399 | // time. This means that we will just | |
400 | // continue | |
401 | // until the end of the document. | |
402 | dp = null; | |
403 | } | |
404 | } | |
405 | | |
406 | // so that we can do this check next time, | |
407 | // record all the | |
408 | // initial states from this time. | |
409 | this.newPositions.add(dpEnd); | |
410 | } | |
411 | | |
412 | synchronized (docLock) { | |
413 | t = syntaxLexer.getNextToken(); | |
414 | } | |
415 | } | |
416 | ||
417 | // remove all the old initial positions from the place | |
418 | // where | |
419 | // we started doing the highlighting right up through | |
420 | // the last | |
421 | // bit of text we touched. | |
422 | workingIt = this.iniPositions.subSet(dpStart, dpEnd).iterator(); | |
423 |
1
1. processEvent : negated conditional → NO_COVERAGE |
while (workingIt.hasNext()) { |
424 | | |
425 | workingIt.next(); | |
426 |
1
1. processEvent : removed call to java/util/Iterator::remove → NO_COVERAGE |
workingIt.remove(); |
427 | } | |
428 | ||
429 | // Remove all the positions that are after the end of | |
430 | // the file.: | |
431 | workingIt = this.iniPositions.tailSet(new DocPosition(doc.getLength())).iterator(); | |
432 |
1
1. processEvent : negated conditional → NO_COVERAGE |
while (workingIt.hasNext()) { |
433 | | |
434 | workingIt.next(); | |
435 |
1
1. processEvent : removed call to java/util/Iterator::remove → NO_COVERAGE |
workingIt.remove(); |
436 | } | |
437 | ||
438 | // and put the new initial positions that we have found | |
439 | // on the list. | |
440 | this.iniPositions.addAll(this.newPositions); | |
441 |
1
1. processEvent : removed call to java/util/HashSet::clear → NO_COVERAGE |
this.newPositions.clear(); |
442 | | |
443 | } catch (IOException e) { | |
444 | LOGGER.log(LogLevelUtil.IGNORE, e); | |
445 | } | |
446 | | |
447 | synchronized (docLock) { | |
448 | | |
449 | this.lastPosition = -1; | |
450 | this.change = 0; | |
451 | } | |
452 | } | |
453 | | |
454 | /** | |
455 | * Stop the thread's method run() | |
456 | */ | |
457 | public void stopThread() { | |
458 |
1
1. stopThread : removed call to java/lang/ref/WeakReference::clear → NO_COVERAGE |
this.document.clear(); |
459 | } | |
460 | } | |
Mutations | ||
59 |
1.1 |
|
67 |
1.1 |
|
145 |
1.1 2.2 |
|
146 |
1.1 2.2 3.3 |
|
147 |
1.1 2.2 |
|
149 |
1.1 |
|
155 |
1.1 |
|
160 |
1.1 2.2 3.3 4.4 |
|
162 |
1.1 |
|
164 |
1.1 2.2 |
|
167 |
1.1 2.2 3.3 4.4 |
|
169 |
1.1 2.2 |
|
171 |
1.1 2.2 |
|
174 |
1.1 2.2 |
|
176 |
1.1 |
|
177 |
1.1 2.2 |
|
185 |
1.1 |
|
195 |
1.1 |
|
202 |
1.1 2.2 |
|
205 |
1.1 |
|
208 |
1.1 |
|
212 |
1.1 |
|
213 |
1.1 |
|
218 |
1.1 |
|
226 |
1.1 |
|
237 |
1.1 |
|
239 |
1.1 |
|
240 |
1.1 |
|
243 |
1.1 2.2 |
|
252 |
1.1 |
|
277 |
1.1 2.2 |
|
282 |
1.1 |
|
285 |
1.1 |
|
293 |
1.1 |
|
294 |
1.1 |
|
302 |
1.1 |
|
326 |
1.1 |
|
331 |
1.1 |
|
344 |
1.1 2.2 |
|
348 |
1.1 2.2 |
|
350 |
1.1 |
|
351 |
1.1 |
|
352 |
1.1 |
|
361 |
1.1 |
|
376 |
1.1 |
|
381 |
1.1 2.2 3.3 |
|
383 |
1.1 2.2 3.3 |
|
390 |
1.1 |
|
423 |
1.1 |
|
426 |
1.1 |
|
432 |
1.1 |
|
435 |
1.1 |
|
441 |
1.1 |
|
458 |
1.1 |