|
1 /* |
|
2 * jEdit text area as document text source |
|
3 * |
|
4 * @author Fabian Immler, TU Munich |
|
5 * @author Johannes Hölzl, TU Munich |
|
6 * @author Makarius |
|
7 */ |
|
8 |
|
9 package isabelle.jedit |
|
10 |
|
11 import scala.actors.Actor, Actor._ |
|
12 import scala.collection.mutable |
|
13 |
|
14 import isabelle.proofdocument.{ProofDocument, Change, Edit, Insert, Remove} |
|
15 import isabelle.prover.{Prover, Command} |
|
16 |
|
17 import java.awt.{Color, Graphics2D} |
|
18 import javax.swing.event.{CaretListener, CaretEvent} |
|
19 |
|
20 import org.gjt.sp.jedit.buffer.{BufferListener, JEditBuffer} |
|
21 import org.gjt.sp.jedit.textarea.{JEditTextArea, TextAreaExtension, TextAreaPainter} |
|
22 import org.gjt.sp.jedit.syntax.{ModeProvider, SyntaxStyle} |
|
23 |
|
24 |
|
25 object TheoryView |
|
26 { |
|
27 def choose_color(command: Command, doc: ProofDocument): Color = |
|
28 { |
|
29 command.status(doc) match { |
|
30 case Command.Status.UNPROCESSED => new Color(255, 228, 225) |
|
31 case Command.Status.FINISHED => new Color(234, 248, 255) |
|
32 case Command.Status.FAILED => new Color(255, 106, 106) |
|
33 case _ => Color.red |
|
34 } |
|
35 } |
|
36 } |
|
37 |
|
38 |
|
39 class TheoryView(text_area: JEditTextArea) |
|
40 { |
|
41 private val buffer = text_area.getBuffer |
|
42 |
|
43 |
|
44 /* prover setup */ |
|
45 |
|
46 val prover: Prover = new Prover(Isabelle.system, Isabelle.default_logic()) |
|
47 |
|
48 prover.command_change += ((command: Command) => |
|
49 if (current_document().commands.contains(command)) |
|
50 Swing_Thread.later { |
|
51 // FIXME proper handling of buffer vs. text areas |
|
52 // repaint if buffer is active |
|
53 if (text_area.getBuffer == buffer) { |
|
54 update_syntax(command) |
|
55 invalidate_line(command) |
|
56 overview.repaint() |
|
57 } |
|
58 }) |
|
59 |
|
60 |
|
61 /* changes vs. edits */ |
|
62 |
|
63 private val change_0 = new Change(prover.document_0.id, None, Nil) |
|
64 private var _changes = List(change_0) // owned by Swing/AWT thread |
|
65 def changes = _changes |
|
66 private var current_change = change_0 |
|
67 |
|
68 private val edits = new mutable.ListBuffer[Edit] // owned by Swing thread |
|
69 |
|
70 private val edits_delay = Swing_Thread.delay_last(300) { |
|
71 if (!edits.isEmpty) { |
|
72 val change = new Change(Isabelle.system.id(), Some(current_change), edits.toList) |
|
73 _changes ::= change |
|
74 prover.input(change) |
|
75 current_change = change |
|
76 edits.clear |
|
77 } |
|
78 } |
|
79 |
|
80 |
|
81 /* buffer_listener */ |
|
82 |
|
83 private val buffer_listener = new BufferListener { |
|
84 override def preContentInserted(buffer: JEditBuffer, |
|
85 start_line: Int, offset: Int, num_lines: Int, length: Int) |
|
86 { |
|
87 edits += Insert(offset, buffer.getText(offset, length)) |
|
88 edits_delay() |
|
89 } |
|
90 |
|
91 override def preContentRemoved(buffer: JEditBuffer, |
|
92 start_line: Int, start: Int, num_lines: Int, removed_length: Int) |
|
93 { |
|
94 edits += Remove(start, buffer.getText(start, removed_length)) |
|
95 edits_delay() |
|
96 } |
|
97 |
|
98 override def contentInserted(buffer: JEditBuffer, |
|
99 start_line: Int, offset: Int, num_lines: Int, length: Int) { } |
|
100 |
|
101 override def contentRemoved(buffer: JEditBuffer, |
|
102 start_line: Int, offset: Int, num_lines: Int, length: Int) { } |
|
103 |
|
104 override def bufferLoaded(buffer: JEditBuffer) { } |
|
105 override def foldHandlerChanged(buffer: JEditBuffer) { } |
|
106 override def foldLevelChanged(buffer: JEditBuffer, start_line: Int, end_line: Int) { } |
|
107 override def transactionComplete(buffer: JEditBuffer) { } |
|
108 } |
|
109 |
|
110 |
|
111 /* text_area_extension */ |
|
112 |
|
113 private val text_area_extension = new TextAreaExtension { |
|
114 override def paintValidLine(gfx: Graphics2D, |
|
115 screen_line: Int, physical_line: Int, start: Int, end: Int, y: Int) |
|
116 { |
|
117 val document = current_document() |
|
118 def from_current(pos: Int) = TheoryView.this.from_current(document, pos) |
|
119 def to_current(pos: Int) = TheoryView.this.to_current(document, pos) |
|
120 val saved_color = gfx.getColor |
|
121 |
|
122 val metrics = text_area.getPainter.getFontMetrics |
|
123 |
|
124 // encolor phase |
|
125 var cmd = document.command_at(from_current(start)) |
|
126 while (cmd.isDefined && cmd.get.start(document) < end) { |
|
127 val e = cmd.get |
|
128 val begin = start max to_current(e.start(document)) |
|
129 val finish = (end - 1) min to_current(e.stop(document)) |
|
130 encolor(gfx, y, metrics.getHeight, begin, finish, |
|
131 TheoryView.choose_color(e, document), true) |
|
132 cmd = document.commands.next(e) |
|
133 } |
|
134 |
|
135 gfx.setColor(saved_color) |
|
136 } |
|
137 |
|
138 override def getToolTipText(x: Int, y: Int): String = |
|
139 { |
|
140 val document = current_document() |
|
141 val offset = from_current(document, text_area.xyToOffset(x, y)) |
|
142 document.command_at(offset) match { |
|
143 case Some(cmd) => |
|
144 document.token_start(cmd.tokens.first) |
|
145 cmd.type_at(document, offset - cmd.start(document)).getOrElse(null) |
|
146 case None => null |
|
147 } |
|
148 } |
|
149 } |
|
150 |
|
151 |
|
152 /* activation */ |
|
153 |
|
154 private val overview = new Document_Overview(prover, text_area, to_current) |
|
155 |
|
156 private val selected_state_controller = new CaretListener { |
|
157 override def caretUpdate(e: CaretEvent) { |
|
158 val doc = current_document() |
|
159 doc.command_at(e.getDot) match { |
|
160 case Some(cmd) |
|
161 if (doc.token_start(cmd.tokens.first) <= e.getDot && |
|
162 Isabelle.plugin.selected_state != cmd) => |
|
163 Isabelle.plugin.selected_state = cmd |
|
164 case _ => |
|
165 } |
|
166 } |
|
167 } |
|
168 |
|
169 def activate() { |
|
170 text_area.addCaretListener(selected_state_controller) |
|
171 text_area.addLeftOfScrollBar(overview) |
|
172 text_area.getPainter. |
|
173 addExtension(TextAreaPainter.LINE_BACKGROUND_LAYER + 1, text_area_extension) |
|
174 buffer.setTokenMarker(new DynamicTokenMarker(buffer, prover)) |
|
175 buffer.addBufferListener(buffer_listener) |
|
176 |
|
177 val dockable = |
|
178 text_area.getView.getDockableWindowManager.getDockable("isabelle-output") |
|
179 if (dockable != null) { |
|
180 val output_dockable = dockable.asInstanceOf[OutputDockable] |
|
181 val output_text_view = prover.output_text_view |
|
182 output_dockable.set_text(output_text_view) |
|
183 } |
|
184 |
|
185 buffer.propertiesChanged() |
|
186 } |
|
187 |
|
188 def deactivate() { |
|
189 buffer.setTokenMarker(buffer.getMode.getTokenMarker) |
|
190 buffer.removeBufferListener(buffer_listener) |
|
191 text_area.getPainter.removeExtension(text_area_extension) |
|
192 text_area.removeLeftOfScrollBar(overview) |
|
193 text_area.removeCaretListener(selected_state_controller) |
|
194 } |
|
195 |
|
196 |
|
197 /* history of changes */ |
|
198 |
|
199 private def doc_or_pred(c: Change): ProofDocument = |
|
200 prover.document(c.id).getOrElse(doc_or_pred(c.parent.get)) |
|
201 |
|
202 def current_document() = doc_or_pred(current_change) |
|
203 |
|
204 |
|
205 /* update to desired version */ |
|
206 |
|
207 def set_version(goal: Change) { |
|
208 // changes in buffer must be ignored |
|
209 buffer.removeBufferListener(buffer_listener) |
|
210 |
|
211 def apply(change: Change): Unit = change.edits.foreach { |
|
212 case Insert(start, text) => buffer.insert(start, text) |
|
213 case Remove(start, text) => buffer.remove(start, text.length) |
|
214 } |
|
215 |
|
216 def unapply(change: Change): Unit = change.edits.reverse.foreach { |
|
217 case Insert(start, text) => buffer.remove(start, text.length) |
|
218 case Remove(start, text) => buffer.insert(start, text) |
|
219 } |
|
220 |
|
221 // undo/redo changes |
|
222 val ancs_current = current_change.ancestors |
|
223 val ancs_goal = goal.ancestors |
|
224 val paired = ancs_current.reverse zip ancs_goal.reverse |
|
225 def last_common[A](xs: List[(A, A)]): Option[A] = { |
|
226 xs match { |
|
227 case (x, y) :: xs => |
|
228 if (x == y) |
|
229 xs match { |
|
230 case (a, b) :: ys => |
|
231 if (a == b) last_common(xs) |
|
232 else Some(x) |
|
233 case _ => Some(x) |
|
234 } |
|
235 else None |
|
236 case _ => None |
|
237 } |
|
238 } |
|
239 val common_anc = last_common(paired).get |
|
240 |
|
241 ancs_current.takeWhile(_ != common_anc) map unapply |
|
242 ancs_goal.takeWhile(_ != common_anc).reverse map apply |
|
243 |
|
244 current_change = goal |
|
245 // invoke repaint |
|
246 buffer.propertiesChanged() |
|
247 invalidate_all() |
|
248 overview.repaint() |
|
249 |
|
250 // track changes in buffer |
|
251 buffer.addBufferListener(buffer_listener) |
|
252 } |
|
253 |
|
254 |
|
255 /* transforming offsets */ |
|
256 |
|
257 private def changes_from(doc: ProofDocument): List[Edit] = |
|
258 List.flatten(current_change.ancestors(_.id == doc.id).reverse.map(_.edits)) ::: |
|
259 edits.toList |
|
260 |
|
261 def from_current(doc: ProofDocument, offset: Int): Int = |
|
262 (offset /: changes_from(doc).reverse) ((i, change) => change before i) |
|
263 |
|
264 def to_current(doc: ProofDocument, offset: Int): Int = |
|
265 (offset /: changes_from(doc)) ((i, change) => change after i) |
|
266 |
|
267 |
|
268 private def lines_of_command(cmd: Command) = |
|
269 { |
|
270 val document = current_document() |
|
271 (buffer.getLineOfOffset(to_current(document, cmd.start(document))), |
|
272 buffer.getLineOfOffset(to_current(document, cmd.stop(document)))) |
|
273 } |
|
274 |
|
275 |
|
276 /* (re)painting */ |
|
277 |
|
278 private val update_delay = Swing_Thread.delay_first(500) { buffer.propertiesChanged() } |
|
279 |
|
280 private def update_syntax(cmd: Command) { |
|
281 val (line1, line2) = lines_of_command(cmd) |
|
282 if (line2 >= text_area.getFirstLine && |
|
283 line1 <= text_area.getFirstLine + text_area.getVisibleLines) |
|
284 update_delay() |
|
285 } |
|
286 |
|
287 private def invalidate_line(cmd: Command) = |
|
288 { |
|
289 val (start, stop) = lines_of_command(cmd) |
|
290 text_area.invalidateLineRange(start, stop) |
|
291 |
|
292 if (Isabelle.plugin.selected_state == cmd) |
|
293 Isabelle.plugin.selected_state = cmd // update State view |
|
294 } |
|
295 |
|
296 private def invalidate_all() = |
|
297 text_area.invalidateLineRange(text_area.getFirstPhysicalLine, |
|
298 text_area.getLastPhysicalLine) |
|
299 |
|
300 private def encolor(gfx: Graphics2D, |
|
301 y: Int, height: Int, begin: Int, finish: Int, color: Color, fill: Boolean) |
|
302 { |
|
303 val start = text_area.offsetToXY(begin) |
|
304 val stop = |
|
305 if (finish < buffer.getLength) text_area.offsetToXY(finish) |
|
306 else { |
|
307 val p = text_area.offsetToXY(finish - 1) |
|
308 val metrics = text_area.getPainter.getFontMetrics |
|
309 p.x = p.x + (metrics.charWidth(' ') max metrics.getMaxAdvance) |
|
310 p |
|
311 } |
|
312 |
|
313 if (start != null && stop != null) { |
|
314 gfx.setColor(color) |
|
315 if (fill) gfx.fillRect(start.x, y, stop.x - start.x, height) |
|
316 else gfx.drawRect(start.x, y, stop.x - start.x, height) |
|
317 } |
|
318 } |
|
319 |
|
320 |
|
321 /* init */ |
|
322 |
|
323 prover.start() |
|
324 |
|
325 edits += Insert(0, buffer.getText(0, buffer.getLength)) |
|
326 edits_delay() |
|
327 } |