PageRenderTime 221ms CodeModel.GetById 172ms app.highlight 42ms RepoModel.GetById 1ms app.codeStats 0ms

/bundles/plugins-trunk/XML/xml/XmlActions.java

#
Java | 1202 lines | 989 code | 100 blank | 113 comment | 168 complexity | 80aadaefdb7f8c0b52423116372a0f56 MD5 | raw file
   1/*
   2 * XmlActions.java - Action implementations
   3 * :tabSize=8:indentSize=8:noTabs=false:
   4 * :folding=explicit:collapseFolds=1:
   5 *
   6 * Copyright (C) 2000, 2003 Slava Pestov
   7 *               1998, 2000 Ollie Rutherfurd
   8 *               2000, 2001 Andre Kaplan
   9 *               1999 Romain Guy
  10 *		 2007 Alan Ezust
  11 *
  12 * The XML plugin is licensed under the GNU General Public License, with
  13 * the following exception:
  14 *
  15 * "Permission is granted to link this code with software released under
  16 * the Apache license version 1.1, for example used by the Xerces XML
  17 * parser package."
  18 */
  19
  20package xml;
  21
  22//{{{ Imports
  23import java.awt.Toolkit;
  24import java.awt.datatransfer.StringSelection;
  25import java.io.IOException;
  26import java.io.StreamTokenizer;
  27import java.io.StringReader;
  28import java.lang.reflect.InvocationTargetException;
  29import java.lang.reflect.Method;
  30import java.util.HashMap;
  31import java.util.Iterator;
  32import java.util.List;
  33import java.util.Map;
  34
  35import javax.swing.text.Segment;
  36import javax.swing.tree.DefaultMutableTreeNode;
  37import javax.swing.tree.TreePath;
  38import javax.swing.SwingUtilities;
  39
  40import org.gjt.sp.jedit.BeanShell;
  41import org.gjt.sp.jedit.Buffer;
  42import org.gjt.sp.jedit.GUIUtilities;
  43import org.gjt.sp.jedit.gui.StatusBar;
  44import org.gjt.sp.jedit.msg.PositionChanging;
  45import org.gjt.sp.jedit.EditBus;
  46import org.gjt.sp.jedit.Macros;
  47import org.gjt.sp.jedit.View;
  48import org.gjt.sp.jedit.jEdit;
  49import org.gjt.sp.jedit.Registers;
  50import org.gjt.sp.jedit.buffer.JEditBuffer;
  51import org.gjt.sp.jedit.textarea.JEditTextArea;
  52import org.gjt.sp.jedit.textarea.Selection;
  53import org.gjt.sp.util.Log;
  54
  55import sidekick.SideKickParsedData;
  56import xml.completion.ElementDecl;
  57import xml.parser.TagParser;
  58import xml.parser.XmlTag;
  59import xml.parser.TagParser.Tag;
  60import xml.parser.TagParser.Attr;
  61
  62import sidekick.html.parser.html.HtmlDocument;
  63import sidekick.util.SideKickElement;
  64import sidekick.util.SideKickAsset;
  65//}}}
  66
  67// {{{ class XMLActions
  68public class XmlActions
  69{
  70	//{{{ Static variables
  71	private static Segment seg = new Segment();
  72	private static boolean closeCompletion;
  73	private static boolean closeCompletionOpen;
  74	private static boolean standaloneExtraSpace;
  75	static final String brackets = "[](){}";
  76	static final String xmlchars = "<>";
  77	//}}}
  78
  79	//{{{ showEditTagDialog() methods
  80
  81	public static void showEditTagDialog(View view)
  82	{
  83		JEditTextArea textArea = view.getTextArea();
  84
  85		if(XmlPlugin.isDelegated(textArea))
  86		{
  87			view.getToolkit().beep();
  88			return;
  89		}
  90
  91		Buffer buffer = view.getBuffer();
  92		XmlParsedData data = XmlParsedData.getParsedData(view, true);
  93		if(data == null)return;
  94
  95		CharSequence text = buffer.getSegment(0,buffer.getLength());
  96
  97		int caret = textArea.getCaretPosition();
  98
  99		TagParser.Tag tag = TagParser.getTagAtOffset(text,caret);
 100		if(tag == null || tag.type == TagParser.T_END_TAG)
 101		{
 102			view.getToolkit().beep();
 103			return;
 104		}
 105
 106		// use a StringTokenizer to parse the tag - WTF?!?? Why not find data?
 107		HashMap<String, Object> attributes = new HashMap<String, Object>();
 108		String attributeName = null;
 109		boolean seenEquals = false;
 110		boolean empty = false;
 111
 112		/* StringTokenizer does not support disabling or changing
 113		 * the escape character, so we have to work around it here. */
 114		char backslashSub = 127;
 115		StreamTokenizer st = new StreamTokenizer(new StringReader(
 116			text.subSequence(tag.start + tag.tag.length() + 1,
 117			tag.end - 1).toString()
 118			.replace('\\',backslashSub)));
 119		st.resetSyntax();
 120		st.wordChars('!',255);
 121		st.whitespaceChars(0,' ');
 122		st.quoteChar('"');
 123		st.quoteChar('\'');
 124		st.ordinaryChar('/');
 125		st.ordinaryChar('=');
 126
 127		Map entityHash = data.entityHash;
 128
 129		//{{{ parse tag
 130		try
 131		{
 132loop:			for(;;)
 133			{
 134				switch(st.nextToken())
 135				{
 136				case StreamTokenizer.TT_EOF:
 137					if(attributeName != null)
 138					{
 139						// in HTML, can have attributes
 140						// without values.
 141						attributes.put(attributeName,
 142							attributeName);
 143					}
 144					break loop;
 145				case '=':
 146					seenEquals = true;
 147					break;
 148				case StreamTokenizer.TT_WORD:
 149					if(attributeName == null)
 150					{
 151						attributeName = (data.html
 152							? st.sval.toLowerCase()
 153							: st.sval);
 154						break;
 155					}
 156					else
 157						/* fall thru */;
 158				case '"':
 159				case '\'':
 160					if(attributeName != null)
 161					{
 162						if(seenEquals)
 163						{
 164							attributes.put(attributeName,
 165								entitiesToCharacters(
 166								st.sval.replace(backslashSub,'\\'),
 167								entityHash));
 168							seenEquals = false;
 169						}
 170						else if(data.html)
 171						{
 172							attributes.put(attributeName,
 173								Boolean.TRUE);
 174						}
 175						attributeName = null;
 176					}
 177					break;
 178				case '/':
 179					empty = true;
 180					break;
 181				}
 182			}
 183		}
 184		catch(IOException io)
 185		{
 186			Log.log(Log.ERROR, XmlActions.class, "this shouldn't happen:", io);
 187		} //}}}
 188
 189		// must really go inside the open tag to have it returned (hence tag.end+1)
 190		ElementDecl elementDecl = data.getElementDecl(tag.tag,tag.end+1);
 191		if(elementDecl == null)
 192		{
 193			String[] pp = { tag.tag };
 194			GUIUtilities.error(view,"xml-edit-tag.undefined-element",pp);
 195			return;
 196		}
 197
 198		// tag.start+1 because  don't want the locally declared namespaces
 199		Map<String,String> namespaces = data.getNamespaceBindings(tag.start+1);
 200		Map<String,String> namespacesToInsert = new HashMap<String,String>();
 201	
 202		// grab namespaces declared in this tag
 203		for(String attrname: attributes.keySet()){
 204			if(attrname.startsWith("xmlns:")){
 205				namespacesToInsert.put((String)attributes.get(attrname), attrname.substring(6));
 206			}
 207		}
 208		
 209		EditTagDialog dialog = new EditTagDialog(view,tag.tag,
 210			elementDecl,attributes,empty,
 211			elementDecl.completionInfo.entityHash,
 212			data.getSortedIds(),data.html, namespaces, namespacesToInsert);
 213
 214		String newTag = dialog.getNewTag();
 215
 216		if(newTag != null)
 217		{
 218			try
 219			{
 220				buffer.beginCompoundEdit();
 221
 222				buffer.remove(tag.start,tag.end - tag.start);
 223				buffer.insert(tag.start,newTag);
 224			}
 225			finally
 226			{
 227				buffer.endCompoundEdit();
 228			}
 229		}
 230	}
 231
 232	/**
 233	 * show EditTagDialog in order to insert open and close tags or edit current tag 
 234	 * @param view						current view
 235	 * @param elementName				qualified element name to insert
 236	 * @param elementDecl				element to insert (may not be null)
 237	 * @param insideTag					Selection of the current start tag (or part of, for instance inf XmlCompletion, what has been already typed)
 238	 * @param namespaces				namespace bindings in scope for current location (not those declared inside the start tag itself)
 239	 * @param namespacesToInsert		namespace bindings that will have to be inserted at the end of the start tag
 240	 * @param reallyShowEditTagDialog	false to disable showing the dialog at all, but insert the start and end tags nonetheless 
 241	 */
 242	public static void showEditTagDialog(View view, String elementName, ElementDecl elementDecl, Selection insideTag, Map<String, String> namespaces, Map<String, String> namespacesToInsert, boolean reallyShowEditTagDialog)
 243	{
 244		Buffer buffer = view.getBuffer();
 245
 246		XmlParsedData data = XmlParsedData.getParsedData(view, true);
 247		if(data == null)return;
 248
 249		String newTag;
 250		String closingTag;
 251
 252		// compute content to insert
 253		if(!reallyShowEditTagDialog)
 254		{
 255			StringBuilder[] openclose = EditTagDialog.composeTag(data, elementDecl, namespaces, namespacesToInsert, true);
 256			// composeTag doesn't insert the <
 257			openclose[0].insert(0, '<');
 258			newTag = openclose[0].toString();
 259			closingTag = openclose[1].toString();
 260		}
 261		else
 262		{
 263			EditTagDialog dialog = new EditTagDialog(view,elementName,elementDecl,
 264				new HashMap<String, Object>(),elementDecl.empty,
 265				elementDecl.completionInfo.entityHash,
 266				data.getSortedIds(),data.html, namespaces, namespacesToInsert);
 267
 268			newTag = dialog.getNewTag();
 269			
 270			if(dialog.isEmpty())
 271				closingTag = "";
 272			else
 273				closingTag = "</" + elementName + ">";
 274		}
 275
 276		// really insert now
 277		// TODO: there is no support for Recorder, unlike in XmlCompletion
 278		if(newTag != null)
 279		{
 280			JEditTextArea textArea = view.getTextArea();
 281			if (insideTag != null) textArea.setSelectedText(insideTag, "");
 282			Selection[] selection = textArea.getSelection();
 283
 284			if(selection.length > 0)
 285			{
 286				try
 287				{
 288					buffer.beginCompoundEdit();
 289					for(int i = 0; i < selection.length; i++)
 290					{
 291						buffer.insert(selection[i].getStart(),
 292							newTag);
 293						buffer.insert(selection[i].getEnd(),
 294							closingTag);
 295					}
 296				}
 297				finally
 298				{
 299					buffer.endCompoundEdit();
 300				}
 301			}
 302			else
 303			{
 304				textArea.setSelectedText(newTag);
 305				int caret = textArea.getCaretPosition();
 306				textArea.setSelectedText(closingTag);
 307				textArea.setCaretPosition(caret);
 308			}
 309
 310			textArea.selectNone();
 311			textArea.requestFocus();
 312		}
 313	} //}}}
 314
 315	//{{{ insertClosingTag() method
 316	public static void insertClosingTag(View view)
 317	{
 318		JEditTextArea textArea = view.getTextArea();
 319		Buffer buffer = view.getBuffer();
 320
 321		if(XmlPlugin.isDelegated(textArea) || !buffer.isEditable())
 322		{
 323			view.getToolkit().beep();
 324			return;
 325		}
 326
 327		XmlParsedData data = XmlParsedData.getParsedData(view,false);
 328
 329		TagParser.Tag tag = TagParser.findLastOpenTag(
 330			buffer.getText(0,textArea.getCaretPosition()),
 331			textArea.getCaretPosition(),data);
 332
 333		if(tag != null)
 334			textArea.setSelectedText("</" + tag.tag + ">");
 335		else
 336		{
 337			view.getToolkit().beep();
 338		}
 339	} //}}}
 340
 341	// {{{ splitTag() method
 342	/**
 343	 * Splits tag at caret, so that attributes are on separate lines.
 344	 */
 345	public static void splitTag(Tag tag, JEditTextArea textArea, CharSequence text) {
 346		View view = textArea.getView();
 347		textArea.setSelection(new Selection.Range(tag.start, tag.end));
 348		XmlParsedData data = XmlParsedData.getParsedData(view,true);
 349		Selection[] s = textArea.getSelection();
 350		if (s.length != 1) return;
 351		Selection sel = s[0];
 352		if (sel.getEnd() - sel.getStart() < 2) return;
 353		int line = textArea.getLineOfOffset(tag.start);
 354		int lineStartOffset = textArea.getLineStartOffset(line);
 355		int indentChars = 2 + sel.getStart() - lineStartOffset;
 356		StringBuffer indent = new StringBuffer("\n");
 357		for (int i=indentChars; i>=0; --i) {
 358			indent.append(" ");
 359		}
 360
 361		TreePath path = data.getTreePathForPosition(textArea.getCaretPosition());
 362		int count = path.getPathCount();
 363		DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getPathComponent(count-1);
 364		StringBuffer result = new StringBuffer();
 365		Object user_object = node.getUserObject();
 366		if (user_object instanceof XmlTag) {
 367			result.append('<');
 368			result.append(tag.tag);
 369			List<Attr> attrs = TagParser.getAttrs(text,tag);
 370			count = attrs.size();
 371			if(count>0)result.append(' ');
 372			for (int i=0; i<count; ++i) {
 373				Attr a = attrs.get(i);
 374				result.append(a.name).append(" = ").append(a.val);
 375				if (i < count ) result.append(indent.toString());
 376			}
 377			result.append('>');
 378		}
 379		else if (user_object instanceof SideKickAsset) {
 380			SideKickElement element = ((SideKickAsset)user_object).getElement();
 381			if (element instanceof HtmlDocument.Tag) {
 382				HtmlDocument.Tag htmlTag = (HtmlDocument.Tag)element;
 383				result.append(htmlTag.tagStart);
 384				result.append(htmlTag.tagName);
 385				List attrs = ((HtmlDocument.Tag)element).attributeList.attributes;
 386				if(attrs.size()>0)result.append(" ");
 387				for (Iterator it = attrs.iterator(); it.hasNext(); ) {
 388					HtmlDocument.Attribute attr = (HtmlDocument.Attribute)it.next();
 389					result.append(attr.name);
 390					if (attr.hasValue) {
 391						String value = attr.value;
 392						if (!value.startsWith("\"")) {
 393							value = "\"" + value;
 394						}
 395						if (!value.endsWith("\"")) {
 396							value += "\"";
 397						}
 398						result.append(" = ").append(value);
 399					}
 400					if (it.hasNext()) {
 401						result.append(indent.toString());
 402					}
 403				}
 404				result.append(htmlTag.tagEnd);
 405			}
 406		}
 407		else {
 408			return;
 409		}
 410		textArea.replaceSelection(result.toString());
 411	}// }}}
 412
 413	// {{{ join() method
 414	/**
 415	 * If inside a HTML or XML, join attributes and tagname all on one line. 
 416	 * Otherwise do nothing.
 417	 */
 418	static public void join (View view) {
 419		JEditTextArea textArea = view.getTextArea();
 420		Buffer buffer = view.getBuffer();
 421		int pos = textArea.getCaretPosition();
 422		CharSequence text = buffer.getSegment(0,buffer.getLength());
 423		Tag tag = TagParser.getTagAtOffset(text, pos);
 424		if (tag == null) return; // we're not in a tag;
 425		
 426		// select it
 427		textArea.setSelection(new Selection.Range(tag.start, tag.end));	
 428		XmlParsedData data = XmlParsedData.getParsedData(view,true);
 429		if(data==null)return;
 430		Selection[] s = textArea.getSelection();
 431		if (s.length != 1) return;
 432		Selection sel = s[0];
 433		if (sel.getEnd() - sel.getStart() < 2) return;
 434		int line = textArea.getLineOfOffset(tag.start);
 435		TreePath path = data.getTreePathForPosition(textArea.getCaretPosition());
 436		int count = path.getPathCount();
 437		DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getPathComponent(count-1);
 438		StringBuffer result = new StringBuffer();
 439		Object user_object = node.getUserObject();
 440		if (user_object instanceof XmlTag) {
 441			result.append('<');
 442			result.append(tag.tag);
 443			List<Attr> attrs = TagParser.getAttrs(text,tag);
 444			for(Attr a: attrs)
 445			{
 446				result.append(' ').append(a.name).append(" = ").append(a.val);
 447			}
 448			if(tag.type == TagParser.T_STANDALONE_TAG)
 449			{
 450				result.append('/');
 451			}
 452			result.append('>');
 453		}
 454		else if (user_object instanceof SideKickAsset) {
 455			SideKickElement element = ((SideKickAsset)user_object).getElement();
 456			if (element instanceof HtmlDocument.Tag) {
 457				HtmlDocument.Tag htmlTag = (HtmlDocument.Tag)element;
 458				result.append(htmlTag.tagStart);
 459				result.append(htmlTag.tagName).append(" ");
 460				List attrs = ((HtmlDocument.Tag)element).attributeList.attributes;
 461				for (Iterator it = attrs.iterator(); it.hasNext(); ) {
 462					HtmlDocument.Attribute attr = (HtmlDocument.Attribute)it.next();
 463					result.append(attr.name);
 464					if (attr.hasValue) {
 465						String value = attr.value;
 466						if (!value.startsWith("\"")) {
 467							value = "\"" + value;
 468						}
 469						if (!value.endsWith("\"")) {
 470							value += "\"";
 471						}
 472						result.append(" = ").append(value);
 473					}
 474				}
 475				result.append(htmlTag.tagEnd.replaceAll("\\s", ""));
 476			}
 477		}
 478		else {
 479			return;
 480		}
 481		textArea.replaceSelection(result.toString());
 482	}// }}}
 483
 484	//{{{ split() method
 485	/**
 486	 * If inside a tag, calls splitTagAtCaret.
 487	 *
 488	 * If the DTD allows this tag to be split, split at the cursor.
 489	 *
 490	 * Note that this can be used to do a kind of 'fast-editing', eg when
 491	 * editing an HTML &lt;p&gt; this will insert an end tag (if necessary)
 492	 * and then place the cursor inside a new &lt;p&gt;.
 493	 *
 494	 * TODO: Syntax Checking
 495	 */
 496	public static void split(View view)
 497	{
 498		JEditTextArea textArea = view.getTextArea();
 499		Buffer buffer = view.getBuffer();
 500		int pos = textArea.getCaretPosition();
 501		CharSequence text = buffer.getSegment(0,buffer.getLength());
 502		Tag t = TagParser.getTagAtOffset(text, pos);
 503		if (t != null && t.end != pos) { // getTagAtOffset will return a tag if you are just after it
 504			splitTag(t, textArea, text);
 505			return;
 506		}
 507		if(XmlPlugin.isDelegated(textArea) || !buffer.isEditable())
 508		{
 509			view.getToolkit().beep();
 510			return;
 511		}
 512
 513		XmlParsedData data = XmlParsedData.getParsedData(view, true);
 514		if(data == null)return;
 515
 516		TagParser.Tag tag = TagParser.findLastOpenTag(
 517			buffer.getText(0,textArea.getCaretPosition()),
 518			textArea.getCaretPosition(),data);
 519
 520		if(tag != null)
 521		{
 522			Segment wsBefore = new Segment();
 523			pos = getPrevNonWhitespaceChar( buffer, tag.start - 1 ) + 1;
 524			buffer.getText( pos, tag.start-pos, wsBefore );
 525			//System.err.println( "wsBefore: [" + wsBefore + "]" );
 526
 527			Segment wsAfter = new Segment();
 528			pos = getNextNonWhitespaceChar( buffer, tag.end );
 529			//Need to do this otherwise the ws in empty tags
 530			//just gets bigger and bigger and bigger...
 531			pos = Math.min( pos, textArea.getCaretPosition() );
 532			buffer.getText( tag.end, pos - tag.end, wsAfter );
 533			//System.err.println( "wsAfter: [" + wsAfter + "]" );
 534
 535			int lineStart = buffer.getLineStartOffset(
 536				buffer.getLineOfOffset( tag.start ) );
 537			String tagIndent = buffer.getText( lineStart, tag.start-lineStart );
 538
 539			//Note that the number of blank lines BEFORE the end tag will
 540			//be the number AFTER the start tag, for symmetry's sake.
 541			int crBeforeEndTag = countNewLines( wsAfter );
 542			int crAfterEndTag = countNewLines( wsBefore );
 543
 544			StringBuffer insert = new StringBuffer();
 545			if ( crBeforeEndTag>0 ) {
 546				for ( int i=0; i<crBeforeEndTag; i++ ) {
 547					insert.append( "\n" );
 548				}
 549				insert.append(tagIndent);
 550			}
 551			insert.append("</" + tag.tag + ">");
 552			insert.append( wsBefore );
 553			insert.append("<" + tag.tag + ">");
 554			insert.append( wsAfter );
 555
 556			//Move the insertion point to here
 557			textArea.setSelectedText(insert.toString());
 558		}
 559	}
 560	//}}}
 561
 562	//{{{ removeTags() method
 563	public static void removeTags(Buffer buffer)
 564	{
 565		if(!buffer.isEditable())
 566		{
 567			Toolkit.getDefaultToolkit().beep();
 568			return;
 569		}
 570
 571		int off = 0;
 572		int len = buffer.getLength();
 573		long startTime = System.currentTimeMillis();
 574		int total = 0;
 575		try
 576		{
 577			buffer.beginCompoundEdit();
 578
 579			CharSequence text = buffer.getSegment(off,len);
 580			for (int i = TagParser.indexOf(text, '<',0);
 581				i != -1; i = TagParser.indexOf(text,'<', ++i))
 582			{
 583				TagParser.Tag tag = TagParser.getTagAtOffset(text,i + 1);
 584				if (tag == null)
 585					continue;
 586				else
 587				{
 588					int length = tag.end - tag.start;
 589					buffer.remove(tag.start - total,length);
 590					total += length;
 591				}
 592			}
 593		}
 594		finally
 595		{
 596			buffer.endCompoundEdit();
 597		}
 598		long endTime = System.currentTimeMillis();
 599		Log.log(Log.DEBUG, XmlActions.class,
 600			"removeTags time: " + (endTime - startTime) + " ms");
 601	} //}}}
 602
 603	//{{{ matchTag() method
 604	public static void matchTag(JEditTextArea textArea) {
 605	    int caretPos = textArea.getCaretPosition();
 606	    // Check if I am near one of the regular brackets
 607	    for (int i=caretPos-1; i<caretPos+3; ++i) try
 608	    {
 609		String s = textArea.getText(i,1);
 610		if (brackets.indexOf(s) > -1)
 611		{
 612		    textArea.goToMatchingBracket();
 613		    return;
 614		}
 615	    }
 616	    catch (ArrayIndexOutOfBoundsException aiobe) {}
 617	    xmlMatchTag(textArea);
 618	}
 619	// }}}
 620
 621	//{{{ xmlMatchTag() method
 622	public static void xmlMatchTag(JEditTextArea textArea)
 623	{
 624		CharSequence text = textArea.getBuffer().getSegment(0,textArea.getBufferLength());
 625		int caret = textArea.getCaretPosition();
 626
 627		// De-Select previous selection
 628		// textArea.select(caret, caret);
 629		textArea.setSelection(new Selection.Range(caret, caret));
 630
 631		// Move cursor inside tag, to help with matching
 632		try { if (text.charAt(caret) == '<')
 633			textArea.goToNextCharacter(false);
 634		} catch (Exception e ) {}
 635
 636		TagParser.Tag tag = TagParser.getTagAtOffset(text,textArea.getCaretPosition());
 637		if (tag != null)
 638		{
 639			TagParser.Tag matchingTag = TagParser.getMatchingTag(text, tag);
 640			if (matchingTag != null)
 641			{
 642				EditBus.send(new PositionChanging(textArea));
 643				textArea.setSelection(new Selection.Range(
 644					matchingTag.start, matchingTag.end
 645				));
 646				textArea.moveCaretPosition(matchingTag.end-1);
 647			}
 648			else
 649				textArea.getToolkit().beep();
 650		}
 651	} //}}}
 652
 653	//{{{ selectElement() method
 654	/**
 655	 * Selects whole element, can be called repeatedly to select
 656	 * parent element of selected element. If no element found, calls
 657	 * "Select Code Block" action -- analogy to the
 658	 * "Select Matching Tag or Bracket" action
 659	 */
 660	public static void selectElement(JEditTextArea textArea)
 661	{
 662
 663		final int step = 2;
 664
 665		CharSequence text = textArea.getBuffer().getSegment(0, textArea.getBufferLength());
 666		boolean isSel = textArea.getSelectionCount() == 1;
 667		int caret, pos;
 668
 669		if (isSel)
 670			caret = pos = textArea.getSelection(0).getEnd();
 671		else
 672			caret = pos = textArea.getCaretPosition();
 673
 674		while (pos >= 0)
 675		{
 676			TagParser.Tag tag = TagParser.getTagAtOffset(text, pos);
 677
 678			if (tag != null)
 679			{
 680				TagParser.Tag matchingTag = TagParser.getMatchingTag(text, tag);
 681				if (matchingTag != null
 682					&& ((tag.type == TagParser.T_START_TAG && matchingTag.end >= caret)
 683					|| (!isSel && tag.type == TagParser.T_END_TAG && tag.end >= caret)))
 684				{
 685					if (tag.start < matchingTag.end)
 686					{
 687						textArea.setSelection(
 688							new Selection.Range(tag.start, matchingTag.end));
 689						textArea.moveCaretPosition(
 690							matchingTag.end);
 691					}
 692					else
 693					{
 694						textArea.setSelection(
 695							new Selection.Range(matchingTag.start,tag.end));
 696						textArea.moveCaretPosition(
 697							matchingTag.start);
 698					}
 699					break;
 700				}
 701				else if (!isSel && tag.type == TagParser.T_STANDALONE_TAG)
 702				{
 703					textArea.setSelection(
 704						new Selection.Range(tag.start, tag.end));
 705					textArea.moveCaretPosition(tag.end);
 706					break;
 707				}
 708				else
 709				{
 710					// No tag found - skip as much as posible
 711					// NOTE: checking if matchingTag.start < tag.start
 712					// shouldn't be necesary, but TagParser.getMatchingTag method
 713					// sometimes finds matching tag only for start,
 714					// tag, e.g.: "<x> => </x>"
 715					pos = (matchingTag != null && matchingTag.start < tag.start)
 716							? matchingTag.start
 717							: tag.start;
 718				}
 719			}
 720			pos -= step;
 721		}
 722
 723		if (pos <= 0) {
 724			textArea.selectBlock();
 725		}
 726
 727	} //}}}
 728
 729	//{{{ selectTag() method
 730	/**
 731	 *  Selects tag at caret. Also returns it. Returns null if there is no tag.
 732	 * */
 733	public static Tag selectTag(JEditTextArea textArea) {
 734		CharSequence text = textArea.getBuffer().getSegment(0, textArea.getBufferLength());
 735		int pos = textArea.getCaretPosition();
 736		Tag t = TagParser.getTagAtOffset(text, pos);
 737		if (t == null) return null;
 738		textArea.setSelection(new Selection.Range(t.start, t.end));
 739		return t;
 740	} // }}}
 741
 742	//{{{ selectBetweenTags() method
 743	/**
 744	 * Selects content of an element, can be called repeatedly
 745	 */
 746	public static void selectBetweenTags(JEditTextArea textArea)
 747	{
 748
 749		final int step = 2;
 750
 751		CharSequence text = textArea.getBuffer().getSegment(0,textArea.getBufferLength());
 752		boolean isSel = textArea.getSelectionCount() == 1;
 753		int caret, pos;
 754
 755		if (isSel)
 756			caret = pos = textArea.getSelection(0).getEnd();
 757		else
 758			caret = pos = textArea.getCaretPosition();
 759
 760		while (pos >= 0)
 761		{
 762			TagParser.Tag tag = TagParser.getTagAtOffset(text, pos);
 763			if (tag != null)
 764			{
 765				TagParser.Tag matchingTag = TagParser.getMatchingTag(text, tag);
 766				if (tag.type == TagParser.T_START_TAG)
 767				{
 768					if (matchingTag != null
 769						&& (matchingTag.start > caret
 770						|| (!isSel && matchingTag.start == caret)))
 771					{
 772						if (tag.start < matchingTag.end)
 773						{
 774							textArea.setSelection(
 775								new Selection.Range(tag.end, matchingTag.start));
 776							textArea.moveCaretPosition(
 777								matchingTag.start);
 778						}
 779						else
 780						{
 781							textArea.setSelection(
 782								new Selection.Range(matchingTag.end,tag.start));
 783							textArea.moveCaretPosition(
 784								matchingTag.end);
 785						}
 786						break;
 787					}
 788					else
 789					{
 790						pos = tag.start - step;
 791						continue;
 792					}
 793				}
 794				else
 795				{
 796					// No tag found - skip as much as posible
 797					// NOTE: checking if matchingTag.start < tag.start
 798					// shouldn't be necesary, but TagParser.getMatchingTag method
 799					// sometimes finds matching tag only for start,
 800					// tag, e.g.: "<x> => </x>"
 801					pos = (matchingTag != null && matchingTag.start < tag.start)
 802							? matchingTag.start
 803							: tag.start;
 804				}
 805			}
 806			pos -= step;
 807		}
 808
 809		if (pos <= 0) {
 810			textArea.getToolkit().beep();
 811		}
 812	}
 813	// }}}
 814
 815	//{{{ insertClosingTagKeyTyped() method
 816	public static void insertClosingTagKeyTyped(View view)
 817	{
 818		JEditTextArea textArea = view.getTextArea();
 819
 820		Macros.Recorder recorder = view.getMacroRecorder();
 821		textArea.userInput('>');
 822
 823		Buffer buffer = view.getBuffer();
 824
 825		if(XmlPlugin.isDelegated(textArea) || !buffer.isEditable()
 826			|| !closeCompletionOpen)
 827			return;
 828
 829		XmlParsedData data = XmlParsedData.getParsedData(view,false);
 830		if(data==null)return;
 831
 832
 833		int caret = textArea.getCaretPosition();
 834
 835		CharSequence text = buffer.getSegment(0,caret);
 836
 837		TagParser.Tag tag = TagParser.getTagAtOffset(text,caret - 1);
 838		if(tag == null)
 839			return;
 840
 841		ElementDecl decl = data.getElementDecl(tag.tag,tag.start+1);
 842		if(tag.type == TagParser.T_STANDALONE_TAG
 843			|| (decl != null && decl.empty))
 844			return;
 845
 846		tag = TagParser.findLastOpenTag(text,caret,data);
 847
 848		if(tag != null)
 849		{
 850			String insert = "</" + tag.tag + ">";
 851			if(recorder != null)
 852				recorder.recordInput(insert,false);
 853			textArea.setSelectedText(insert);
 854
 855			String code = "textArea.setCaretPosition("
 856				+ "textArea.getCaretPosition() - "
 857				+ insert.length() + ");";
 858			if(recorder != null)
 859				recorder.record(code);
 860			BeanShell.eval(view,BeanShell.getNameSpace(),code);
 861		}
 862	} //}}}
 863
 864	//{{{ completeClosingTag() method
 865	public static void completeClosingTag(View view, boolean insertSlash)
 866	{
 867		JEditTextArea textArea = view.getTextArea();
 868
 869		Macros.Recorder recorder = view.getMacroRecorder();
 870
 871		if(insertSlash)
 872		{
 873			if(recorder != null)
 874				recorder.recordInput(1,'/',false);
 875			textArea.userInput('/');
 876		}
 877
 878		JEditBuffer buffer = textArea.getBuffer();
 879
 880		if(XmlPlugin.isDelegated(textArea))
 881			return;
 882
 883		XmlParsedData data = XmlParsedData.getParsedData(view, true);
 884		if(data==null)return;
 885
 886		if(!buffer.isEditable() || !closeCompletion)
 887		{
 888			return;
 889		}
 890
 891		int caret = textArea.getCaretPosition();
 892		if(caret == 1)
 893			return;
 894
 895		CharSequence text = buffer.getSegment(0,buffer.getLength());
 896
 897		if(text.charAt(caret - 2) != '<')
 898			return;
 899
 900		// check if caret is inside a tag
 901		if(TagParser.getTagAtOffset(text,caret) != null)
 902			return;
 903
 904		TagParser.Tag tag = TagParser.findLastOpenTag(text,caret - 2,data);
 905
 906		if(tag != null)
 907		{
 908			String insert = tag.tag + ">";
 909			if(recorder != null)
 910				recorder.recordInput(insert,false);
 911			textArea.setSelectedText(insert);
 912		}
 913	} //}}}
 914
 915	//{{{ charactersToEntities() methods
 916	public static String charactersToEntities(String s, Map hash)
 917	{
 918		final String specialChars = "<>&\"\\";
 919		StringBuffer buf = new StringBuffer();
 920		for(int i = 0; i < s.length(); i++)
 921		{
 922			char ch = s.charAt(i);
 923			if(ch >= 0x7f || specialChars.indexOf(ch) > -1)
 924			{
 925				Character c = new Character(ch);
 926				String entity = (String)hash.get(c);
 927				if(entity != null)
 928				{
 929					buf.append('&');
 930					buf.append(entity);
 931					buf.append(';');
 932
 933					continue;
 934				}
 935			}
 936
 937			buf.append(ch);
 938		}
 939
 940		return buf.toString();
 941	}
 942
 943	public static void charactersToEntities(View view)
 944	{
 945		Buffer buffer = view.getBuffer();
 946		JEditTextArea textArea = view.getTextArea();
 947
 948		if(XmlPlugin.isDelegated(textArea) || !buffer.isEditable())
 949		{
 950			view.getToolkit().beep();
 951			return;
 952		}
 953
 954		XmlParsedData data = XmlParsedData.getParsedData(view, true);
 955		if(data == null)return;
 956		
 957		Map entityHash = data.entityHash;
 958
 959		Selection[] selection = textArea.getSelection();
 960		for(int i = 0; i < selection.length; i++)
 961		{
 962			String old = textArea.getSelectedText(selection[i]);
 963			textArea.setSelectedText(selection[i], charactersToEntities(old, entityHash));
 964		}
 965	} //}}}
 966
 967	//{{{ entitiesToCharacters() methods
 968	public static String entitiesToCharacters(String s, Map hash)
 969	{
 970		StringBuffer buf = new StringBuffer();
 971		for(int i = 0; i < s.length(); i++)
 972		{
 973			char ch = s.charAt(i);
 974			if(ch == '&')
 975			{
 976				int index = s.indexOf(';',i);
 977				if(index != -1)
 978				{
 979					String entityName = s.substring(i + 1,index);
 980					Character c = (Character)hash.get(entityName);
 981					if(c != null)
 982					{
 983						buf.append(c.charValue());
 984						i = index;
 985						continue;
 986					}
 987				}
 988			}
 989
 990			buf.append(ch);
 991		}
 992
 993		return buf.toString();
 994	}
 995
 996	public static void entitiesToCharacters(View view)
 997	{
 998		Buffer buffer = view.getBuffer();
 999		JEditTextArea textArea = view.getTextArea();
1000
1001		if(XmlPlugin.isDelegated(textArea) || !buffer.isEditable())
1002		{
1003			view.getToolkit().beep();
1004			return;
1005		}
1006
1007		XmlParsedData data = XmlParsedData.getParsedData(view, true);
1008		if(data == null)return;
1009
1010		Map entityHash = data.entityHash;
1011
1012		Selection[] selection = textArea.getSelection();
1013		for(int i = 0; i < selection.length; i++)
1014		{
1015			textArea.setSelectedText(selection[i],
1016				entitiesToCharacters(textArea.getSelectedText(
1017				selection[i]),entityHash));
1018		}
1019	} //}}}
1020
1021	//{{{ getStandaloneEnd() method
1022	public static String getStandaloneEnd()
1023	{
1024		return (standaloneExtraSpace ? " />" : "/>");
1025	} //}}}
1026
1027	//{{{ generateDTD() method
1028	public static void generateDTD(View view)
1029	{
1030		JEditTextArea textArea = view.getTextArea();
1031		Buffer buffer = view.getBuffer();
1032		String text = buffer.getText(0,buffer.getLength());
1033		String encoding = buffer.getStringProperty(Buffer.ENCODING);
1034		// String declaration = "<?xml version=\"1.0\" encoding=\"" + encoding + "\"?>\n";
1035		String dtd = DTDGenerator.write(view, text);
1036		StatusBar status = view.getStatus();
1037		if (dtd.trim().equals(""))
1038			status.setMessageAndClear("Document produced an empty DTD");
1039		else
1040		{
1041			Buffer newbuffer = jEdit.newFile(view);
1042			newbuffer.setMode("sgml");
1043			newbuffer.setStringProperty(Buffer.ENCODING, encoding);
1044			// newbuffer.insert(0, declaration + dtd);
1045			newbuffer.insert(0, dtd);
1046			status.updateBufferStatus();
1047		}
1048	}
1049	//}}}
1050
1051	//{{{ openSchema() method
1052	public static void openSchema(View view)
1053	{
1054		JEditTextArea textArea = view.getTextArea();
1055		Buffer buffer = view.getBuffer();
1056		
1057		String schemaURL = buffer.getStringProperty(SchemaMappingManager.BUFFER_SCHEMA_PROP);
1058		if(schemaURL == null)
1059		{
1060			schemaURL = buffer.getStringProperty(SchemaMappingManager.BUFFER_AUTO_SCHEMA_PROP);
1061		}
1062		if(schemaURL != null){
1063			Buffer newbuffer = jEdit.openFile(view,schemaURL);
1064		}
1065	}
1066	//}}}
1067
1068	//{{{ copyXPath() method
1069	public static void copyXPath(View view)
1070	{
1071		XmlParsedData data = XmlParsedData.getParsedData(view, true);
1072		
1073		if(data == null)
1074		{
1075			view.getToolkit().beep();
1076			return;
1077		}
1078		
1079		JEditTextArea textArea = view.getTextArea();
1080	
1081		int pos = textArea.getCaretPosition();
1082		
1083		String xpath = data.getXPathForPosition(pos);
1084		
1085		if(xpath!=null)
1086		{
1087			Registers.getRegister('$').setTransferable(new StringSelection(xpath));
1088		}
1089		
1090	}
1091	//}}}
1092
1093	// {{{ non-public methods
1094
1095	//{{{ propertiesChanged() method
1096	static void propertiesChanged()
1097	{
1098		closeCompletion = jEdit.getBooleanProperty(
1099			"xml.close-complete");
1100		
1101		if(closeCompletionOpen != jEdit.getBooleanProperty(
1102				"xml.close-complete-open"))
1103		{
1104			closeCompletionOpen = !closeCompletionOpen;
1105			SwingUtilities.invokeLater(new Runnable(){
1106			public void run(){
1107				/* with the jEdit 5 Keymap API, it's no more possible
1108				 * to define a shortcut with action-name.shortcut=X in the XML.props
1109				 * => so set it programatically !
1110				 * */
1111				try {
1112					// test the presence of the jEdit 5 KeyMap API
1113					Class.forName("org.jedit.keymap.Keymap");
1114	
1115					// OK, the KeyMap API exists
1116					String shortcut = ">";
1117					// shortcut will be undefined when closeCompletionOpen is not active
1118					String actionName = closeCompletionOpen? "xml-insert-closing-tag" : null;
1119				
1120					Class c = Class.forName("xml.JEdit5Support");
1121					Method m = c.getDeclaredMethod("setShortcut", new Class[]{String.class,String.class});
1122					m.invoke(null, actionName, shortcut);
1123				} catch (ClassNotFoundException e) {
1124					// NOPMD: don't log an error when org.jedit.keymap.Keymap is not found
1125				} catch (Exception e) {
1126					Log.log(Log.WARNING, XmlActions.class, "error setting shortcut to implement 'insert closing tag when opening tag is typed'",e);
1127				}
1128			}});
1129
1130		}
1131		standaloneExtraSpace = jEdit.getBooleanProperty(
1132			"xml.standalone-extra-space");
1133	} //}}}
1134
1135	//{{{ getPrevNonWhitespaceChar() method
1136	/**
1137	 * Find the offset of the previous non whitespace character.
1138	 */
1139	private static int getPrevNonWhitespaceChar( Buffer buf, int start )
1140	{
1141		//It might be more efficient if there were a getCharAt() method on the buffer?
1142
1143		//This is trying to conserve memory by not creating strings all the time
1144		Segment seg = new Segment( new char[1], 0, 1 );
1145		int pos = start;
1146		while ( pos>0 )
1147		{
1148			buf.getText( pos, 1, seg );
1149			if ( ! Character.isWhitespace( seg.first() ) )
1150				break;
1151			pos--;
1152		}
1153		return pos;
1154	}
1155	//}}}
1156
1157	//{{{ getNextNonWhitespaceChar() method
1158	/**
1159	 * Find the offset of the next non-whitespace character.
1160	 */
1161	private static int getNextNonWhitespaceChar( Buffer buf, int start )
1162	{
1163		//It might be more efficient if there were a getCharAt() method on the buffer?
1164
1165		//This is trying to conserve memory by not creating strings all the time
1166		Segment seg = new Segment( new char[1], 0, 1 );
1167		int pos = start;
1168		while ( pos < buf.getLength() )
1169		{
1170			buf.getText( pos, 1, seg );
1171			//System.err.println( "NNWS Testing: " + seg.first() + " at " + pos );
1172			if ( ! Character.isWhitespace( seg.first() ) )
1173				break;
1174			pos++;
1175		}
1176
1177		return pos;
1178	}
1179	//}}}
1180
1181	//{{{ countNewLines() method
1182	/**
1183	 * Count the number of newlines in the given segment.
1184	 */
1185	private static int countNewLines( Segment seg )
1186	{
1187		//It might help if there were a getCharAt() method on the buffer
1188		//or the buffer itself implemented CharaterIterator?
1189
1190		//This is trying to conserve memory by not creating strings all the time
1191		int count = 0;
1192		for ( int pos = seg.getBeginIndex(); pos<seg.getEndIndex(); pos++ ) {
1193			if ( seg.array[pos] == '\n' ) {
1194				count++;
1195			}
1196		}
1197		return count;
1198	}
1199	//}}}
1200
1201	//}}}
1202} // }}}