| /******************************************************************************* |
| * Copyright (c) 2005, 2007 IBM Corporation and others. |
| * This program and the accompanying materials are made available under the |
| * terms of the Eclipse Public License v. 2.0 which is available at |
| * http://www.eclipse.org/legal/epl-2.0. |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| |
| *******************************************************************************/ |
| package org.eclipse.dltk.javascript.internal.ui.text; |
| |
| import org.eclipse.core.runtime.Assert; |
| import org.eclipse.dltk.core.IMethod; |
| import org.eclipse.dltk.core.IModelElement; |
| import org.eclipse.dltk.core.IScriptProject; |
| import org.eclipse.dltk.core.ISourceModule; |
| import org.eclipse.dltk.core.ModelException; |
| import org.eclipse.dltk.core.ScriptModelUtil; |
| import org.eclipse.dltk.internal.ui.editor.EditorUtility; |
| import org.eclipse.dltk.javascript.internal.corext.codemanipulation.JSCodeGeneration; |
| import org.eclipse.dltk.javascript.internal.ui.JavaScriptUI; |
| import org.eclipse.dltk.javascript.parser.jsdoc.JSDocTag; |
| import org.eclipse.dltk.javascript.scriptdoc.JavaHeuristicScanner; |
| import org.eclipse.dltk.javascript.scriptdoc.JavaIndenter; |
| import org.eclipse.dltk.javascript.ui.text.IJavaScriptPartitions; |
| import org.eclipse.dltk.ui.DLTKUIPlugin; |
| import org.eclipse.dltk.ui.PreferenceConstants; |
| import org.eclipse.dltk.ui.text.util.TabStyle; |
| import org.eclipse.jface.preference.IPreferenceStore; |
| import org.eclipse.jface.text.BadLocationException; |
| import org.eclipse.jface.text.DefaultIndentLineAutoEditStrategy; |
| import org.eclipse.jface.text.Document; |
| import org.eclipse.jface.text.DocumentCommand; |
| import org.eclipse.jface.text.DocumentRewriteSession; |
| import org.eclipse.jface.text.DocumentRewriteSessionType; |
| import org.eclipse.jface.text.IDocument; |
| import org.eclipse.jface.text.IRegion; |
| import org.eclipse.jface.text.TextUtilities; |
| import org.eclipse.jface.text.rules.FastPartitioner; |
| import org.eclipse.ui.IEditorPart; |
| import org.eclipse.ui.IWorkbenchPage; |
| import org.eclipse.ui.texteditor.ITextEditorExtension3; |
| |
| /** |
| * Auto indent strategy sensitive to brackets. |
| */ |
| public class JavascriptAutoEditStrategy extends |
| DefaultIndentLineAutoEditStrategy { |
| |
| /** The line comment introducer. Value is "{@value} " */ |
| private static final String LINE_COMMENT = "//"; //$NON-NLS-1$ |
| |
| private boolean fCloseBrace; |
| private boolean fIsSmartTab; |
| private boolean fIsSmartMode; |
| |
| private String fPartitioning; |
| final IScriptProject fProject; |
| JsPreferenceInterpreter prefs; |
| |
| /** |
| * Creates a new Java auto indent strategy for the given document |
| * partitioning. |
| * |
| * @param partitioning |
| * the document partitioning |
| * @param project |
| * the project to get formatting preferences from, or null to use |
| * default preferences |
| */ |
| public JavascriptAutoEditStrategy(String partitioning, |
| IScriptProject project) { |
| fPartitioning = partitioning; |
| fProject = project; |
| this.prefs = new JsPreferenceInterpreter(JavaScriptUI.getDefault() |
| .getPreferenceStore()); |
| } |
| |
| private String getIndentOfLine(IDocument d, int line) |
| throws BadLocationException { |
| if (line > -1) { |
| int start = d.getLineOffset(line); |
| int end = start + d.getLineLength(line) - 1; |
| int whiteEnd = findEndOfWhiteSpace(d, start, end); |
| return d.get(start, whiteEnd - start); |
| } else { |
| return ""; //$NON-NLS-1$ |
| } |
| } |
| |
| private int getStringEnd(String d, int offset, int endOffset, char ch) { |
| while (offset < endOffset) { |
| char curr = d.charAt(offset); |
| if (curr == '\\') { |
| // ignore escaped characters |
| offset++; |
| } else if (curr == ch) { |
| return offset; |
| } |
| offset++; |
| } |
| return endOffset; |
| } |
| |
| private void smartIndentAfterClosingBracket(IDocument d, DocumentCommand c) { |
| if (c.offset == -1 || d.getLength() == 0) |
| return; |
| |
| try { |
| int p = (c.offset == d.getLength() ? c.offset - 1 : c.offset); |
| int line = d.getLineOfOffset(p); |
| int start = d.getLineOffset(line); |
| int whiteend = findEndOfWhiteSpace(d, start, c.offset); |
| |
| JavaHeuristicScanner scanner = new JavaHeuristicScanner(d); |
| JavaIndenter indenter = new JavaIndenter(d, scanner, fProject); |
| |
| // shift only when line does not contain any text up to the closing |
| // bracket |
| if (whiteend == c.offset) { |
| // evaluate the line with the opening bracket that matches out |
| // closing bracket |
| int reference = indenter.findReferencePosition(c.offset, false, |
| true, false, false); |
| int indLine = d.getLineOfOffset(reference); |
| if (indLine != -1 && indLine != line) { |
| // take the indent of the found line |
| StringBuffer replaceText = new StringBuffer( |
| getIndentOfLine(d, indLine)); |
| // add the rest of the current line including the just added |
| // close bracket |
| replaceText.append(d.get(whiteend, c.offset - whiteend)); |
| replaceText.append(c.text); |
| // modify document command |
| c.length += c.offset - start; |
| c.offset = start; |
| c.text = replaceText.toString(); |
| } |
| } |
| } catch (BadLocationException e) { |
| DLTKUIPlugin.log(e); |
| } |
| } |
| |
| private void smartIndentAfterOpeningBracket(IDocument d, DocumentCommand c) { |
| if (c.offset < 1 || d.getLength() == 0) |
| return; |
| |
| JavaHeuristicScanner scanner = new JavaHeuristicScanner(d); |
| |
| int p = (c.offset == d.getLength() ? c.offset - 1 : c.offset); |
| |
| try { |
| // current line |
| int line = d.getLineOfOffset(p); |
| int lineOffset = d.getLineOffset(line); |
| |
| // make sure we don't have any leading comments etc. |
| if (d.get(lineOffset, p - lineOffset).trim().length() != 0) |
| return; |
| |
| // line of last Java code |
| int pos = scanner.findNonWhitespaceBackward(p, |
| JavaHeuristicScanner.UNBOUND); |
| if (pos == -1) |
| return; |
| int lastLine = d.getLineOfOffset(pos); |
| |
| // only shift if the last java line is further up and is a braceless |
| // block candidate |
| if (lastLine <= line) { |
| |
| JavaIndenter indenter = new JavaIndenter(d, scanner, fProject); |
| StringBuffer indent = indenter.computeIndentation(p, true); |
| String toDelete = d.get(lineOffset, c.offset - lineOffset); |
| if (indent != null && !indent.toString().equals(toDelete)) { |
| c.text = indent.append(c.text).toString(); |
| c.length += c.offset - lineOffset; |
| c.offset = lineOffset; |
| } |
| } |
| |
| } catch (BadLocationException e) { |
| DLTKUIPlugin.log(e); |
| } |
| |
| } |
| |
| private void smartIndentAfterNewLine(IDocument d, DocumentCommand c) { |
| JavaHeuristicScanner scanner = new JavaHeuristicScanner(d); |
| JavaIndenter indenter = new JavaIndenter(d, scanner, fProject); |
| StringBuffer indent = indenter.computeIndentation(c.offset); |
| if (indent == null) |
| indent = new StringBuffer(); |
| |
| int docLength = d.getLength(); |
| if (c.offset == -1 || docLength == 0) |
| return; |
| |
| try { |
| int p = (c.offset == docLength ? c.offset - 1 : c.offset); |
| int line = d.getLineOfOffset(p); |
| |
| StringBuffer buf = new StringBuffer(c.text + indent); |
| |
| IRegion reg = d.getLineInformation(line); |
| int lineEnd = reg.getOffset() + reg.getLength(); |
| |
| int contentStart = findEndOfWhiteSpace(d, c.offset, lineEnd); |
| c.length = Math.max(contentStart - c.offset, 0); |
| |
| int start = reg.getOffset(); |
| // insert closing brace on new line after an unclosed opening brace |
| if (closeBrace() |
| && countBrackets(d.get(), reg.getOffset(), c.offset, false) > 0 |
| && !isClosed(d, c.offset, c.length)) { |
| c.caretOffset = c.offset + buf.length(); |
| c.shiftsCaret = false; |
| |
| buf.append(TextUtilities.getDefaultLineDelimiter(d)); |
| final StringBuffer reference; |
| int nonWS = findEndOfWhiteSpace(d, start, lineEnd); |
| if (nonWS < c.offset && d.getChar(nonWS) == '{') |
| reference = new StringBuffer(d.get(start, nonWS - start)); |
| else |
| reference = indenter.getReferenceIndentation(c.offset); |
| if (reference != null) |
| buf.append(reference); |
| buf.append('}'); |
| } |
| // insert extra line upon new line between two braces |
| else if (c.offset > start && contentStart < lineEnd |
| && d.getChar(contentStart) == '}') { |
| int firstCharPos = scanner.findNonWhitespaceBackward( |
| c.offset - 1, start); |
| if (firstCharPos != JavaHeuristicScanner.NOT_FOUND |
| && d.getChar(firstCharPos) == '{') { |
| c.caretOffset = c.offset + buf.length(); |
| c.shiftsCaret = false; |
| |
| StringBuffer reference = null; |
| int nonWS = findEndOfWhiteSpace(d, start, lineEnd); |
| if (nonWS < c.offset && d.getChar(nonWS) == '{') |
| reference = new StringBuffer( |
| d.get(start, nonWS - start)); |
| else |
| reference = indenter.getReferenceIndentation(c.offset); |
| |
| buf.append(TextUtilities.getDefaultLineDelimiter(d)); |
| |
| if (reference != null) |
| buf.append(reference); |
| } |
| } else { |
| IRegion prevLine = d.getLineInformation(line); |
| String str = d.get(prevLine.getOffset(), prevLine.getLength()); |
| if (str.trim().startsWith(C_START)) { |
| /* |
| * handle only comment/jsdoc start here, as it happens in |
| * code partition. Continuation is handled in |
| * JSDocAutoIndentStrategy attached to comment/jsdoc |
| * partitions. |
| */ |
| String result = handleJsCodeCompleteStars(str, prevLine, |
| line, d, c); |
| if (result != null) { |
| buf.setLength(0); |
| buf.append(result); |
| } |
| } |
| } |
| |
| c.text = buf.toString(); |
| |
| } catch (BadLocationException e) { |
| DLTKUIPlugin.log(e); |
| } |
| } |
| |
| static final String C_START = "/*"; |
| static final String C_END = "*/"; |
| |
| private String handleJsCodeCompleteStars(String str, IRegion prevLine, |
| int line, IDocument d, DocumentCommand c) { |
| int index = str.indexOf(C_START); |
| // if it is directly closed then there is no doc to append. |
| if (str.indexOf(C_END, index + 2) != -1) |
| return null; |
| |
| // handle the start comment character prefix; |
| if (prevLine.getOffset() + index < c.offset) { |
| StringBuffer buf = new StringBuffer(); |
| StringBuffer indentStr = new StringBuffer(index); |
| int counter = 0; |
| while (counter < index) { |
| char ch = str.charAt(counter++); |
| if (Character.isWhitespace(ch)) { |
| indentStr.append(ch); |
| } else { |
| indentStr.append(' '); |
| } |
| } |
| |
| boolean enclosedComment = false; |
| try { |
| index = line + 1; |
| IRegion nextLine = null; |
| while ((nextLine = d.getLineInformation(index++)) != null) { |
| String strNextLine = d.get(nextLine.getOffset(), |
| nextLine.getLength()); |
| int stComment = strNextLine.indexOf(C_START); |
| int endComment = strNextLine.indexOf(C_END); |
| |
| if (stComment != -1 && endComment != -1) { |
| if (stComment < endComment) { |
| break; |
| } |
| } else if (endComment != -1) { |
| enclosedComment = true; |
| break; |
| } else if (stComment != -1) { |
| break; |
| } |
| |
| } |
| } catch (Exception ex) { |
| |
| } |
| final String endTag = "\n" + indentStr + " */"; |
| String generatedString = null; |
| if (!enclosedComment && JSDocAutoIndentStrategy.isGenerateStub()) { |
| try { |
| d.replace(c.offset, 0, endTag); |
| } catch (BadLocationException e) { |
| return null; |
| } |
| c.length = endTag.length(); |
| final IMethod method = findMethod(d, |
| c.offset + endTag.length(), true); |
| if (method != null) { |
| final String lineDelimiter = TextUtilities |
| .getDefaultLineDelimiter(d); |
| generatedString = JSCodeGeneration.getMethodComment(method, |
| null, lineDelimiter); |
| if (generatedString != null) { |
| generatedString = JSDocAutoIndentStrategy |
| .normalizeGeneratedDoc(generatedString); |
| generatedString = JSCodeGeneration.changeIndent( |
| generatedString, 0, method.getScriptProject(), |
| indentStr.toString(), lineDelimiter); |
| } |
| } |
| } |
| buf.append("\n" + indentStr + " * "); |
| if (generatedString != null) { |
| buf.append(generatedString); |
| } |
| c.caretOffset = c.offset + buf.length(); |
| c.shiftsCaret = false; |
| if (!enclosedComment) |
| buf.append(endTag); |
| return buf.toString(); |
| } |
| |
| return null; |
| } |
| |
| static IMethod findMethod(IDocument document, int offset, boolean reconcile) { |
| final int len = document.getLength(); |
| try { |
| while (offset < len |
| && Character.isWhitespace(document.getChar(offset))) { |
| ++offset; |
| } |
| } catch (BadLocationException e) { |
| return null; |
| } |
| final IModelElement modelElement = EditorUtility |
| .getActiveEditorModelInput(); |
| if (modelElement != null && modelElement instanceof ISourceModule) { |
| final ISourceModule module = (ISourceModule) modelElement; |
| if (reconcile) { |
| try { |
| ScriptModelUtil.reconcile(module); |
| } catch (ModelException e) { |
| return null; |
| } |
| } |
| final IModelElement member; |
| try { |
| member = module.getElementAt(offset); |
| } catch (ModelException e) { |
| return null; |
| } |
| try { |
| if (member instanceof IMethod |
| && ((IMethod) member).getSourceRange().getOffset() == offset) { |
| return (IMethod) member; |
| } |
| } catch (ModelException e) { |
| JavaScriptUI.log(e); |
| } |
| } |
| return null; |
| } |
| |
| private boolean isClosed(IDocument d, int offset, int length) { |
| String sm = d.get(); |
| int levelBefore = countBrackets(sm, 0, offset, true); |
| int levelAfter = -countBrackets(sm, offset, sm.length(), true); |
| return levelBefore <= levelAfter; |
| } |
| |
| /** |
| * @param offset |
| * @param sm |
| * @param levelBefore |
| */ |
| private int countBrackets(String sm, int start, int end, boolean countClose) { |
| int level = 0; |
| for (int a = start; a < end; a++) { |
| char charAt = sm.charAt(a); |
| switch (charAt) { |
| case '\'': { |
| a = getStringEnd(sm, a + 1, end, '\''); |
| break; |
| } |
| case '"': { |
| a = getStringEnd(sm, a + 1, end, '"'); |
| break; |
| } |
| case '/': { |
| int aPlus1 = a + 1; |
| if (aPlus1 < end) { |
| // test if single line comment |
| if (sm.charAt(aPlus1) == '/') { |
| // skip it all. |
| a = sm.indexOf('\n', aPlus1); |
| if (a == -1) |
| a = sm.length(); |
| } else if (sm.charAt(aPlus1) == '*') { |
| // start of doc search for the end.. |
| a = sm.indexOf(C_END, aPlus1); |
| if (a == -1) |
| a = sm.length(); |
| else |
| a = a + 1; |
| } else // regexp? |
| { |
| aPlus1++; |
| while (aPlus1 < end) { |
| char c = sm.charAt(aPlus1); |
| aPlus1++; |
| if (c == '\\') { |
| // escape char add one mre |
| aPlus1++; |
| continue; |
| } |
| if (c == '/') { |
| // reg exp found (/xxx/) |
| a = aPlus1; |
| break; |
| } |
| if (c == '\n') |
| break; |
| } |
| } |
| } |
| break; |
| } |
| case '<': { |
| // xml test |
| int aPlus1 = a + 1; |
| StringBuilder sb = new StringBuilder(5); |
| sb.append("</"); |
| while (aPlus1 < end) { |
| char c = sm.charAt(aPlus1); |
| if (Character.isJavaIdentifierPart(c)) { |
| sb.append(c); |
| aPlus1++; |
| } else if ((c == '>' || c == ' ') && sb.length() > 2) { |
| if (c == '>') { |
| // search for close tag. |
| int index = sm.substring(aPlus1, end).indexOf( |
| sb.toString()); |
| if (index != -1) { |
| a = aPlus1 + index + sb.length(); |
| } |
| } else { |
| while (++aPlus1 < end) { |
| c = sm.charAt(aPlus1); |
| if (c == '/') { |
| if (++aPlus1 < end) { |
| c = sm.charAt(aPlus1); |
| if (c == '>') { |
| a = aPlus1 + 1; |
| break; |
| } |
| aPlus1--; |
| } |
| } else if (c == '>') { |
| // search for close tag. |
| int index = sm.substring(aPlus1, end) |
| .indexOf(sb.toString()); |
| if (index != -1) { |
| a = aPlus1 + index + sb.length(); |
| } |
| break; |
| } else if (c == '"' || c == '\'') { |
| int index = sm.substring(aPlus1 + 1, end) |
| .indexOf(c); |
| if (index != -1) { |
| aPlus1 = aPlus1 + index; |
| } |
| } |
| } |
| } |
| break; |
| } else { |
| break; |
| } |
| } |
| break; |
| } |
| case '{': { |
| level++; |
| break; |
| } |
| case '}': { |
| if (countClose) |
| level--; |
| break; |
| } |
| } |
| } |
| return level; |
| } |
| |
| /** |
| * Installs a java partitioner with <code>document</code>. |
| * |
| * @param document |
| * the document |
| */ |
| private static void installJavaStuff(Document document) { |
| FastPartitioner partitioner = new FastPartitioner( |
| new JavascriptPartitionScanner(), |
| IJavaScriptPartitions.LEGAL_CONTENT_TYPES); |
| partitioner.connect(document); |
| document.setDocumentPartitioner(IJavaScriptPartitions.JS_PARTITIONING, |
| partitioner); |
| } |
| |
| /** |
| * Installs a java partitioner with <code>document</code>. |
| * |
| * @param document |
| * the document |
| */ |
| private static void removeJavaStuff(Document document) { |
| document.setDocumentPartitioner(IJavaScriptPartitions.JS_PARTITIONING, |
| null); |
| } |
| |
| private void smartPaste(IDocument document, DocumentCommand command) { |
| int newOffset = command.offset; |
| int newLength = command.length; |
| String newText = command.text; |
| |
| try { |
| JavaHeuristicScanner scanner = new JavaHeuristicScanner(document); |
| JavaIndenter indenter = new JavaIndenter(document, scanner, |
| fProject); |
| int offset = newOffset; |
| |
| // reference position to get the indent from |
| int refOffset = indenter.findReferencePosition(offset); |
| if (refOffset == JavaHeuristicScanner.NOT_FOUND) |
| return; |
| int peerOffset = getPeerPosition(document, command); |
| peerOffset = indenter.findReferencePosition(peerOffset); |
| refOffset = Math.min(refOffset, peerOffset); |
| |
| // eat any WS before the insertion to the beginning of the line |
| int firstLine = 1; // don't format the first line per default, as |
| // it has other content before it |
| IRegion line = document.getLineInformationOfOffset(offset); |
| String notSelected = document.get(line.getOffset(), |
| offset - line.getOffset()); |
| if (notSelected.trim().length() == 0) { |
| newLength += notSelected.length(); |
| newOffset = line.getOffset(); |
| firstLine = 0; |
| } |
| |
| // prefix: the part we need for formatting but won't paste |
| IRegion refLine = document.getLineInformationOfOffset(refOffset); |
| String prefix = document.get(refLine.getOffset(), newOffset |
| - refLine.getOffset()); |
| |
| // handle the indentation computation inside a temporary document |
| Document temp = new Document(prefix + newText); |
| DocumentRewriteSession session = temp |
| .startRewriteSession(DocumentRewriteSessionType.STRICTLY_SEQUENTIAL); |
| scanner = new JavaHeuristicScanner(temp); |
| indenter = new JavaIndenter(temp, scanner, fProject); |
| installJavaStuff(temp); |
| |
| // indent the first and second line |
| // compute the relative indentation difference from the second line |
| // (as the first might be partially selected) and use the value to |
| // indent all other lines. |
| boolean isIndentDetected = false; |
| StringBuffer addition = new StringBuffer(); |
| int insertLength = 0; |
| int first = document.computeNumberOfLines(prefix) + firstLine; // don't |
| // format |
| // first |
| // line |
| int lines = temp.getNumberOfLines(); |
| int tabLength = getVisualTabLengthPreference(); |
| boolean changed = false; |
| for (int l = first; l < lines; l++) { // we don't change the |
| // number of lines while |
| // adding indents |
| |
| IRegion r = temp.getLineInformation(l); |
| int lineOffset = r.getOffset(); |
| int lineLength = r.getLength(); |
| |
| if (lineLength == 0) // don't modify empty lines |
| continue; |
| |
| if (!isIndentDetected) { |
| |
| // indent the first pasted line |
| String current = getCurrentIndent(temp, l); |
| StringBuffer correct = indenter |
| .computeIndentation(lineOffset); |
| if (correct == null) |
| return; // bail out |
| |
| insertLength = subtractIndent(correct, current, addition, |
| tabLength); |
| if (l != first |
| && temp.get(lineOffset, lineLength).trim().length() != 0) { |
| isIndentDetected = true; |
| if (insertLength == 0) { |
| // no adjustment needed, bail out |
| if (firstLine == 0) { |
| // but we still need to adjust the first line |
| command.offset = newOffset; |
| command.length = newLength; |
| if (changed) |
| break; // still need to get the leading |
| // indent of the first line |
| } |
| return; |
| } |
| removeJavaStuff(temp); |
| } else { |
| changed = insertLength != 0; |
| } |
| } |
| |
| // relatively indent all pasted lines |
| if (insertLength > 0) |
| addIndent(temp, l, addition, tabLength); |
| else if (insertLength < 0) |
| cutIndent(temp, l, -insertLength, tabLength); |
| |
| } |
| |
| temp.stopRewriteSession(session); |
| newText = temp.get(prefix.length(), |
| temp.getLength() - prefix.length()); |
| |
| command.offset = newOffset; |
| command.length = newLength; |
| command.text = newText; |
| |
| } catch (BadLocationException e) { |
| DLTKUIPlugin.log(e); |
| } |
| |
| } |
| |
| /** |
| * Returns the indentation of the line <code>line</code> in |
| * <code>document</code>. The returned string may contain pairs of leading |
| * slashes that are considered part of the indentation. The space before the |
| * asterisk in a javadoc-like comment is not considered part of the |
| * indentation. |
| * |
| * @param document |
| * the document |
| * @param line |
| * the line |
| * @return the indentation of <code>line</code> in <code>document</code> |
| * @throws BadLocationException |
| * if the document is changed concurrently |
| */ |
| private static String getCurrentIndent(Document document, int line) |
| throws BadLocationException { |
| IRegion region = document.getLineInformation(line); |
| int from = region.getOffset(); |
| int endOffset = region.getOffset() + region.getLength(); |
| |
| // go behind line comments |
| int to = from; |
| while (to < endOffset - 2 && document.get(to, 2).equals(LINE_COMMENT)) |
| to += 2; |
| |
| while (to < endOffset) { |
| char ch = document.getChar(to); |
| if (!Character.isWhitespace(ch)) |
| break; |
| to++; |
| } |
| |
| // don't count the space before javadoc like, asterisk-style comment |
| // lines |
| if (to > from && to < endOffset - 1 |
| && document.get(to - 1, 2).equals(" *")) { //$NON-NLS-1$ |
| String type = TextUtilities.getContentType(document, |
| IJavaScriptPartitions.JS_PARTITIONING, to, true); |
| if (type.equals(IJavaScriptPartitions.JS_DOC) |
| || type.equals(IJavaScriptPartitions.JS_MULTI_LINE_COMMENT)) |
| to--; |
| } |
| |
| return document.get(from, to - from); |
| } |
| |
| /** |
| * Computes the difference of two indentations and returns the difference in |
| * length of current and correct. If the return value is positive, |
| * <code>addition</code> is initialized with a substring of that length of |
| * <code>correct</code>. |
| * |
| * @param correct |
| * the correct indentation |
| * @param current |
| * the current indentation (might contain non-whitespace) |
| * @param difference |
| * a string buffer - if the return value is positive, it will be |
| * cleared and set to the substring of <code>current</code> of |
| * that length |
| * @param tabLength |
| * the length of a tab |
| * @return the difference in length of <code>correct</code> and |
| * <code>current</code> |
| */ |
| private int subtractIndent(CharSequence correct, CharSequence current, |
| StringBuffer difference, int tabLength) { |
| int c1 = computeVisualLength(correct, tabLength); |
| int c2 = computeVisualLength(current, tabLength); |
| int diff = c1 - c2; |
| if (diff <= 0) |
| return diff; |
| |
| difference.setLength(0); |
| int len = 0, i = 0; |
| while (len < diff) { |
| char c = correct.charAt(i++); |
| difference.append(c); |
| len += computeVisualLength(c, tabLength); |
| } |
| |
| return diff; |
| } |
| |
| /** |
| * Indents line <code>line</code> in <code>document</code> with |
| * <code>indent</code>. Leaves leading comment signs alone. |
| * |
| * @param document |
| * the document |
| * @param line |
| * the line |
| * @param indent |
| * the indentation to insert |
| * @param tabLength |
| * the length of a tab |
| * @throws BadLocationException |
| * on concurrent document modification |
| */ |
| private void addIndent(Document document, int line, CharSequence indent, |
| int tabLength) throws BadLocationException { |
| IRegion region = document.getLineInformation(line); |
| int insert = region.getOffset(); |
| int endOffset = region.getOffset() + region.getLength(); |
| |
| // Compute insert after all leading line comment markers |
| int newInsert = insert; |
| while (newInsert < endOffset - 2 |
| && document.get(newInsert, 2).equals(LINE_COMMENT)) |
| newInsert += 2; |
| |
| // Heuristic to check whether it is commented code or just a comment |
| if (newInsert > insert) { |
| int whitespaceCount = 0; |
| int i = newInsert; |
| while (i < endOffset - 1) { |
| char ch = document.get(i, 1).charAt(0); |
| if (!Character.isWhitespace(ch)) |
| break; |
| whitespaceCount = whitespaceCount |
| + computeVisualLength(ch, tabLength); |
| i++; |
| } |
| |
| if (whitespaceCount != 0 && whitespaceCount >= 4) |
| insert = newInsert; |
| } |
| |
| // Insert indent |
| document.replace(insert, 0, indent.toString()); |
| } |
| |
| /** |
| * Cuts the visual equivalent of <code>toDelete</code> characters out of the |
| * indentation of line <code>line</code> in <code>document</code>. Leaves |
| * leading comment signs alone. |
| * |
| * @param document |
| * the document |
| * @param line |
| * the line |
| * @param toDelete |
| * the number of space equivalents to delete |
| * @param tabLength |
| * the length of a tab |
| * @throws BadLocationException |
| * on concurrent document modification |
| */ |
| private void cutIndent(Document document, int line, int toDelete, |
| int tabLength) throws BadLocationException { |
| IRegion region = document.getLineInformation(line); |
| int from = region.getOffset(); |
| int endOffset = region.getOffset() + region.getLength(); |
| |
| // go behind line comments |
| while (from < endOffset - 2 |
| && document.get(from, 2).equals(LINE_COMMENT)) |
| from += 2; |
| |
| int to = from; |
| while (toDelete > 0 && to < endOffset) { |
| char ch = document.getChar(to); |
| if (!Character.isWhitespace(ch)) |
| break; |
| toDelete -= computeVisualLength(ch, tabLength); |
| if (toDelete >= 0) |
| to++; |
| else |
| break; |
| } |
| |
| document.replace(from, to - from, ""); //$NON-NLS-1$ |
| } |
| |
| /** |
| * Returns the visual length of a given <code>CharSequence</code> taking |
| * into account the visual tabulator length. |
| * |
| * @param seq |
| * the string to measure |
| * @param tabLength |
| * the length of a tab |
| * @return the visual length of <code>seq</code> |
| */ |
| private int computeVisualLength(CharSequence seq, int tabLength) { |
| int size = 0; |
| |
| for (int i = 0; i < seq.length(); i++) { |
| char ch = seq.charAt(i); |
| if (ch == '\t') { |
| if (tabLength != 0) |
| size += tabLength - size % tabLength; |
| // else: size stays the same |
| } else { |
| size++; |
| } |
| } |
| return size; |
| } |
| |
| /** |
| * Returns the visual length of a given character taking into account the |
| * visual tabulator length. |
| * |
| * @param ch |
| * the character to measure |
| * @param tabLength |
| * the length of a tab |
| * @return the visual length of <code>ch</code> |
| */ |
| private int computeVisualLength(char ch, int tabLength) { |
| if (ch == '\t') |
| return tabLength; |
| else |
| return 1; |
| } |
| |
| /** |
| * The preference setting for the visual tabulator display. |
| * |
| * @return the number of spaces displayed for a tabulator in the editor |
| */ |
| private int getVisualTabLengthPreference() { |
| return 4; |
| } |
| |
| private int getPeerPosition(IDocument document, DocumentCommand command) { |
| if (document.getLength() == 0) |
| return 0; |
| /* |
| * Search for scope closers in the pasted text and find their opening |
| * peers in the document. |
| */ |
| Document pasted = new Document(command.text); |
| installJavaStuff(pasted); |
| int firstPeer = command.offset; |
| |
| JavaHeuristicScanner pScanner = new JavaHeuristicScanner(pasted); |
| JavaHeuristicScanner dScanner = new JavaHeuristicScanner(document); |
| |
| // add scope relevant after context to peer search |
| int afterToken = dScanner.nextToken(command.offset + command.length, |
| JavaHeuristicScanner.UNBOUND); |
| try { |
| switch (afterToken) { |
| case Symbols.TokenRBRACE: |
| pasted.replace(pasted.getLength(), 0, "}"); //$NON-NLS-1$ |
| break; |
| case Symbols.TokenRPAREN: |
| pasted.replace(pasted.getLength(), 0, ")"); //$NON-NLS-1$ |
| break; |
| case Symbols.TokenRBRACKET: |
| pasted.replace(pasted.getLength(), 0, "]"); //$NON-NLS-1$ |
| break; |
| } |
| } catch (BadLocationException e) { |
| // cannot happen |
| Assert.isTrue(false); |
| } |
| |
| int pPos = 0; // paste text position (increasing from 0) |
| int dPos = Math.max(0, command.offset - 1); // document position |
| // (decreasing from paste |
| // offset) |
| while (true) { |
| int token = pScanner.nextToken(pPos, JavaHeuristicScanner.UNBOUND); |
| pPos = pScanner.getPosition(); |
| switch (token) { |
| case Symbols.TokenLBRACE: |
| case Symbols.TokenLBRACKET: |
| case Symbols.TokenLPAREN: |
| pPos = skipScope(pScanner, pPos, token); |
| if (pPos == JavaHeuristicScanner.NOT_FOUND) |
| return firstPeer; |
| break; // closed scope -> keep searching |
| case Symbols.TokenRBRACE: |
| int peer = dScanner.findOpeningPeer(dPos, '{', '}'); |
| dPos = peer - 1; |
| if (peer == JavaHeuristicScanner.NOT_FOUND) |
| return firstPeer; |
| firstPeer = peer; |
| break; // keep searching |
| case Symbols.TokenRBRACKET: |
| peer = dScanner.findOpeningPeer(dPos, '[', ']'); |
| dPos = peer - 1; |
| if (peer == JavaHeuristicScanner.NOT_FOUND) |
| return firstPeer; |
| firstPeer = peer; |
| break; // keep searching |
| case Symbols.TokenRPAREN: |
| peer = dScanner.findOpeningPeer(dPos, '(', ')'); |
| dPos = peer - 1; |
| if (peer == JavaHeuristicScanner.NOT_FOUND) |
| return firstPeer; |
| firstPeer = peer; |
| break; // keep searching |
| case Symbols.TokenCASE: |
| case Symbols.TokenDEFAULT: |
| JavaIndenter indenter = new JavaIndenter(document, dScanner, |
| fProject); |
| peer = indenter.findReferencePosition(dPos, false, false, |
| false, true); |
| if (peer == JavaHeuristicScanner.NOT_FOUND) |
| return firstPeer; |
| firstPeer = peer; |
| break; // keep searching |
| |
| case Symbols.TokenEOF: |
| return firstPeer; |
| default: |
| // keep searching |
| } |
| } |
| } |
| |
| /** |
| * Skips the scope opened by <code>token</code> in <code>document</code>, |
| * returns either the position of the |
| * |
| * @param pos |
| * @param token |
| * @return the position after the scope |
| */ |
| private static int skipScope(JavaHeuristicScanner scanner, int pos, |
| int token) { |
| int openToken = token; |
| int closeToken; |
| switch (token) { |
| case Symbols.TokenLPAREN: |
| closeToken = Symbols.TokenRPAREN; |
| break; |
| case Symbols.TokenLBRACKET: |
| closeToken = Symbols.TokenRBRACKET; |
| break; |
| case Symbols.TokenLBRACE: |
| closeToken = Symbols.TokenRBRACE; |
| break; |
| default: |
| Assert.isTrue(false); |
| return -1; // dummy |
| } |
| |
| int depth = 1; |
| int p = pos; |
| |
| while (true) { |
| int tok = scanner.nextToken(p, JavaHeuristicScanner.UNBOUND); |
| p = scanner.getPosition(); |
| |
| if (tok == openToken) { |
| depth++; |
| } else if (tok == closeToken) { |
| depth--; |
| if (depth == 0) |
| return p + 1; |
| } else if (tok == Symbols.TokenEOF) { |
| return JavaHeuristicScanner.NOT_FOUND; |
| } |
| } |
| } |
| |
| private boolean isLineDelimiter(IDocument document, String text) { |
| String[] delimiters = document.getLegalLineDelimiters(); |
| if (delimiters != null) |
| return TextUtilities.startsWith(delimiters, text) > -1; |
| return false; |
| } |
| |
| private void smartIndentOnKeypress(IDocument document, |
| DocumentCommand command) { |
| switch (command.text.charAt(0)) { |
| case '}': |
| smartIndentAfterClosingBracket(document, command); |
| break; |
| case '{': |
| smartIndentAfterOpeningBracket(document, command); |
| break; |
| // quotes and brackets are handled by JavaScriptBracketInserter |
| // case '\"': |
| // case '\'': |
| // case '(': |
| // case '[': |
| // autoClose(document, command); |
| // break; |
| case 'e': |
| smartIndentUponE(document, command); |
| break; |
| case '*': |
| smartCloseJSDoc(document, command); |
| break; |
| } |
| } |
| |
| private void smartIndentUponE(IDocument d, DocumentCommand c) { |
| if (c.offset < 4 || d.getLength() == 0) |
| return; |
| |
| try { |
| String content = d.get(c.offset - 3, 3); |
| if (content.equals("els")) { //$NON-NLS-1$ |
| JavaHeuristicScanner scanner = new JavaHeuristicScanner(d); |
| int p = c.offset - 3; |
| |
| // current line |
| int line = d.getLineOfOffset(p); |
| int lineOffset = d.getLineOffset(line); |
| |
| // make sure we don't have any leading comments etc. |
| if (d.get(lineOffset, p - lineOffset).trim().length() != 0) |
| return; |
| |
| // line of last Java code |
| int pos = scanner.findNonWhitespaceBackward(p - 1, |
| JavaHeuristicScanner.UNBOUND); |
| if (pos == -1) |
| return; |
| int lastLine = d.getLineOfOffset(pos); |
| |
| // only shift if the last java line is further up and is a |
| // braceless block candidate |
| if (lastLine < line) { |
| |
| JavaIndenter indenter = new JavaIndenter(d, scanner, |
| fProject); |
| int ref = indenter.findReferencePosition(p, true, false, |
| false, false); |
| if (ref == JavaHeuristicScanner.NOT_FOUND) |
| return; |
| int refLine = d.getLineOfOffset(ref); |
| String indent = getIndentOfLine(d, refLine); |
| |
| if (indent != null) { |
| c.text = indent.toString() + "else"; //$NON-NLS-1$ |
| c.length += c.offset - lineOffset; |
| c.offset = lineOffset; |
| } |
| } |
| |
| return; |
| } |
| |
| if (content.equals("cas")) { //$NON-NLS-1$ |
| JavaHeuristicScanner scanner = new JavaHeuristicScanner(d); |
| int p = c.offset - 3; |
| |
| // current line |
| int line = d.getLineOfOffset(p); |
| int lineOffset = d.getLineOffset(line); |
| |
| // make sure we don't have any leading comments etc. |
| if (d.get(lineOffset, p - lineOffset).trim().length() != 0) |
| return; |
| |
| // line of last Java code |
| int pos = scanner.findNonWhitespaceBackward(p - 1, |
| JavaHeuristicScanner.UNBOUND); |
| if (pos == -1) |
| return; |
| int lastLine = d.getLineOfOffset(pos); |
| |
| // only shift if the last java line is further up and is a |
| // braceless block candidate |
| if (lastLine < line) { |
| |
| JavaIndenter indenter = new JavaIndenter(d, scanner, |
| fProject); |
| int ref = indenter.findReferencePosition(p, false, false, |
| false, true); |
| if (ref == JavaHeuristicScanner.NOT_FOUND) |
| return; |
| int refLine = d.getLineOfOffset(ref); |
| int nextToken = scanner.nextToken(ref, |
| JavaHeuristicScanner.UNBOUND); |
| String indent; |
| if (nextToken == Symbols.TokenCASE |
| || nextToken == Symbols.TokenDEFAULT) |
| indent = getIndentOfLine(d, refLine); |
| else |
| // at the brace of the switch |
| indent = indenter.computeIndentation(p).toString(); |
| |
| if (indent != null) { |
| c.text = indent.toString() + "case"; //$NON-NLS-1$ |
| c.length += c.offset - lineOffset; |
| c.offset = lineOffset; |
| } |
| } |
| |
| return; |
| } |
| |
| } catch (BadLocationException e) { |
| DLTKUIPlugin.log(e); |
| } |
| } |
| |
| static void smartCloseJSDoc(IDocument d, DocumentCommand c) { |
| if (c.offset < 3 || d.getLength() == 0) |
| return; |
| try { |
| final String content = d.get(c.offset - 2, 2); |
| if (content.equals("/*")) { //$NON-NLS-1$ |
| final JavaHeuristicScanner scanner = new JavaHeuristicScanner(d); |
| int pos = c.offset - 2; |
| pos = scanner.findNonWhitespaceBackward(pos - 1, |
| JavaHeuristicScanner.UNBOUND); |
| if (pos == JavaHeuristicScanner.NOT_FOUND) { |
| return; |
| } |
| final int prevToken = scanner.previousToken(pos, |
| JavaHeuristicScanner.UNBOUND); |
| if (prevToken == Symbols.TokenVAR |
| || prevToken == Symbols.TokenCOMMA |
| && scanner.looksLikeVarStatement(scanner.getPosition())) { |
| c.text += " " + JSDocTag.TYPE + " {} */ "; |
| c.shiftsCaret = false; |
| c.caretOffset = c.offset + c.text.indexOf('}'); |
| } else { |
| pos = scanner.findNonWhitespaceForward(c.offset, |
| JavaHeuristicScanner.UNBOUND); |
| if (pos == JavaHeuristicScanner.NOT_FOUND) { |
| return; |
| } |
| final int nextToken = scanner.nextToken(pos, |
| JavaHeuristicScanner.UNBOUND); |
| if (nextToken == Symbols.TokenFUNCTION) { |
| c.text += " */"; |
| if (c.offset < d.getLength() |
| && !Character.isWhitespace(d.getChar(c.offset))) { |
| c.text += " "; |
| } |
| c.shiftsCaret = false; |
| c.caretOffset = c.offset + 2; |
| } |
| } |
| } |
| } catch (BadLocationException e) { |
| DLTKUIPlugin.log(e); |
| } |
| } |
| |
| /* |
| * @see |
| * org.eclipse.jface.text.IAutoIndentStrategy#customizeDocumentCommand(org |
| * .eclipse.jface.text.IDocument, org.eclipse.jface.text.DocumentCommand) |
| */ |
| @Override |
| public void customizeDocumentCommand(IDocument d, DocumentCommand c) { |
| if (c.doit == false) |
| return; |
| clearCachedValues(); |
| if (!isSmartMode()) { |
| super.customizeDocumentCommand(d, c); |
| return; |
| } |
| if (!fIsSmartTab && isRepresentingTab(c.text)) |
| return; |
| if (c.length == 0 && c.text != null && isLineDelimiter(d, c.text)) |
| smartIndentAfterNewLine(d, c); |
| else if (c.text.length() == 1) |
| smartIndentOnKeypress(d, c); |
| else if (c.text.length() > 1 |
| && getPreferenceStore().getBoolean( |
| PreferenceConstants.EDITOR_SMART_PASTE)) |
| smartPaste(d, c); // no smart backspace for paste |
| } |
| |
| /** |
| * Tells whether the given inserted string represents hitting the Tab key. |
| * |
| * @param text |
| * the text to check |
| * @return <code>true</code> if the text represents hitting the Tab key |
| * @since 3.0 |
| */ |
| private boolean isRepresentingTab(String text) { |
| if (text == null) |
| return false; |
| |
| if (TabStyle.SPACES == prefs.getTabStyle()) { |
| if (text.length() == 0 |
| || text.length() > getVisualTabLengthPreference()) |
| return false; |
| for (int i = 0; i < text.length(); i++) { |
| if (text.charAt(i) != ' ') |
| return false; |
| } |
| return true; |
| } else { |
| return text.length() == 1 && text.charAt(0) == '\t'; |
| } |
| } |
| |
| private static IPreferenceStore getPreferenceStore() { |
| return JavaScriptUI.getDefault().getPreferenceStore(); |
| } |
| |
| private boolean closeBrace() { |
| return fCloseBrace; |
| } |
| |
| private boolean isSmartMode() { |
| return fIsSmartMode; |
| } |
| |
| private void clearCachedValues() { |
| fCloseBrace = prefs.closeBrackets(); |
| fIsSmartTab = prefs.isSmartTab(); |
| fIsSmartMode = computeSmartMode(); |
| } |
| |
| protected boolean computeSmartMode() { |
| IWorkbenchPage page = DLTKUIPlugin.getActivePage(); |
| if (page != null) { |
| IEditorPart part = page.getActiveEditor(); |
| if (part instanceof ITextEditorExtension3) { |
| ITextEditorExtension3 extension = (ITextEditorExtension3) part; |
| return extension.getInsertMode() == ITextEditorExtension3.SMART_INSERT; |
| } |
| } |
| return false; |
| } |
| } |