Add character highlighting to SCL module editor
[simantics/platform.git] / bundles / org.simantics.scl.ui / src / org / simantics / scl / ui / editor2 / SCLModuleEditor2.java
1 package org.simantics.scl.ui.editor2;
2
3 import java.text.CharacterIterator;
4
5 import org.eclipse.jface.action.IAction;
6 import org.eclipse.jface.resource.JFaceResources;
7 import org.eclipse.jface.resource.LocalResourceManager;
8 import org.eclipse.jface.resource.ResourceManager;
9 import org.eclipse.jface.text.BadLocationException;
10 import org.eclipse.jface.text.IDocument;
11 import org.eclipse.jface.text.link.LinkedModeModel;
12 import org.eclipse.jface.text.link.LinkedPosition;
13 import org.eclipse.jface.text.source.DefaultCharacterPairMatcher;
14 import org.eclipse.jface.text.source.ISourceViewer;
15 import org.eclipse.swt.SWT;
16 import org.eclipse.swt.custom.ST;
17 import org.eclipse.swt.custom.StyledText;
18 import org.eclipse.swt.graphics.Point;
19 import org.eclipse.swt.widgets.Composite;
20 import org.eclipse.ui.IEditorInput;
21 import org.eclipse.ui.IEditorSite;
22 import org.eclipse.ui.PartInitException;
23 import org.eclipse.ui.contexts.IContextService;
24 import org.eclipse.ui.editors.text.TextEditor;
25 import org.eclipse.ui.texteditor.ITextEditorActionDefinitionIds;
26 import org.eclipse.ui.texteditor.IUpdate;
27 import org.eclipse.ui.texteditor.SourceViewerDecorationSupport;
28 import org.eclipse.ui.texteditor.TextNavigationAction;
29 import org.simantics.scl.ui.editor.SCLSourceViewerConfigurationNew;
30 import org.simantics.scl.ui.editor.completion.SCLTextEditorEnvironment;
31 import org.simantics.scl.ui.editor2.iterator.DocumentCharacterIterator;
32 import org.simantics.scl.ui.editor2.iterator.JavaWordIterator;
33
34 import com.ibm.icu.text.BreakIterator;
35
36 public class SCLModuleEditor2 extends TextEditor {
37     
38     private static final char[] CHARS = new char[] { '(', ')', '{', '}', '[', ']', '<', '>' };
39     
40     private static final String MATCHING_BRACKETS = "matchingBrackets";
41     private static final String MATCHING_BRACKETS_COLOR = "matchingBracketsColor";
42     private static final String HIGHLIGHT_BRACKET_AT_CARET_LOCATION = "highlightBracketAtCaretLocation";
43     private static final String ENCLOSING_BRACKETS = "enclosingBrackets";
44     
45     private boolean disposed = false;
46     protected ResourceManager resourceManager;
47     private DefaultCharacterPairMatcher matcher;
48
49     public SCLModuleEditor2() {
50         super();
51         resourceManager = new LocalResourceManager(JFaceResources.getResources());
52         SCLSourceViewerConfigurationNew sourceViewerConfiguration = new SCLSourceViewerConfigurationNew(resourceManager);
53         setDocumentProvider(new SCLModuleEditor2DocumentProvider(sourceViewerConfiguration));
54         setSourceViewerConfiguration(sourceViewerConfiguration);
55     }
56     
57     @Override
58     public boolean isTabsToSpacesConversionEnabled() {
59         return true;
60     }
61     
62     @Override
63     public void init(IEditorSite site, IEditorInput input)
64             throws PartInitException {
65         super.init(site, input);
66         getPreferenceStore().setValue(MATCHING_BRACKETS, true);
67         getPreferenceStore().setValue(MATCHING_BRACKETS_COLOR, "192,192,192");
68         getPreferenceStore().setValue(HIGHLIGHT_BRACKET_AT_CARET_LOCATION, true);
69         getPreferenceStore().setValue(ENCLOSING_BRACKETS, true);
70     }
71
72     @Override
73     public void createPartControl(Composite parent) {
74         super.createPartControl(parent);
75         getEditorSite().getService(IContextService.class).activateContext("org.simantics.scl.ui.editor");
76         updatePartName();
77     }
78
79     @Override
80     protected void createNavigationActions() {
81         super.createNavigationActions();
82         
83         // Taken from org.eclipse.jdt.internal.ui.javaeditor.JavaEditor.createNavigationActions()
84         final StyledText textWidget= getSourceViewer().getTextWidget();
85         
86         IAction action = new NavigatePreviousSubWordAction();
87         action.setActionDefinitionId(ITextEditorActionDefinitionIds.WORD_PREVIOUS);
88         setAction(ITextEditorActionDefinitionIds.WORD_PREVIOUS, action);
89         textWidget.setKeyBinding(SWT.CTRL | SWT.ARROW_LEFT, SWT.NULL);
90
91         action = new NavigateNextSubWordAction();
92         action.setActionDefinitionId(ITextEditorActionDefinitionIds.WORD_NEXT);
93         setAction(ITextEditorActionDefinitionIds.WORD_NEXT, action);
94         textWidget.setKeyBinding(SWT.CTRL | SWT.ARROW_RIGHT, SWT.NULL);
95
96         action = new SelectPreviousSubWordAction();
97         action.setActionDefinitionId(ITextEditorActionDefinitionIds.SELECT_WORD_PREVIOUS);
98         setAction(ITextEditorActionDefinitionIds.SELECT_WORD_PREVIOUS, action);
99         textWidget.setKeyBinding(SWT.CTRL | SWT.SHIFT | SWT.ARROW_LEFT, SWT.NULL);
100
101         action = new SelectNextSubWordAction();
102         action.setActionDefinitionId(ITextEditorActionDefinitionIds.SELECT_WORD_NEXT);
103         setAction(ITextEditorActionDefinitionIds.SELECT_WORD_NEXT, action);
104         textWidget.setKeyBinding(SWT.CTRL | SWT.SHIFT | SWT.ARROW_RIGHT, SWT.NULL);
105     }
106
107     protected void updatePartName() {
108         setPartName(getEditorInput().getName());
109     }
110
111     @Override
112     protected void configureSourceViewerDecorationSupport(SourceViewerDecorationSupport support) {
113         matcher = new DefaultCharacterPairMatcher(CHARS);
114         support.setCharacterPairMatcher(matcher);
115         support.setMatchingCharacterPainterPreferenceKeys(MATCHING_BRACKETS, MATCHING_BRACKETS_COLOR, HIGHLIGHT_BRACKET_AT_CARET_LOCATION, ENCLOSING_BRACKETS);
116         super.configureSourceViewerDecorationSupport(support);
117     }
118     
119     @Override
120     public void dispose() {
121         disposed = true;
122         super.dispose();
123         resourceManager.dispose();
124         matcher.dispose();
125     }
126
127     public boolean isDisposed() {
128         return disposed;
129     }
130     
131     public SCLTextEditorEnvironment getSCLTextEditorEnvironment() {
132         return ((SCLSourceViewerConfigurationNew)getSourceViewerConfiguration())
133                 .getSclTextEditorEnvironment();
134     }
135
136     public IDocument getDocument() {
137         return getSourceViewer().getDocument();
138     }
139     
140     /**
141      * Text navigation action to navigate to the next sub-word.
142      *
143      * @since 3.0
144      */
145     protected abstract class NextSubWordAction extends TextNavigationAction {
146
147         protected JavaWordIterator fIterator= new JavaWordIterator();
148
149         /**
150          * Creates a new next sub-word action.
151          *
152          * @param code Action code for the default operation. Must be an action code from @see org.eclipse.swt.custom.ST.
153          */
154         protected NextSubWordAction(int code) {
155             super(getSourceViewer().getTextWidget(), code);
156         }
157
158         /*
159          * @see org.eclipse.jface.action.IAction#run()
160          */
161         @Override
162         public void run() {
163             final ISourceViewer viewer= getSourceViewer();
164             final IDocument document= viewer.getDocument();
165             try {
166                 fIterator.setText((CharacterIterator)new DocumentCharacterIterator(document));
167                 int position= widgetOffset2ModelOffset(viewer, viewer.getTextWidget().getCaretOffset());
168                 if (position == -1)
169                     return;
170
171                 int next= findNextPosition(position);
172                 if (isBlockSelectionModeEnabled() && document.getLineOfOffset(next) != document.getLineOfOffset(position)) {
173                     super.run(); // may navigate into virtual white space
174                 } else if (next != BreakIterator.DONE) {
175                     setCaretPosition(next);
176                     getTextWidget().showSelection();
177                     fireSelectionChanged();
178                 }
179             } catch (BadLocationException x) {
180                 // ignore
181             }
182         }
183
184         /**
185          * Finds the next position after the given position.
186          *
187          * @param position the current position
188          * @return the next position
189          */
190         protected int findNextPosition(int position) {
191             ISourceViewer viewer= getSourceViewer();
192             int widget= -1;
193             int next= position;
194             while (next != BreakIterator.DONE && widget == -1) { // XXX: optimize
195                 next= fIterator.following(next);
196                 if (next != BreakIterator.DONE)
197                     widget= modelOffset2WidgetOffset(viewer, next);
198             }
199
200             IDocument document= viewer.getDocument();
201             LinkedModeModel model= LinkedModeModel.getModel(document, position);
202             if (model != null && next != BreakIterator.DONE) {
203                 LinkedPosition linkedPosition= model.findPosition(new LinkedPosition(document, position, 0));
204                 if (linkedPosition != null) {
205                     int linkedPositionEnd= linkedPosition.getOffset() + linkedPosition.getLength();
206                     if (position != linkedPositionEnd && linkedPositionEnd < next)
207                         next= linkedPositionEnd;
208                 } else {
209                     LinkedPosition nextLinkedPosition= model.findPosition(new LinkedPosition(document, next, 0));
210                     if (nextLinkedPosition != null) {
211                         int nextLinkedPositionOffset= nextLinkedPosition.getOffset();
212                         if (position != nextLinkedPositionOffset && nextLinkedPositionOffset < next)
213                             next= nextLinkedPositionOffset;
214                     }
215                 }
216             }
217
218             return next;
219         }
220
221         /**
222          * Sets the caret position to the sub-word boundary given with <code>position</code>.
223          *
224          * @param position Position where the action should move the caret
225          */
226         protected abstract void setCaretPosition(int position);
227     }
228
229     /**
230      * Text navigation action to navigate to the next sub-word.
231      *
232      * @since 3.0
233      */
234     protected class NavigateNextSubWordAction extends NextSubWordAction {
235
236         /**
237          * Creates a new navigate next sub-word action.
238          */
239         public NavigateNextSubWordAction() {
240             super(ST.WORD_NEXT);
241         }
242
243         /*
244          * @see org.eclipse.jdt.internal.ui.javaeditor.JavaEditor.NextSubWordAction#setCaretPosition(int)
245          */
246         @Override
247         protected void setCaretPosition(final int position) {
248             getTextWidget().setCaretOffset(modelOffset2WidgetOffset(getSourceViewer(), position));
249         }
250     }
251
252     /**
253      * Text operation action to delete the next sub-word.
254      *
255      * @since 3.0
256      */
257     protected class DeleteNextSubWordAction extends NextSubWordAction implements IUpdate {
258
259         /**
260          * Creates a new delete next sub-word action.
261          */
262         public DeleteNextSubWordAction() {
263             super(ST.DELETE_WORD_NEXT);
264         }
265
266         /*
267          * @see org.eclipse.jdt.internal.ui.javaeditor.JavaEditor.NextSubWordAction#setCaretPosition(int)
268          */
269         @Override
270         protected void setCaretPosition(final int position) {
271             if (!validateEditorInputState())
272                 return;
273
274             final ISourceViewer viewer= getSourceViewer();
275             StyledText text= viewer.getTextWidget();
276             Point widgetSelection= text.getSelection();
277             if (isBlockSelectionModeEnabled() && widgetSelection.y != widgetSelection.x) {
278                 final int caret= text.getCaretOffset();
279                 final int offset= modelOffset2WidgetOffset(viewer, position);
280
281                 if (caret == widgetSelection.x)
282                     text.setSelectionRange(widgetSelection.y, offset - widgetSelection.y);
283                 else
284                     text.setSelectionRange(widgetSelection.x, offset - widgetSelection.x);
285                 text.invokeAction(ST.DELETE_NEXT);
286             } else {
287                 Point selection= viewer.getSelectedRange();
288                 final int caret, length;
289                 if (selection.y != 0) {
290                     caret= selection.x;
291                     length= selection.y;
292                 } else {
293                     caret= widgetOffset2ModelOffset(viewer, text.getCaretOffset());
294                     length= position - caret;
295                 }
296
297                 try {
298                     viewer.getDocument().replace(caret, length, ""); //$NON-NLS-1$
299                 } catch (BadLocationException exception) {
300                     // Should not happen
301                 }
302             }
303         }
304
305         /*
306          * @see org.eclipse.ui.texteditor.IUpdate#update()
307          */
308         public void update() {
309             setEnabled(isEditorInputModifiable());
310         }
311     }
312
313     /**
314      * Text operation action to select the next sub-word.
315      *
316      * @since 3.0
317      */
318     protected class SelectNextSubWordAction extends NextSubWordAction {
319
320         /**
321          * Creates a new select next sub-word action.
322          */
323         public SelectNextSubWordAction() {
324             super(ST.SELECT_WORD_NEXT);
325         }
326
327         /*
328          * @see org.eclipse.jdt.internal.ui.javaeditor.JavaEditor.NextSubWordAction#setCaretPosition(int)
329          */
330         @Override
331         protected void setCaretPosition(final int position) {
332             final ISourceViewer viewer= getSourceViewer();
333
334             final StyledText text= viewer.getTextWidget();
335             if (text != null && !text.isDisposed()) {
336
337                 final Point selection= text.getSelection();
338                 final int caret= text.getCaretOffset();
339                 final int offset= modelOffset2WidgetOffset(viewer, position);
340
341                 if (caret == selection.x)
342                     text.setSelectionRange(selection.y, offset - selection.y);
343                 else
344                     text.setSelectionRange(selection.x, offset - selection.x);
345             }
346         }
347     }
348
349     /**
350      * Text navigation action to navigate to the previous sub-word.
351      *
352      * @since 3.0
353      */
354     protected abstract class PreviousSubWordAction extends TextNavigationAction {
355
356         protected JavaWordIterator fIterator= new JavaWordIterator();
357
358         /**
359          * Creates a new previous sub-word action.
360          *
361          * @param code Action code for the default operation. Must be an action code from @see org.eclipse.swt.custom.ST.
362          */
363         protected PreviousSubWordAction(final int code) {
364             super(getSourceViewer().getTextWidget(), code);
365         }
366
367         /*
368          * @see org.eclipse.jface.action.IAction#run()
369          */
370         @Override
371         public void run() {
372             final ISourceViewer viewer= getSourceViewer();
373             final IDocument document= viewer.getDocument();
374             try {
375                 fIterator.setText((CharacterIterator)new DocumentCharacterIterator(document));
376                 int position= widgetOffset2ModelOffset(viewer, viewer.getTextWidget().getCaretOffset());
377                 if (position == -1)
378                     return;
379
380                 int previous= findPreviousPosition(position);
381                 if (isBlockSelectionModeEnabled() && document.getLineOfOffset(previous) != document.getLineOfOffset(position)) {
382                     super.run(); // may navigate into virtual white space
383                 } else if (previous != BreakIterator.DONE) {
384                     setCaretPosition(previous);
385                     getTextWidget().showSelection();
386                     fireSelectionChanged();
387                 }
388             } catch (BadLocationException x) {
389                 // ignore - getLineOfOffset failed
390             }
391
392         }
393
394         /**
395          * Finds the previous position before the given position.
396          *
397          * @param position the current position
398          * @return the previous position
399          */
400         protected int findPreviousPosition(int position) {
401             ISourceViewer viewer= getSourceViewer();
402             int widget= -1;
403             int previous= position;
404             while (previous != BreakIterator.DONE && widget == -1) { // XXX: optimize
405                 previous= fIterator.preceding(previous);
406                 if (previous != BreakIterator.DONE)
407                     widget= modelOffset2WidgetOffset(viewer, previous);
408             }
409
410             IDocument document= viewer.getDocument();
411             LinkedModeModel model= LinkedModeModel.getModel(document, position);
412             if (model != null && previous != BreakIterator.DONE) {
413                 LinkedPosition linkedPosition= model.findPosition(new LinkedPosition(document, position, 0));
414                 if (linkedPosition != null) {
415                     int linkedPositionOffset= linkedPosition.getOffset();
416                     if (position != linkedPositionOffset && previous < linkedPositionOffset)
417                         previous= linkedPositionOffset;
418                 } else {
419                     LinkedPosition previousLinkedPosition= model.findPosition(new LinkedPosition(document, previous, 0));
420                     if (previousLinkedPosition != null) {
421                         int previousLinkedPositionEnd= previousLinkedPosition.getOffset() + previousLinkedPosition.getLength();
422                         if (position != previousLinkedPositionEnd && previous < previousLinkedPositionEnd)
423                             previous= previousLinkedPositionEnd;
424                     }
425                 }
426             }
427
428             return previous;
429         }
430
431         /**
432          * Sets the caret position to the sub-word boundary given with <code>position</code>.
433          *
434          * @param position Position where the action should move the caret
435          */
436         protected abstract void setCaretPosition(int position);
437     }
438
439     /**
440      * Text navigation action to navigate to the previous sub-word.
441      *
442      * @since 3.0
443      */
444     protected class NavigatePreviousSubWordAction extends PreviousSubWordAction {
445
446         /**
447          * Creates a new navigate previous sub-word action.
448          */
449         public NavigatePreviousSubWordAction() {
450             super(ST.WORD_PREVIOUS);
451         }
452
453         /*
454          * @see org.eclipse.jdt.internal.ui.javaeditor.JavaEditor.PreviousSubWordAction#setCaretPosition(int)
455          */
456         @Override
457         protected void setCaretPosition(final int position) {
458             getTextWidget().setCaretOffset(modelOffset2WidgetOffset(getSourceViewer(), position));
459         }
460     }
461
462     /**
463      * Text operation action to delete the previous sub-word.
464      *
465      * @since 3.0
466      */
467     protected class DeletePreviousSubWordAction extends PreviousSubWordAction implements IUpdate {
468
469         /**
470          * Creates a new delete previous sub-word action.
471          */
472         public DeletePreviousSubWordAction() {
473             super(ST.DELETE_WORD_PREVIOUS);
474         }
475
476         /*
477          * @see org.eclipse.jdt.internal.ui.javaeditor.JavaEditor.PreviousSubWordAction#setCaretPosition(int)
478          */
479         @Override
480         protected void setCaretPosition(int position) {
481             if (!validateEditorInputState())
482                 return;
483
484             final int length;
485             final ISourceViewer viewer= getSourceViewer();
486             StyledText text= viewer.getTextWidget();
487             Point widgetSelection= text.getSelection();
488             if (isBlockSelectionModeEnabled() && widgetSelection.y != widgetSelection.x) {
489                 final int caret= text.getCaretOffset();
490                 final int offset= modelOffset2WidgetOffset(viewer, position);
491
492                 if (caret == widgetSelection.x)
493                     text.setSelectionRange(widgetSelection.y, offset - widgetSelection.y);
494                 else
495                     text.setSelectionRange(widgetSelection.x, offset - widgetSelection.x);
496                 text.invokeAction(ST.DELETE_PREVIOUS);
497             } else {
498                 Point selection= viewer.getSelectedRange();
499                 if (selection.y != 0) {
500                     position= selection.x;
501                     length= selection.y;
502                 } else {
503                     length= widgetOffset2ModelOffset(viewer, text.getCaretOffset()) - position;
504                 }
505
506                 try {
507                     viewer.getDocument().replace(position, length, ""); //$NON-NLS-1$
508                 } catch (BadLocationException exception) {
509                     // Should not happen
510                 }
511             }
512         }
513
514         /*
515          * @see org.eclipse.ui.texteditor.IUpdate#update()
516          */
517         public void update() {
518             setEnabled(isEditorInputModifiable());
519         }
520     }
521
522     /**
523      * Text operation action to select the previous sub-word.
524      *
525      * @since 3.0
526      */
527     protected class SelectPreviousSubWordAction extends PreviousSubWordAction {
528
529         /**
530          * Creates a new select previous sub-word action.
531          */
532         public SelectPreviousSubWordAction() {
533             super(ST.SELECT_WORD_PREVIOUS);
534         }
535
536         /*
537          * @see org.eclipse.jdt.internal.ui.javaeditor.JavaEditor.PreviousSubWordAction#setCaretPosition(int)
538          */
539         @Override
540         protected void setCaretPosition(final int position) {
541             final ISourceViewer viewer= getSourceViewer();
542
543             final StyledText text= viewer.getTextWidget();
544             if (text != null && !text.isDisposed()) {
545
546                 final Point selection= text.getSelection();
547                 final int caret= text.getCaretOffset();
548                 final int offset= modelOffset2WidgetOffset(viewer, position);
549
550                 if (caret == selection.x)
551                     text.setSelectionRange(selection.y, offset - selection.y);
552                 else
553                     text.setSelectionRange(selection.x, offset - selection.x);
554             }
555         }
556     }
557
558 }