View Javadoc

1   /**
2    * Copyright (C) 2008 Atlassian
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *    http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package com.atlassian.theplugin.idea.ui.linkhiglighter;
17  
18  import com.atlassian.theplugin.idea.IdeaHelper;
19  import com.intellij.openapi.editor.Document;
20  import com.intellij.openapi.editor.Editor;
21  import com.intellij.openapi.editor.event.DocumentAdapter;
22  import com.intellij.openapi.editor.event.DocumentEvent;
23  import com.intellij.openapi.editor.markup.RangeHighlighter;
24  import com.intellij.openapi.project.Project;
25  import com.intellij.openapi.vfs.VirtualFile;
26  import com.intellij.psi.PsiFile;
27  import org.jetbrains.annotations.NotNull;
28  
29  import java.util.ArrayList;
30  import java.util.List;
31  
32  /**
33   * User: pmaruszak
34   */
35  public class JiraLinkHighlighter {
36  	private final Project project;
37  	private final VirtualFile newFile;
38  	private final PsiFile psiFile;
39  	private final Editor editor;
40  	private final JiraEditorLinkParser jiraEditorLinkParser;
41  	private final List<JiraURLTextRange> ranges = new ArrayList<JiraURLTextRange>();
42  	private EditorInputHandler inputEditorInputHandler = null;
43  	private DocumentAdapter docAdapter = null;
44  	private boolean isListening = false;
45  
46  	public JiraLinkHighlighter(@NotNull final Project project, final VirtualFile newFile,
47  			final PsiFile psiFile,
48  			final Editor editor, JiraEditorLinkParser jiraEditorLinkParser) {
49  		this.project = project;
50  
51  
52  		this.newFile = newFile;
53  		this.psiFile = psiFile;
54  		this.editor = editor;
55  		this.jiraEditorLinkParser = jiraEditorLinkParser;
56  	}
57  
58  	public void stopListening() {
59  		if (isListening) {
60  			editor.removeEditorMouseListener(inputEditorInputHandler);
61  			editor.removeEditorMouseMotionListener(inputEditorInputHandler);
62  			editor.getContentComponent().removeKeyListener(inputEditorInputHandler);
63  			editor.getDocument().removeDocumentListener(docAdapter);
64  			isListening = false;
65  		}
66  	}
67  
68  	public void removeAllRanges() {
69  		for (JiraURLTextRange range : ranges) {
70  			range.setActive(false);
71  		}
72  		highlightLink(ranges);
73  		ranges.clear();
74  	}
75  
76  	public void startListeninig() {
77  		listenOnDocument();
78  		listenOnInput();
79  		isListening = true;
80  	}
81  
82  	public void reparseAll() {
83  		int length = editor.getDocument().getCharsSequence().length();
84  		parseDocumentRanges(0, length, length);
85  	}
86  
87  
88  	public void checkComments() {
89  		List<JiraURLTextRange> newRanges = new ArrayList<JiraURLTextRange>();
90  		for (JiraURLTextRange range : ranges) {
91  			boolean isComment = JiraEditorLinkParser.isComment(psiFile, range.getStartOffset());
92  			if (isComment != range.isActive()) {
93  				range.setActive(isComment);
94  				newRanges.add(range);
95  			}
96  		}
97  
98  		if (!newRanges.isEmpty()) {
99  			highlightLink(newRanges);
100 		}
101 
102 	}
103 
104 	private void listenOnInput() {
105 		inputEditorInputHandler = new EditorInputHandler(IdeaHelper.getProjectCfgManager(project), project, editor, psiFile,
106 				jiraEditorLinkParser);
107 		editor.getContentComponent().addKeyListener(inputEditorInputHandler);
108 		editor.addEditorMouseMotionListener(inputEditorInputHandler);
109 		editor.addEditorMouseListener(inputEditorInputHandler);
110 	}
111 
112 	private void listenOnDocument() {
113 		final Document doc = editor.getDocument();
114 		docAdapter = new DocumentAdapter() {
115 
116 			public void beforeDocumentChange(final DocumentEvent event) {
117 				if (event.getNewLength() < event.getOldLength()) { //deletion
118 					forgetDocumentRanges(event.getOffset(), event.getOffset() + event.getOldLength());
119 				}
120 				super.beforeDocumentChange(event);
121 			}
122 
123 			public void documentChanged(final DocumentEvent event) {
124 				parseDocumentRanges(event.getOffset(), event.getOldLength(), event.getNewLength());
125 				super.documentChanged(event);
126 			}
127 		};
128 
129 		doc.addDocumentListener(docAdapter);
130 		//@todo: implement unregistering listener  
131 
132 	}
133 
134 	private void parseDocumentRanges(final int offset, final int oldLength, final int newLength) {
135 		List<JiraURLTextRange> newRanges = jiraEditorLinkParser.getJiraURLTextRange(editor, getStartLineOffset(editor, offset),
136 				getEndLineOffset(editor, offset + newLength));
137 
138 		List<JiraURLTextRange> rangesToForget = new ArrayList<JiraURLTextRange>();
139 		List<JiraURLTextRange> intersectingRanges = getIntersectingRanges(offset, oldLength);
140 
141 		if (newLength != oldLength) {
142 			//shift ranges
143 			for (JiraURLTextRange range : ranges) {
144 				if (range.getStartOffset() >= offset) {
145 					range.shift(newLength - oldLength);
146 				}
147 			}
148 		}
149 
150 		for (JiraURLTextRange irange : intersectingRanges) {
151 			boolean oldInNew = false;
152 			for (JiraURLTextRange newRange : newRanges) {
153 				if (irange.equals(newRange)) {
154 					oldInNew = true;
155 					break;
156 				}
157 			}
158 
159 			if (!oldInNew) {
160 				rangesToForget.add(irange);
161 			}
162 		}
163 
164 		if (!rangesToForget.isEmpty()) {
165 			for (JiraURLTextRange range : rangesToForget) {
166 				range.setActive(false);
167 				ranges.remove(range);
168 			}
169 			highlightLink(rangesToForget);
170 		}
171 
172 		if (!newRanges.isEmpty()) {
173 			List<JiraURLTextRange> rangesToRemember = new ArrayList<JiraURLTextRange>();
174 			for (JiraURLTextRange range : newRanges) {
175 				if (!ranges.contains(range)) {
176 					rangesToRemember.add(range);
177 				}
178 			}
179 
180 			if (!rangesToRemember.isEmpty()) {
181 				ranges.addAll(rangesToRemember);
182 				highlightLink(ranges);
183 
184 			}
185 		}
186 
187 	}
188 
189 	private List<JiraURLTextRange> getIntersectingRanges(final int offset, final int oldLength) {
190 		List<JiraURLTextRange> intersecting = new ArrayList<JiraURLTextRange>();
191 		for (JiraURLTextRange range : ranges) {
192 			if (Math.max(range.getStartOffset(), offset) <= Math.min(range.getEndOffset(), offset + oldLength)) {
193 				intersecting.add(range);
194 			}
195 		}
196 
197 		return intersecting;
198 	}
199 
200 	public static int getEndLineOffset(final Editor editor, final int o) {
201 		final int textLength = editor.getDocument().getTextLength();
202 		int offset = Math.max(Math.min(o, Math.max(0, textLength)), Math.min(0, textLength));
203 		int lineCount = editor.getDocument().getLineCount();
204 		int lineNumber = 0;
205 
206 		if (offset < 0) {
207 			lineNumber = 0;
208 		} else if (offset < textLength) {
209 			lineNumber = editor.getDocument().getLineNumber(offset);
210 		} else {
211 			lineNumber = lineCount - 1;
212 		}
213 
214 		if (lineNumber >= 0 && lineNumber < lineCount) {
215 			return editor.getDocument().getLineEndOffset(lineNumber);
216 		} else {
217 			return 0;
218 		}
219 	}
220 
221 	private void highlightLink(final List<JiraURLTextRange> rangesList) {
222 		for (RangeHighlighter h : editor.getMarkupModel().getAllHighlighters()) {
223 			JiraURLTextRange jiraRange = JiraURLTextRange.getFrom(h);
224 			if (jiraRange != null && rangesList.contains(jiraRange) && !jiraRange.isActive()) {
225 				editor.getMarkupModel().removeHighlighter(h);
226 
227 			}
228 		}
229 
230 		for (JiraURLTextRange urlTextRange : rangesList) {
231 			if (urlTextRange.isActive()) {
232 				urlTextRange.addLinkHighlighter(editor);
233 			}
234 		}
235 	}
236 
237 	public static int getStartLineOffset(final Editor editor, final int o) {
238 		final int textLength = editor.getDocument().getTextLength();
239 		int offset = Math.max(Math.min(o, Math.max(0, textLength)), Math.min(0, textLength));
240 		int lineCount = editor.getDocument().getLineCount();
241 		int lineNumber = 0;
242 
243 		if (offset < 0) {
244 			lineNumber = 0;
245 		} else if (offset < textLength) {
246 			lineNumber = editor.getDocument().getLineNumber(offset);
247 		} else {
248 			lineNumber = lineCount - 1;
249 		}
250 
251 		if (lineNumber >= 0 && lineNumber < lineCount) {
252 			return editor.getDocument().getLineStartOffset(lineNumber);
253 		} else {
254 			return 0;
255 		}
256 	}
257 
258 	private void forgetDocumentRanges(final int start, final int end) {
259 		List<JiraURLTextRange> forgetRangesList = new ArrayList<JiraURLTextRange>();
260 		for (JiraURLTextRange urlRange : ranges) {
261 			if (Math.max(start, urlRange.getStartOffset()) < Math.min(end, urlRange.getEndOffset())) {
262 				forgetRangesList.add(urlRange);
263 				urlRange.setActive(false);
264 			}
265 
266 			if (!forgetRangesList.isEmpty()) {
267 				highlightLink(forgetRangesList);
268 			}
269 
270 		}
271 
272 		if (!forgetRangesList.isEmpty()) {
273 			ranges.removeAll(forgetRangesList);
274 		}
275 
276 	}
277 }