//---------------------------------------------------------------------------- // 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; } }