//----------------------------------------------------------------------------
// COMPONENT NAME: LPEX Editor
//
// © Copyright IBM Corporation 2004, 2007
// All Rights Reserved.
//
// DESCRIPTION:
// DetabCommand - sample user-defined command (detab)
//----------------------------------------------------------------------------
package com.ibm.lpex.samples;
import com.ibm.lpex.core.LpexCommand;
import com.ibm.lpex.core.LpexNls;
import com.ibm.lpex.core.LpexStringTokenizer;
import com.ibm.lpex.core.LpexView;
/**
* Sample command <b>detab</b> - expand all tabs to spaces.
* Expands all the tab characters in the document to blanks. By default, it
* uses the value of the <b>tabs</b> editor parameter in the current view.
* Alternatively, the tab settings may be specified via the command parameters.
*
* <p>Here is the DetabCommand
* <a href="doc-files/DetabCommand.java.html">source code</a>.
* This class shares several methods with {@link EntabCommand}.</p>
*
* <p>To run this sample:
* <ul>
* <li>Define this user command via an editor preference page, where available,
* or from the editor command line:
* <pre>set commandClass.detab com.ibm.lpex.samples.DetabCommand</pre></li>
* <li>Run it from the editor command line:
* <pre>detab [<i>stop1 stop2 ..</i> [every <i>increment</i>]]</pre></li>
* </ul></p>
*
* @see com.ibm.lpex.samples.EntabCommand
* @see com.ibm.lpex.samples All the samples
*/
public class DetabCommand implements LpexCommand
{
/**
* Runs this command.
* Expands all the tabs in the document.
*
* @param lpexView the document view in which the command was issued
* @param parameters optional tab stops
*/
public boolean doCommand(LpexView lpexView, String parameters)
{
parameters = parameters.trim();
if ("?".equals(parameters)) // command help
{
if (lpexView != null)
{
lpexView.doCommand("set messageText Syntax: detab [<tab stop> .. [every <increment>]]");
}
return true;
}
// if no view, just validate the parameters (if any)
if (lpexView == null)
{
return validSettings(parameters);
}
// determine and validate the tab settings to use
Settings currentSettings = new Settings();
if (!initSettings(lpexView, parameters, currentSettings,
TestCommand.commandName(this, lpexView)))
{
return false;
}
// expand the tabs in the document
boolean changes = expandTabs(lpexView, currentSettings);
// indicate successful completion
message(lpexView, changes? "Tabs in document expanded." :
"No tabs found in document.");
return true;
}
/**
* Expands the tabs in the document, according to the given settings.
*
* @return indication whether any tabs were expanded in the document
*/
boolean expandTabs(LpexView lpexView, Settings currentSettings)
{
// remember original cursor location on the screen
int originalElement = lpexView.currentElement();
int originalPosition = lpexView.queryInt("displayPosition");
// process all non-show elements
boolean changes = false;
int elements = lpexView.elements();
for (int element = 1; element <= elements; element++)
{
if (!lpexView.show(element))
{
if (expandTabs(element, currentSettings))
{
changes = true;
}
}
}
// restore the cursor
if (changes && (lpexView.currentElement() != originalElement ||
lpexView.queryInt("displayPosition") != originalPosition))
{
lpexView.jump(originalElement, originalPosition);
}
return changes;
}
/**
* Expands the tabs in a non-show element, according to the given settings.
* Tabs are interpreted in terms of display columns.
*
* <p>Currently, we change the entire element text - could be subtler and
* replace '\t' with a blank and then insert any additional blanks up to the
* tab stop, in order to maintain marks better.</p>
*
* @return indication whether any tabs were expanded in the element
*/
boolean expandTabs(int element, Settings currentSettings)
{
LpexView lpexView = currentSettings._lpexView;
String text = lpexView.elementText(element);
if (text.indexOf('\t') < 0)
{
return false; // empty line / no tabs.
}
StringBuilder buffer = new StringBuilder(text.length() + 32);
int tc = 0; // last-used tab stop in currentSettings._tabStops[]
int lastTabVal = 0; // last-used tab stop position
int i = 0; // current index into text
int pos = 1; // current display column position
for (; i < text.length(); i++)
{
char c = text.charAt(i);
if (c == '\t')
{
// establish relevant tab stop
while (tc < currentSettings._tabStops.length && lastTabVal <= pos)
{
lastTabVal = currentSettings._tabStops[tc++];
}
while (lastTabVal <= pos)
{
lastTabVal += currentSettings._tabIncrement; // NB it's non-zero
}
int tabLen = lastTabVal - pos;
for (int j = 0; j < tabLen; j++)
{
buffer.append(' ');
}
pos = lastTabVal; // we're at lastTabVal display column position now
}
else
{
buffer.append(c);
pos += getSourceWidth(lpexView, c);
}
}
lpexView.setElementText(element, buffer.toString());
return true;
}
/**
* Estimates the width in display columns for one Java Unicode char converted
* to the document's source character encoding. Far from fail-safe...
*
* @param lpexView view on a document
* @param c Java Unicode char
*/
int getSourceWidth(LpexView lpexView, char c)
{
// a bidi mark (not visible on the display)?
if (LpexNls.isBidiMark(c))
{
return 0;
}
// a surrogate - for tabs calcs, returning 1 for each surrogate if a pair should be OK?
if (c >= '\uD800' && c <= '\uDBFF' || // high surrogate
c >= '\uDC00' && c <= '\uDFFF') // low surrogate
{
return 1;
}
// to determine Asian double-width characters, the only way right now is to kludge it
// via checking for encoding into 2 bytes when the doc's source encoding is MBCS...
int len = 1;
if (lpexView.nls().isSourceMbcs())
{
len = lpexView.nls().sourceLength(c);
if (len > 2)
{
return 2;
}
}
return len;
}
/**
* Validates any tab stops specified.
*
* @param tabs optional tabs setting, e.g., "1 4 8 every 8"
* @return true = tabs correct / none
*/
static boolean validSettings(String tabs)
{
return (tabs != null && tabs.length() != 0)?
initSettings(null, tabs, null, null) : true;
}
/**
* Validates and sets up the tab stops and view for one run of the command.
*
* @param lpexView document view running the command
* @param tabs optional tabs setting, e.g., "1 4 8 every 8"
* @param settings Settings object to store the result
* @param command command name (e.g., "detab")
* @return true = tabs correct, settings initialized
*/
static boolean initSettings(LpexView lpexView, String tabs, Settings settings,
String command)
{
int tabIncrement = 1; // if none, tab stop on every character
int lastTabStop = 0;
int count = 1;
// validate, and determine the count of, the tab stops
if (tabs == null || tabs.length() == 0)
{
tabs = lpexView.query("current.tabs");
}
LpexStringTokenizer st = new LpexStringTokenizer(tabs);
String token = null;
while (st.hasMoreTokens())
{
token = st.nextToken();
try
{
int tabStop = Integer.parseInt(token);
if (tabStop <= lastTabStop)
{
return incorrectParameter(lpexView, token, command);
}
if (tabStop != 1)
{
count++;
}
lastTabStop = tabStop;
token = null;
}
catch(NumberFormatException e)
{
break;
}
}
// determine the tab increment
if (token != null)
{
if (!token.equals("every"))
{
return incorrectParameter(lpexView, token, command);
}
if (!st.hasMoreTokens())
{
return missingParameter(lpexView, command);
}
token = st.nextToken();
try
{
tabIncrement = Integer.parseInt(token);
if (tabIncrement < 1)
{
return incorrectParameter(lpexView, token, command);
}
}
catch(NumberFormatException e)
{
return incorrectParameter(lpexView, token, command);
}
if (st.hasMoreTokens())
{
return incorrectParameter(lpexView, st.nextToken(), command);
}
}
// set up the tab stops
int[] tabStops = new int[count];
tabStops[0] = 1; // always a first tab stop at 1
int i = 1;
st = new LpexStringTokenizer(tabs);
try
{
while (st.hasMoreTokens() && i < count)
{
int tabStop = Integer.parseInt(st.nextToken());
if (tabStop != 1)
{
tabStops[i] = tabStop;
i++;
}
}
}
catch(NumberFormatException e) {}
if (settings != null)
{
settings._tabStops = tabStops;
settings._tabIncrement = tabIncrement;
settings._lpexView = lpexView;
}
return true;
}
/**
* Displays an incorrect-parameter message on the message line.
*/
static boolean incorrectParameter(LpexView lpexView, String parm, String command)
{
message(lpexView, "\""+parm+"\" is not a correct parameter for the \""+command+"\" command.");
return false;
}
/**
* Displays a missing-parameter message on the message line.
*/
static boolean missingParameter(LpexView lpexView, String command)
{
message(lpexView, "Additional parameters are required for the \""+command+"\" command.");
return false;
}
/**
* Displays a message on the editor message line.
*/
static void message(LpexView lpexView, String message)
{
if (lpexView != null)
{
lpexView.doCommand("set messageText " + message);
}
}
/**
* Settings used during one run of the command.
*/
static class Settings
{
int[] _tabStops; // e.g., 1, 4, 8
int _tabIncrement; // e.g., 8
LpexView _lpexView;
}
}