1 /*******************************************************************************
2  * Copyright (c) 2000, 2015 IBM Corporation and others.
3  *
4  * This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License 2.0
6  * which accompanies this distribution, and is available at
7  * https://www.eclipse.org/legal/epl-2.0/
8  *
9  * SPDX-License-Identifier: EPL-2.0
10  *
11  * Contributors:
12  *     IBM Corporation - initial API and implementation
13  *******************************************************************************/
14 package org.eclipse.ui.texteditor;
15 
16 import java.util.HashMap;
17 import java.util.Map;
18 import java.util.ResourceBundle;
19 
20 import org.osgi.framework.Bundle;
21 
22 import org.eclipse.swt.widgets.Shell;
23 
24 import org.eclipse.core.commands.ExecutionException;
25 import org.eclipse.core.commands.operations.IOperationHistory;
26 import org.eclipse.core.commands.operations.IUndoableOperation;
27 
28 import org.eclipse.core.runtime.IAdaptable;
29 import org.eclipse.core.runtime.ILog;
30 import org.eclipse.core.runtime.IStatus;
31 import org.eclipse.core.runtime.Platform;
32 import org.eclipse.core.runtime.Status;
33 
34 import org.eclipse.core.resources.IResource;
35 
36 import org.eclipse.jface.dialogs.IInputValidator;
37 import org.eclipse.jface.dialogs.InputDialog;
38 import org.eclipse.jface.window.Window;
39 
40 import org.eclipse.jface.text.BadLocationException;
41 import org.eclipse.jface.text.IDocument;
42 import org.eclipse.jface.text.ITextSelection;
43 
44 import org.eclipse.ui.IEditorInput;
45 import org.eclipse.ui.PlatformUI;
46 import org.eclipse.ui.ide.undo.CreateMarkersOperation;
47 
48 
49 /**
50  * Action for creating a marker of a specified type for the editor's input element based on the
51  * editor's selection. If required, the action asks the user to provide a marker label. The action
52  * is initially associated with a text editor via the constructor, but that can be subsequently
53  * changed using <code>setEditor</code>.
54  * <p>
55  * The following keys, prepended by the given option prefix, are used for retrieving resources from
56  * the given bundle:
57  * </p>
58  * <ul>
59  * <li><code>"dialog.title"</code> - the input dialog's title</li>
60  * <li><code>"dialog.message"</code> - the input dialog's message</li>
61  * <li><code>"error.dialog.title"</code> - the error dialog's title</li>
62  * <li><code>"error.dialog.message"</code> - the error dialog's message</li>
63  * </ul>
64  * <p>
65  * This class may be instantiated but is not intended to be subclassed.
66  * </p>
67  *
68  * @noextend This class is not intended to be subclassed by clients.
69  */
70 public class AddMarkerAction extends TextEditorAction {
71 
72 
73 	/** The maximum length of an proposed label. */
74 	private static final int MAX_LABEL_LENGTH= 80;
75 	/** The type for newly created markers. */
76 	private String fMarkerType;
77 	/** Should the user be asked for a label? */
78 	private boolean fAskForLabel;
79 	/** The action's resource bundle. */
80 	private ResourceBundle fBundle;
81 	/** The prefix used for resource bundle lookup. */
82 	private String fPrefix;
83 
84 
85 	/**
86 	 * Creates a new action for the given text editor. The action configures its
87 	 * visual representation from the given resource bundle.
88 	 *
89 	 * @param bundle the resource bundle
90 	 * @param prefix a prefix to be prepended to the various resource keys
91 	 *   (described in <code>ResourceAction</code> constructor), or
92 	 *   <code>null</code> if none
93 	 * @param textEditor the text editor
94 	 * @param markerType the type of marker to add
95 	 * @param askForLabel <code>true</code> if the user should be asked for
96 	 *   a label for the new marker
97 	 * @see TextEditorAction#TextEditorAction(ResourceBundle, String, ITextEditor)
98 	 */
AddMarkerAction(ResourceBundle bundle, String prefix, ITextEditor textEditor, String markerType, boolean askForLabel)99 	public AddMarkerAction(ResourceBundle bundle, String prefix, ITextEditor textEditor, String markerType, boolean askForLabel) {
100 		super(bundle, prefix, textEditor);
101 		fBundle= bundle;
102 		fPrefix= prefix;
103 		fMarkerType= markerType;
104 		fAskForLabel= askForLabel;
105 	}
106 
107 	/**
108 	 * Returns this action's resource bundle.
109 	 *
110 	 * @return this action's resource bundle
111 	 */
getResourceBundle()112 	protected ResourceBundle getResourceBundle() {
113 		return fBundle;
114 	}
115 
116 	/**
117 	 * Returns this action's resource key prefix.
118 	 *
119 	 * @return this action's resource key prefix
120 	 */
getResourceKeyPrefix()121 	protected String getResourceKeyPrefix() {
122 		return fPrefix;
123 	}
124 
125 	@Override
run()126 	public void run() {
127 		IResource resource= getResource();
128 		if (resource == null)
129 			return;
130 		Map<String, Object> attributes= getInitialAttributes();
131 		if (fAskForLabel) {
132 			if (!askForLabel(attributes))
133 				return;
134 		}
135 
136 		String name= getToolTipText();
137 		name= name == null ? TextEditorMessages.AddMarkerAction_addMarker : name;
138 
139 		final Shell shell= getTextEditor().getSite().getShell();
140 		IAdaptable context= new IAdaptable() {
141 			@SuppressWarnings("unchecked")
142 			@Override
143 			public <T> T getAdapter(Class<T> adapter) {
144 				if (adapter == Shell.class)
145 					return (T) shell;
146 				return null;
147 			}
148 		};
149 
150 		IUndoableOperation operation= new CreateMarkersOperation(fMarkerType, attributes, resource, name);
151 		IOperationHistory operationHistory= PlatformUI.getWorkbench().getOperationSupport().getOperationHistory();
152 		try {
153 			operationHistory.execute(operation, null, context);
154 		} catch (ExecutionException x) {
155 			Bundle bundle= Platform.getBundle(PlatformUI.PLUGIN_ID);
156 			ILog log= Platform.getLog(bundle);
157 			String msg= getString(fBundle, fPrefix + "error.dialog.message", fPrefix + "error.dialog.message"); //$NON-NLS-2$ //$NON-NLS-1$
158 			log.log(new Status(IStatus.ERROR, PlatformUI.PLUGIN_ID, IStatus.OK, msg, x));
159 		}
160 	}
161 
162 	@Override
update()163 	public void update() {
164 		setEnabled(getResource() != null);
165 	}
166 
167 	/**
168 	 * Asks the user for a marker label. Returns <code>true</code> if a label
169 	 * is entered, <code>false</code> if the user cancels the input dialog.
170 	 * The value for the attribute <code>message</code> is modified in the given
171 	 * attribute map.
172 	 *
173 	 * @param attributes the attributes map
174 	 * @return <code>true</code> if a label has been entered
175 	 */
askForLabel(Map<String, Object> attributes)176 	protected boolean askForLabel(Map<String, Object> attributes) {
177 
178 		Object o= attributes.get("message"); //$NON-NLS-1$
179 		String proposal= (o instanceof String) ? (String) o : ""; //$NON-NLS-1$
180 		String title= getString(fBundle, fPrefix + "dialog.title", fPrefix + "dialog.title"); //$NON-NLS-2$ //$NON-NLS-1$
181 		String message= getString(fBundle, fPrefix + "dialog.message", fPrefix + "dialog.message"); //$NON-NLS-2$ //$NON-NLS-1$
182 		IInputValidator inputValidator= newText -> (newText == null || newText.trim().isEmpty()) ? " " : null;
183 		InputDialog dialog= new InputDialog(getTextEditor().getSite().getShell(), title, message, proposal, inputValidator);
184 
185 		String label= null;
186 		if (dialog.open() != Window.CANCEL)
187 			label= dialog.getValue();
188 
189 		if (label == null)
190 			return false;
191 
192 		label= label.trim();
193 		if (label.isEmpty())
194 			return false;
195 
196 		attributes.put("message", label); //$NON-NLS-1$
197 		return true;
198 	}
199 
200 	/**
201 	 * Returns the attributes the new marker will be initialized with.
202 	 * <p>
203 	 * Subclasses may extend or replace this method.</p>
204 	 *
205 	 * @return the attributes the new marker will be initialized with
206 	 */
getInitialAttributes()207 	protected Map<String, Object> getInitialAttributes() {
208 
209 		Map<String, Object> attributes= new HashMap<>(11);
210 
211 		ITextSelection selection= (ITextSelection) getTextEditor().getSelectionProvider().getSelection();
212 		if (!selection.isEmpty()) {
213 
214 			int start= selection.getOffset();
215 			int length= selection.getLength();
216 
217 			if (length < 0) {
218 				length= -length;
219 				start -= length;
220 			}
221 
222 			MarkerUtilities.setCharStart(attributes, start);
223 			MarkerUtilities.setCharEnd(attributes, start + length);
224 
225 			// marker line numbers are 1-based
226 			int line= selection.getStartLine();
227 			MarkerUtilities.setLineNumber(attributes, line == -1 ? -1 : line + 1);
228 
229 			IDocument document= getTextEditor().getDocumentProvider().getDocument(getTextEditor().getEditorInput());
230 			MarkerUtilities.setMessage(attributes, getLabelProposal(document, start, length));
231 
232 		}
233 
234 		return attributes;
235 	}
236 
237 	/**
238 	 * Returns the initial label for the marker.
239 	 *
240 	 * @param document the document from which to extract a label proposal
241 	 * @param offset the document offset of the range from which to extract the label proposal
242 	 * @param length the length of the range from which to extract the label proposal
243 	 * @return the label proposal
244 	 */
getLabelProposal(IDocument document, int offset, int length)245 	protected String getLabelProposal(IDocument document, int offset, int length) {
246 
247 
248 		try {
249 
250 
251 			if (length > 0) {
252 
253 				// find first white char but skip leading white chars
254 				int i= 0;
255 				boolean skip= true;
256 				while (i < length) {
257 					boolean isWhitespace= Character.isWhitespace(document.getChar(offset + i));
258 					if (!skip && isWhitespace)
259 						break;
260 					if (skip && !isWhitespace)
261 						skip= false;
262 					i++;
263 				}
264 
265 				String label= document.get(offset, i);
266 				return label.trim();
267 			}
268 
269 
270 			char ch;
271 
272 			// Get the first white char before the selection.
273 			int left= offset;
274 
275 			int line= document.getLineOfOffset(offset);
276 			int limit= document.getLineOffset(line);
277 
278 			while (left > limit) {
279 				ch= document.getChar(left);
280 				if (Character.isWhitespace(ch))
281 					break;
282 				--left;
283 			}
284 
285 			limit += document.getLineLength(line);
286 
287 			// Now get the first letter.
288 			while (left <= limit) {
289 				ch= document.getChar(left);
290 				if (!Character.isWhitespace(ch))
291 					break;
292 				++left;
293 			}
294 
295 			if (left > limit)
296 				return null;
297 
298 			limit= Math.min(limit, left + MAX_LABEL_LENGTH);
299 
300 			// Get the next white char.
301 			int right= (offset + length > limit ? limit : offset + length);
302 			while (right < limit) {
303 				ch= document.getChar(right);
304 				if (Character.isWhitespace(ch))
305 					break;
306 				++right;
307 			}
308 
309 			// Trim the string and return it.
310 			if (left != right) {
311 				String label= document.get(left, right - left);
312 				return label.trim();
313 			}
314 
315 		} catch (BadLocationException x) {
316 			// don't proposal label then
317 		}
318 
319 		return null;
320 	}
321 
322 	/**
323 	 * Returns the resource on which to create the marker,
324 	 * or <code>null</code> if there is no applicable resource. This
325 	 * queries the editor's input using <code>getAdapter(IResource.class)</code>.
326 	 * Subclasses may override this method.
327 	 *
328 	 * @return the resource to which to attach the newly created marker
329 	 */
getResource()330 	protected IResource getResource() {
331 		ITextEditor editor= getTextEditor();
332 		if (editor != null) {
333 			IEditorInput input= editor.getEditorInput();
334 			return ((IAdaptable) input).getAdapter(IResource.class);
335 		}
336 		return null;
337 	}
338 }
339