PageRenderTime 1474ms CodeModel.GetById 1416ms app.highlight 51ms RepoModel.GetById 1ms app.codeStats 0ms

/jEdit/tags/jedit-4-5-pre1/org/gjt/sp/jedit/TextUtilities.java

#
Java | 1044 lines | 632 code | 81 blank | 331 comment | 173 complexity | 27f2c63324ac9d98398c1fdaee8df77f MD5 | raw file
   1/*
   2 * TextUtilities.java - Various text functions
   3 * Copyright (C) 1998, 2005 Slava Pestov
   4 * :tabSize=8:indentSize=8:noTabs=false:
   5 * :folding=explicit:collapseFolds=1:
   6 *
   7 * This program is free software; you can redistribute it and/or
   8 * modify it under the terms of the GNU General Public License
   9 * as published by the Free Software Foundation; either version 2
  10 * of the License, or any later version.
  11 *
  12 * This program is distributed in the hope that it will be useful,
  13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  15 * GNU General Public License for more details.
  16 *
  17 * You should have received a copy of the GNU General Public License
  18 * along with this program; if not, write to the Free Software
  19 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
  20 */
  21
  22package org.gjt.sp.jedit;
  23
  24//{{{ Imports
  25import java.util.*;
  26import javax.swing.text.Segment;
  27import org.gjt.sp.jedit.buffer.JEditBuffer;
  28import org.gjt.sp.jedit.syntax.*;
  29import org.gjt.sp.util.StandardUtilities;
  30//}}}
  31
  32/**
  33 * Contains several text manipulation methods.
  34 *
  35 * <ul>
  36 * <li>Bracket matching
  37 * <li>Word start and end offset calculation
  38 * <li>String comparison
  39 * <li>Converting tabs to spaces and vice versa
  40 * <li>Wrapping text
  41 * <li>String case conversion
  42 * </ul>
  43 *
  44 * @author Slava Pestov
  45 * @version $Id: TextUtilities.java 19412 2011-03-01 15:07:08Z kpouer $
  46 */
  47public class TextUtilities
  48{
  49	// to avoid slowdown with large files; only scan 10000 lines either way
  50	public static final int BRACKET_MATCH_LIMIT = 10000;
  51	public static final int WHITESPACE = 0;
  52	public static final int WORD_CHAR = 1;
  53	public static final int SYMBOL = 2;
  54
  55
  56	//{{{ getTokenAtOffset() method
  57	/**
  58	 * Returns the token that contains the specified offset.
  59	 * @param tokens The token list
  60	 * @param offset The offset
  61	 * @since jEdit 4.0pre3
  62	 */
  63	public static Token getTokenAtOffset(Token tokens, int offset)
  64	{
  65		if(offset == 0 && tokens.id == Token.END)
  66			return tokens;
  67
  68		for(;;)
  69		{
  70			if(tokens.id == Token.END)
  71				throw new ArrayIndexOutOfBoundsException("offset > line length");
  72
  73			if(tokens.offset + tokens.length > offset)
  74				return tokens;
  75			else
  76				tokens = tokens.next;
  77		}
  78	} //}}}
  79
  80	//{{{ getComplementaryBracket() method
  81	/**
  82	 * Given an opening bracket, return the corresponding closing bracket
  83	 * and store true in <code>direction[0]</code>. Given a closing bracket,
  84	 * return the corresponding opening bracket and store false in
  85	 * <code>direction[0]</code>. Otherwise, return <code>\0</code>.
  86	 * @since jEdit 4.3pre2
  87	 */
  88	public static char getComplementaryBracket(char ch, boolean[] direction)
  89	{
  90		switch(ch)
  91		{
  92		case '(': if (direction != null) direction[0] = true;  return ')';
  93		case ')': if (direction != null) direction[0] = false; return '(';
  94		case '[': if (direction != null) direction[0] = true;  return ']';
  95		case ']': if (direction != null) direction[0] = false; return '[';
  96		case '{': if (direction != null) direction[0] = true;  return '}';
  97		case '}': if (direction != null) direction[0] = false; return '{';
  98		case '<': if (direction != null) direction[0] = true;  return '>';
  99		case '>': if (direction != null) direction[0] = false; return '<';
 100		default:  return '\0';
 101		}
 102	} //}}}
 103
 104	//{{{ findMatchingBracket() method
 105	/**
 106	 * Returns the offset of the bracket matching the one at the
 107	 * specified offset of the buffer, or -1 if the bracket is
 108	 * unmatched (or if the character is not a bracket).
 109	 * @param buffer The buffer
 110	 * @param line The line
 111	 * @param offset The offset within that line
 112	 * @since jEdit 2.6pre1
 113	 */
 114	public static int findMatchingBracket(JEditBuffer buffer, int line, int offset)
 115	{
 116		if(offset < 0 || offset >= buffer.getLineLength(line))
 117		{
 118			throw new ArrayIndexOutOfBoundsException(offset + ":"
 119				+ buffer.getLineLength(line));
 120		}
 121
 122		Segment lineText = new Segment();
 123		buffer.getLineText(line,lineText);
 124
 125		char c = lineText.array[lineText.offset + offset];
 126		// false - backwards, true - forwards
 127		boolean[] direction = new boolean[1];
 128
 129		// corresponding character
 130		char cprime = getComplementaryBracket(c,direction);
 131
 132		if( cprime == '\0' )
 133		{ // c is no bracket
 134			return -1;
 135		}
 136
 137		// 1 because we've already 'seen' the first bracket
 138		int count = 1;
 139
 140		DefaultTokenHandler tokenHandler = new DefaultTokenHandler();
 141		buffer.markTokens(line,tokenHandler);
 142
 143		// Get the syntax token at 'offset'
 144		// only tokens with the same type will be checked for
 145		// the corresponding bracket
 146		byte idOfBracket = getTokenAtOffset(tokenHandler.getTokens(),offset).id;
 147
 148		boolean haveTokens = true;
 149
 150		int startLine = line;
 151
 152		//{{{ Forward search
 153		if(direction[0])
 154		{
 155			offset++;
 156
 157			for(;;)
 158			{
 159				for(int i = offset; i < lineText.count; i++)
 160				{
 161					char ch = lineText.array[lineText.offset + i];
 162					if(ch == c)
 163					{
 164						if(!haveTokens)
 165						{
 166							tokenHandler.init();
 167							buffer.markTokens(line,tokenHandler);
 168							haveTokens = true;
 169						}
 170						if(getTokenAtOffset(tokenHandler.getTokens(),i).id == idOfBracket)
 171							count++;
 172					}
 173					else if(ch == cprime)
 174					{
 175						if(!haveTokens)
 176						{
 177							tokenHandler.init();
 178							buffer.markTokens(line,tokenHandler);
 179							haveTokens = true;
 180						}
 181						if(getTokenAtOffset(tokenHandler.getTokens(),i).id == idOfBracket)
 182						{
 183							count--;
 184							if(count == 0)
 185								return buffer.getLineStartOffset(line) + i;
 186						}
 187					}
 188				}
 189
 190				//{{{ Go on to next line
 191				line++;
 192				if(line >= buffer.getLineCount() || (line - startLine) > BRACKET_MATCH_LIMIT)
 193					break;
 194				buffer.getLineText(line,lineText);
 195				offset = 0;
 196				haveTokens = false;
 197				//}}}
 198			}
 199		} //}}}
 200		//{{{ Backward search
 201		else
 202		{
 203			offset--;
 204
 205			for(;;)
 206			{
 207				for(int i = offset; i >= 0; i--)
 208				{
 209					char ch = lineText.array[lineText.offset + i];
 210					if(ch == c)
 211					{
 212						if(!haveTokens)
 213						{
 214							tokenHandler.init();
 215							buffer.markTokens(line,tokenHandler);
 216							haveTokens = true;
 217						}
 218						if(getTokenAtOffset(tokenHandler.getTokens(),i).id == idOfBracket)
 219							count++;
 220					}
 221					else if(ch == cprime)
 222					{
 223						if(!haveTokens)
 224						{
 225							tokenHandler.init();
 226							buffer.markTokens(line,tokenHandler);
 227							haveTokens = true;
 228						}
 229						if(getTokenAtOffset(tokenHandler.getTokens(),i).id == idOfBracket)
 230						{
 231							count--;
 232							if(count == 0)
 233								return buffer.getLineStartOffset(line) + i;
 234						}
 235					}
 236				}
 237
 238				//{{{ Go on to previous line
 239				line--;
 240				if(line < 0 || (startLine - line) > BRACKET_MATCH_LIMIT)
 241					break;
 242				buffer.getLineText(line,lineText);
 243				offset = lineText.count - 1;
 244				haveTokens = false;
 245				//}}}
 246			}
 247		} //}}}
 248
 249		// Nothing found
 250		return -1;
 251	} //}}}
 252
 253	//{{{ join() method
 254	/** Similar to perl's join() method on lists,
 255	 *    but works with all collections.
 256	 *
 257	 * @param c An iterable collection of Objects
 258	 * @param delim a string to put between each object
 259	 * @return a joined toString() representation of the collection
 260	 *
 261	 * @since jedit 4.3pre3
 262	 */
 263	public static String join(Collection<String> c, String delim)
 264	{
 265		StringBuilder retval = new StringBuilder();
 266		Iterator<String> itr = c.iterator();
 267		if (itr.hasNext())
 268			retval.append( itr.next() );
 269		else
 270			return "";
 271		while (itr.hasNext())
 272		{
 273			retval.append(delim);
 274			retval.append(itr.next());
 275		}
 276		return retval.toString();
 277	} //}}}
 278
 279	//{{{ findWordStart() methods
 280	/**
 281	 * Locates the start of the word at the specified position.
 282	 * @param line The text
 283	 * @param pos The position
 284	 * @param noWordSep Characters that are non-alphanumeric, but
 285	 * should be treated as word characters anyway
 286	 */
 287	public static int findWordStart(String line, int pos, String noWordSep)
 288	{
 289		return findWordStart(line, pos, noWordSep, true, false);
 290	}
 291
 292	/**
 293	 * Locates the start of the word at the specified position.
 294	 * @param line The text
 295	 * @param pos The position
 296	 * @param noWordSep Characters that are non-alphanumeric, but
 297	 * should be treated as word characters anyway
 298	 * @since jEdit 4.3pre15
 299	 */
 300	public static int findWordStart(CharSequence line,
 301					int pos,
 302					String noWordSep)
 303	{
 304		return findWordStart(line, pos, noWordSep, true, false, false);
 305	}
 306
 307	/**
 308	 * Locates the start of the word at the specified position.
 309	 * @param line The text
 310	 * @param pos The position
 311	 * @param noWordSep Characters that are non-alphanumeric, but
 312	 * should be treated as word characters anyway
 313	 * @param joinNonWordChars Treat consecutive non-alphanumeric
 314	 * characters as one word
 315	 * @since jEdit 4.2pre5
 316	 */
 317	public static int findWordStart(String line, int pos, String noWordSep,
 318		boolean joinNonWordChars)
 319	{
 320		return findWordStart(line,pos,noWordSep,joinNonWordChars,false);
 321	}
 322
 323	/**
 324	 * Locates the start of the word at the specified position.
 325	 * @param line The text
 326	 * @param pos The position
 327	 * @param noWordSep Characters that are non-alphanumeric, but
 328	 * should be treated as word characters anyway
 329	 * @param joinNonWordChars Treat consecutive non-alphanumeric
 330	 * characters as one word
 331	 * @param eatWhitespace Include whitespace at start of word
 332	 * @since jEdit 4.1pre2
 333	 */
 334	public static int findWordStart(String line, int pos, String noWordSep,
 335		boolean joinNonWordChars, boolean eatWhitespace)
 336	{
 337		return findWordStart(line, pos, noWordSep, joinNonWordChars,
 338			false, eatWhitespace);
 339	}
 340
 341	/**
 342	 * Locates the start of the word at the specified position.
 343	 * @param line The text
 344	 * @param pos The position
 345	 * @param noWordSep Characters that are non-alphanumeric, but
 346	 * should be treated as word characters anyway
 347	 * @param joinNonWordChars Treat consecutive non-alphanumeric
 348	 * characters as one word
 349	 * @param camelCasedWords Treat "camelCased" parts as words
 350	 * @param eatWhitespace Include whitespace at start of word
 351	 * @since jEdit 4.3pre10
 352	 */
 353	public static int findWordStart(String line, int pos, String noWordSep,
 354		boolean joinNonWordChars, boolean camelCasedWords,
 355		boolean eatWhitespace)
 356	{
 357		return findWordStart((CharSequence) line, pos, noWordSep,
 358				     joinNonWordChars, camelCasedWords,
 359				     eatWhitespace);
 360	}
 361
 362	/**
 363	 * Locates the start of the word at the specified position.
 364	 * @param line The text
 365	 * @param pos The position
 366	 * @param noWordSep Characters that are non-alphanumeric, but
 367	 * should be treated as word characters anyway
 368	 * @param joinNonWordChars Treat consecutive non-alphanumeric
 369	 * characters as one word
 370	 * @param camelCasedWords Treat "camelCased" parts as words
 371	 * @param eatWhitespace Include whitespace at start of word
 372	 * @since jEdit 4.3pre15
 373	 */
 374	public static int findWordStart(CharSequence line,
 375					int pos,
 376					String noWordSep,
 377					boolean joinNonWordChars,
 378					boolean camelCasedWords,
 379					boolean eatWhitespace)
 380	{
 381		return findWordStart(line, pos, noWordSep, joinNonWordChars, camelCasedWords, eatWhitespace, false);
 382	}
 383
 384	/**
 385	 * Locates the start of the word at the specified position.
 386	 * @param line The text
 387	 * @param pos The position
 388	 * @param noWordSep Characters that are non-alphanumeric, but
 389	 * should be treated as word characters anyway
 390	 * @param joinNonWordChars Treat consecutive non-alphanumeric
 391	 * characters as one word
 392	 * @param camelCasedWords Treat "camelCased" parts as words
 393	 * @param eatWhitespace Include whitespace at start of word
 394	 * @param eatOnlyAfterWord Eat only whitespace after a word,
 395	 * in effect this finds actual word starts even if eating
 396	 * @since jEdit 4.4pre1
 397	 */
 398	public static int findWordStart(CharSequence line, int pos, String noWordSep,
 399		boolean joinNonWordChars, boolean camelCasedWords,
 400		boolean eatWhitespace, boolean eatOnlyAfterWord)
 401	{
 402		char ch = line.charAt(pos);
 403
 404		if(noWordSep == null)
 405			noWordSep = "";
 406
 407		//{{{ the character under the cursor changes how we behave.
 408		int type = getCharType(ch, noWordSep);
 409		//}}}
 410
 411		for(int i = pos; i >= 0; i--)
 412		{
 413			char lastCh = ch;
 414			ch = line.charAt(i);
 415			switch(type)
 416			{
 417			//{{{ Whitespace...
 418			case WHITESPACE:
 419				// only select other whitespace in this case, unless eating only after words
 420				if(Character.isWhitespace(ch))
 421					break;
 422				// word char or symbol; stop, unless eating only after words
 423				else if (!eatOnlyAfterWord)
 424				{
 425					return i + 1;
 426				}
 427				// we have eaten after-word-whitespace and now continue until word start
 428				else if (Character.isLetterOrDigit(ch) || noWordSep.indexOf(ch) != -1)
 429				{
 430					type = WORD_CHAR;
 431				}
 432				else
 433					type = SYMBOL;
 434				break; //}}}
 435			//{{{ Word character...
 436			case WORD_CHAR:
 437				// stop at next last (in writing direction) upper case char if camel cased
 438				// (don't stop at every upper case char, don't treat noWordSep as word chars)
 439				if (camelCasedWords && Character.isUpperCase(ch) && !Character.isUpperCase(lastCh)
 440						&& Character.isLetterOrDigit(lastCh))
 441				{
 442					return i;
 443				}
 444				// stop at next first (in writing direction) upper case char if camel cased
 445				// (don't stop at every upper case char)
 446				else if (camelCasedWords && !Character.isUpperCase(ch) && Character.isUpperCase(lastCh))
 447				{
 448					return i + 1;
 449				}
 450				// word char; keep going
 451				else if(Character.isLetterOrDigit(ch) ||
 452					noWordSep.indexOf(ch) != -1)
 453				{
 454					break;
 455				}
 456				// whitespace; include in word if eating, but not if only eating after word
 457				else if(Character.isWhitespace(ch)
 458					&& eatWhitespace && !eatOnlyAfterWord)
 459				{
 460					type = WHITESPACE;
 461					break;
 462				}
 463				else
 464					return i + 1; //}}}
 465			//{{{ Symbol...
 466			case SYMBOL:
 467				if(!joinNonWordChars && pos != i)
 468					return i + 1;
 469
 470				// whitespace; include in word if eating, but not if only eating after word
 471				if(Character.isWhitespace(ch))
 472				{
 473					if(eatWhitespace && !eatOnlyAfterWord)
 474					{
 475						type = WHITESPACE;
 476						break;
 477					}
 478					else
 479						return i + 1;
 480				}
 481				else if(Character.isLetterOrDigit(ch) ||
 482					noWordSep.indexOf(ch) != -1)
 483				{
 484					return i + 1;
 485				}
 486				else
 487				{
 488					break;
 489				} //}}}
 490			}
 491		}
 492
 493		return 0;
 494	} //}}}
 495
 496	//{{{ findWordEnd() methods
 497	/**
 498	 * Locates the end of the word at the specified position.
 499	 * @param line The text
 500	 * @param pos The position
 501	 * @param noWordSep Characters that are non-alphanumeric, but
 502	 * should be treated as word characters anyway
 503	 */
 504	public static int findWordEnd(String line, int pos, String noWordSep)
 505	{
 506		return findWordEnd(line, pos, noWordSep, true);
 507	}
 508
 509	/**
 510	 * Locates the end of the word at the specified position.
 511	 * @param line The text
 512	 * @param pos The position
 513	 * @param noWordSep Characters that are non-alphanumeric, but
 514	 * should be treated as word characters anyway
 515	 * @since jEdit 4.3pre15
 516	 */
 517	public static int findWordEnd(CharSequence line,
 518				      int pos,
 519				      String noWordSep)
 520	{
 521		return findWordEnd(line, pos, noWordSep, true, false, false);
 522	}
 523
 524	/**
 525	 * Locates the end of the word at the specified position.
 526	 * @param line The text
 527	 * @param pos The position
 528	 * @param noWordSep Characters that are non-alphanumeric, but
 529	 * should be treated as word characters anyway
 530	 * @param joinNonWordChars Treat consecutive non-alphanumeric
 531	 * characters as one word
 532	 * @since jEdit 4.1pre2
 533	 */
 534	public static int findWordEnd(String line, int pos, String noWordSep,
 535		boolean joinNonWordChars)
 536	{
 537		return findWordEnd(line,pos,noWordSep,joinNonWordChars,false);
 538	}
 539
 540	/**
 541	 * Locates the end of the word at the specified position.
 542	 * @param line The text
 543	 * @param pos The position
 544	 * @param noWordSep Characters that are non-alphanumeric, but
 545	 * should be treated as word characters anyway
 546	 * @param joinNonWordChars Treat consecutive non-alphanumeric
 547	 * characters as one word
 548	 * @param eatWhitespace Include whitespace at end of word
 549	 * @since jEdit 4.2pre5
 550	 */
 551	public static int findWordEnd(String line, int pos, String noWordSep,
 552		boolean joinNonWordChars, boolean eatWhitespace)
 553	{
 554		return findWordEnd(line, pos, noWordSep, joinNonWordChars,
 555			false, eatWhitespace);
 556	}
 557
 558	/**
 559	 * Locates the end of the word at the specified position.
 560	 * @param line The text
 561	 * @param pos The position
 562	 * @param noWordSep Characters that are non-alphanumeric, but
 563	 * should be treated as word characters anyway
 564	 * @param joinNonWordChars Treat consecutive non-alphanumeric
 565	 * characters as one word
 566	 * @param camelCasedWords Treat "camelCased" parts as words
 567	 * @param eatWhitespace Include whitespace at end of word
 568	 * @since jEdit 4.3pre10
 569	 */
 570	public static int findWordEnd(String line, int pos, String noWordSep,
 571		boolean joinNonWordChars, boolean camelCasedWords,
 572		boolean eatWhitespace)
 573	{
 574		return findWordEnd((CharSequence)line, pos, noWordSep,
 575				   joinNonWordChars, camelCasedWords,
 576				   eatWhitespace);
 577	}
 578
 579	/**
 580	 * Locates the end of the word at the specified position.
 581	 * @param line The text
 582	 * @param pos The position
 583	 * @param noWordSep Characters that are non-alphanumeric, but
 584	 * should be treated as word characters anyway
 585	 * @param joinNonWordChars Treat consecutive non-alphanumeric
 586	 * characters as one word
 587	 * @param camelCasedWords Treat "camelCased" parts as words
 588	 * @param eatWhitespace Include whitespace at end of word
 589	 * @since jEdit 4.3pre15
 590	 */
 591	public static int findWordEnd(CharSequence line,
 592				      int pos,
 593				      String noWordSep,
 594				      boolean joinNonWordChars,
 595				      boolean camelCasedWords,
 596				      boolean eatWhitespace)
 597	{
 598		if(pos != 0)
 599			pos--;
 600
 601		char ch = line.charAt(pos);
 602
 603		if(noWordSep == null)
 604			noWordSep = "";
 605
 606		//{{{ the character under the cursor changes how we behave.
 607		int type = getCharType(ch, noWordSep);
 608		//}}}
 609
 610		for(int i = pos; i < line.length(); i++)
 611		{
 612			char lastCh = ch;
 613			ch = line.charAt(i);
 614			switch(type)
 615			{
 616			//{{{ Whitespace...
 617			case WHITESPACE:
 618				// only select other whitespace in this case
 619				if(Character.isWhitespace(ch))
 620					break;
 621				else
 622					return i; //}}}
 623			//{{{ Word character...
 624			case WORD_CHAR:
 625				// stop at next last upper case char if camel cased
 626				// (don't stop at every upper case char, don't treat noWordSep as word chars)
 627				if (camelCasedWords && i > pos + 1 && !Character.isUpperCase(ch) && Character.isLetterOrDigit(ch)
 628						&& Character.isUpperCase(lastCh))
 629				{
 630					return i - 1;
 631				}
 632				// stop at next first upper case char if camel caseg (don't stop at every upper case char)
 633				else if (camelCasedWords && Character.isUpperCase(ch) && !Character.isUpperCase(lastCh))
 634				{
 635					return i;
 636				}
 637				else if(Character.isLetterOrDigit(ch) ||
 638					noWordSep.indexOf(ch) != -1)
 639				{
 640					break;
 641				}
 642				// whitespace; include in word if eating
 643				else if(Character.isWhitespace(ch)
 644					&& eatWhitespace)
 645				{
 646					type = WHITESPACE;
 647					break;
 648				}
 649				else
 650					return i; //}}}
 651			//{{{ Symbol...
 652			case SYMBOL:
 653				if(!joinNonWordChars && i != pos)
 654					return i;
 655
 656				// if we see whitespace, set flag.
 657				if(Character.isWhitespace(ch))
 658				{
 659					if(eatWhitespace)
 660					{
 661						type = WHITESPACE;
 662						break;
 663					}
 664					else
 665						return i;
 666				}
 667				else if(Character.isLetterOrDigit(ch) ||
 668					noWordSep.indexOf(ch) != -1)
 669				{
 670					return i;
 671				}
 672				else
 673				{
 674					break;
 675				} //}}}
 676			}
 677		}
 678
 679		return line.length();
 680	} //}}}
 681
 682	//{{{ getCharType() method
 683	/**
 684	 * Returns the type of the char.
 685	 *
 686	 * @param ch the character
 687	 * @param noWordSep Characters that are non-alphanumeric, but
 688	 * should be treated as word characters anyway, it must not be null
 689	 * @return the type of the char : {@link #WHITESPACE},
 690	 * {@link #WORD_CHAR}, {@link #SYMBOL}
 691	 * @since jEdit 4.4pre1
 692	 */
 693	public static int getCharType(char ch, String noWordSep)
 694	{
 695		int type;
 696		if(Character.isWhitespace(ch))
 697			type = WHITESPACE;
 698		else if(Character.isLetterOrDigit(ch)
 699			|| noWordSep.indexOf(ch) != -1)
 700			type = WORD_CHAR;
 701		else
 702			type = SYMBOL;
 703		return type;
 704	} //}}}
 705
 706
 707	//{{{ spacesToTabs() method
 708	/**
 709	 * Converts consecutive spaces to tabs in the specified string.
 710	 * @param in The string
 711	 * @param tabSize The tab size
 712	 */
 713	public static String spacesToTabs(String in, int tabSize)
 714	{
 715		StringBuilder buf = new StringBuilder();
 716		int width = 0;
 717		int whitespace = 0;
 718		for(int i = 0; i < in.length(); i++)
 719		{
 720			switch(in.charAt(i))
 721			{
 722			case ' ':
 723				whitespace++;
 724				width++;
 725				break;
 726			case '\t':
 727				int tab = tabSize - (width % tabSize);
 728				width += tab;
 729				whitespace += tab;
 730				break;
 731			case '\n':
 732				if(whitespace != 0)
 733				{
 734					buf.append(StandardUtilities
 735						.createWhiteSpace(whitespace,tabSize,
 736						width - whitespace));
 737				}
 738				whitespace = 0;
 739				width = 0;
 740				buf.append('\n');
 741				break;
 742			default:
 743				if(whitespace != 0)
 744				{
 745					buf.append(StandardUtilities
 746						.createWhiteSpace(whitespace,tabSize,
 747						width - whitespace));
 748					whitespace = 0;
 749				}
 750				buf.append(in.charAt(i));
 751				width++;
 752				break;
 753			}
 754		}
 755
 756		if(whitespace != 0)
 757		{
 758			buf.append(StandardUtilities.createWhiteSpace(whitespace,tabSize,
 759				width - whitespace));
 760		}
 761
 762		return buf.toString();
 763	} //}}}
 764
 765	//{{{ tabsToSpaces() method
 766	/**
 767	 * Converts tabs to consecutive spaces in the specified string.
 768	 * @param in The string
 769	 * @param tabSize The tab size
 770	 */
 771	public static String tabsToSpaces(String in, int tabSize)
 772	{
 773		StringBuilder buf = new StringBuilder();
 774		int width = 0;
 775		for(int i = 0; i < in.length(); i++)
 776		{
 777			switch(in.charAt(i))
 778			{
 779			case '\t':
 780				int count = tabSize - (width % tabSize);
 781				width += count;
 782				while(--count >= 0)
 783					buf.append(' ');
 784				break;
 785			case '\n':
 786				width = 0;
 787				buf.append(in.charAt(i));
 788				break;
 789			default:
 790				width++;
 791				buf.append(in.charAt(i));
 792				break;
 793			}
 794		}
 795		return buf.toString();
 796	} //}}}
 797
 798	//{{{ format() method
 799	/**
 800	 * Formats the specified text by merging and breaking lines to the
 801	 * specified width.
 802	 * @param text The text
 803	 * @param maxLineLength The maximum line length
 804	 * @param tabSize The tab size
 805	 */
 806	public static String format(String text, int maxLineLength, int tabSize)
 807	{
 808		StringBuilder buf = new StringBuilder();
 809
 810		int index = 0;
 811
 812		for(;;)
 813		{
 814			int newIndex = text.indexOf("\n\n",index);
 815			if(newIndex == -1)
 816				break;
 817
 818			formatParagraph(text.substring(index,newIndex),
 819				maxLineLength,tabSize,buf);
 820			buf.append("\n\n");
 821			index = newIndex + 2;
 822		}
 823
 824		if(index != text.length())
 825		{
 826			formatParagraph(text.substring(index),
 827				maxLineLength,tabSize,buf);
 828		}
 829
 830		return buf.toString();
 831	} //}}}
 832
 833	//{{{ indexIgnoringWhitespace() method
 834	/**
 835	 * Inverse of <code>ignoringWhitespaceIndex()</code>.
 836	 * @param str a string (not an empty string)
 837	 * @param index The index
 838	 * @return The number of non-whitespace characters that precede the index.
 839	 * @since jEdit 4.3pre2
 840	 */
 841	public static int indexIgnoringWhitespace(String str, int index)
 842	{
 843		int j = 0;
 844		for(int i = 0; i < index; i++)
 845			if(!Character.isWhitespace(str.charAt(i))) j++;
 846		return j;
 847	} //}}}
 848
 849	//{{{ ignoringWhitespaceIndex() method
 850	/**
 851	 * Inverse of <code>indexIgnoringWhitespace()</code>.
 852	 * @param str a string (not an empty string)
 853	 * @param index The index
 854	 * @return The index into the string where the number of non-whitespace
 855	 * characters that precede the index is count.
 856	 * @since jEdit 4.3pre2
 857	 */
 858	public static int ignoringWhitespaceIndex(String str, int index)
 859	{
 860		int j = 0;
 861		for(int i = 0;;i++)
 862		{
 863			if(!Character.isWhitespace(str.charAt(i))) j++;
 864
 865			if(j > index)
 866				return i;
 867			if(i == str.length() - 1)
 868				return i + 1;
 869		}
 870	} //}}}
 871
 872	//{{{ getStringCase() methods
 873	public static final int MIXED = 0;
 874	public static final int LOWER_CASE = 1;
 875	public static final int UPPER_CASE = 2;
 876	public static final int TITLE_CASE = 3;
 877
 878	/**
 879	 * Returns if the specified string is all upper case, all lower case,
 880	 * or title case (first letter upper case, rest lower case).
 881	 * @param str The string
 882	 * @since jEdit 4.4pre1
 883	 */
 884	public static int getStringCase(CharSequence str)
 885	{
 886		if(str.length() == 0)
 887			return MIXED;
 888
 889		int state = -1;
 890
 891		char ch = str.charAt(0);
 892		if(Character.isLetter(ch))
 893		{
 894			if(Character.isUpperCase(ch))
 895				state = UPPER_CASE;
 896			else
 897				state = LOWER_CASE;
 898		}
 899
 900		for(int i = 1; i < str.length(); i++)
 901		{
 902			ch = str.charAt(i);
 903			if(!Character.isLetter(ch))
 904				continue;
 905
 906			switch(state)
 907			{
 908			case UPPER_CASE:
 909				if(Character.isLowerCase(ch))
 910				{
 911					if(i == 1)
 912						state = TITLE_CASE;
 913					else
 914						return MIXED;
 915				}
 916				break;
 917			case LOWER_CASE:
 918			case TITLE_CASE:
 919				if(Character.isUpperCase(ch))
 920					return MIXED;
 921				break;
 922			}
 923		}
 924
 925		return state;
 926	}
 927
 928	/**
 929	 * Returns if the specified string is all upper case, all lower case,
 930	 * or title case (first letter upper case, rest lower case).
 931	 * @param str The string
 932	 * @since jEdit 4.0pre1
 933	 */
 934	public static int getStringCase(String str)
 935	{
 936		return getStringCase((CharSequence) str);
 937	} //}}}
 938
 939	//{{{ toTitleCase() method
 940	/**
 941	 * Converts the specified string to title case, by capitalizing the
 942	 * first letter.
 943	 * @param str The string
 944	 * @since jEdit 4.0pre1
 945	 */
 946	public static String toTitleCase(String str)
 947	{
 948		if(str.length() == 0)
 949			return str;
 950		else
 951		{
 952			return Character.toUpperCase(str.charAt(0))
 953				+ str.substring(1).toLowerCase();
 954		}
 955	} //}}}
 956
 957	//{{{ escapeText() method
 958	/**
 959	 * Escapes a given string for use in a java.util.regex pattern.
 960	 * @since jEdit 4.5pre1
 961	 */
 962	public static String escapeText(String text)
 963	{
 964		// Make sure that every "\E" appearing in the text is escaped, and then
 965		// surround it with the quotation tags \Q and \E.
 966		String result = text.replace("\\E", "\\\\E");
 967		return "\\Q" + result + "\\E";
 968	} //}}}
 969	
 970	//{{{ Private members
 971	//{{{ formatParagraph() method
 972	private static void formatParagraph(String text, int maxLineLength,
 973		int tabSize, StringBuilder buf)
 974	{
 975		// align everything to paragraph's leading indent
 976		int leadingWhitespaceCount = StandardUtilities.getLeadingWhiteSpace(text);
 977		String leadingWhitespace = text.substring(0,leadingWhitespaceCount);
 978		int leadingWhitespaceWidth = StandardUtilities.getLeadingWhiteSpaceWidth(text,tabSize);
 979
 980		buf.append(leadingWhitespace);
 981
 982		int lineLength = leadingWhitespaceWidth;
 983		StringTokenizer st = new StringTokenizer(text);
 984		while(st.hasMoreTokens())
 985		{
 986			String word = st.nextToken();
 987			if(lineLength == leadingWhitespaceWidth)
 988			{
 989				// do nothing
 990			}
 991			else if(lineLength + word.length() + 1 > maxLineLength)
 992			{
 993				buf.append('\n');
 994				buf.append(leadingWhitespace);
 995				lineLength = leadingWhitespaceWidth;
 996			}
 997			else
 998			{
 999				buf.append(' ');
1000				lineLength++;
1001			}
1002			buf.append(word);
1003			lineLength += word.length();
1004		}
1005	} //}}}
1006
1007	//{{{ indexIgnoringWhitespace() method
1008	public static void indexIgnoringWhitespace(String text, int maxLineLength,
1009		int tabSize, StringBuffer buf)
1010	{
1011		// align everything to paragraph's leading indent
1012		int leadingWhitespaceCount = StandardUtilities.getLeadingWhiteSpace(text);
1013		String leadingWhitespace = text.substring(0,leadingWhitespaceCount);
1014		int leadingWhitespaceWidth = StandardUtilities.getLeadingWhiteSpaceWidth(text,tabSize);
1015
1016		buf.append(leadingWhitespace);
1017
1018		int lineLength = leadingWhitespaceWidth;
1019		StringTokenizer st = new StringTokenizer(text);
1020		while(st.hasMoreTokens())
1021		{
1022			String word = st.nextToken();
1023			if(lineLength == leadingWhitespaceWidth)
1024			{
1025				// do nothing
1026			}
1027			else if(lineLength + word.length() + 1 > maxLineLength)
1028			{
1029				buf.append('\n');
1030				buf.append(leadingWhitespace);
1031				lineLength = leadingWhitespaceWidth;
1032			}
1033			else
1034			{
1035				buf.append(' ');
1036				lineLength++;
1037			}
1038			buf.append(word);
1039			lineLength += word.length();
1040		}
1041	} //}}}
1042
1043	//}}}
1044}