| /*=============================================================================# |
| # Copyright (c) 2015, 2019 Stephan Wahlbrink and others. |
| # |
| # This program and the accompanying materials are made available under the |
| # terms of the Eclipse Public License 2.0 which is available at |
| # https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 |
| # which is available at https://www.apache.org/licenses/LICENSE-2.0. |
| # |
| # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 |
| # |
| # Contributors: |
| # Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation |
| #=============================================================================*/ |
| |
| package org.eclipse.statet.docmlet.wikitext.core.source; |
| |
| import java.lang.reflect.InvocationTargetException; |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| import org.eclipse.jface.text.BadLocationException; |
| import org.eclipse.jface.text.IDocument; |
| import org.eclipse.jface.text.IRegion; |
| import org.eclipse.jface.text.Region; |
| import org.eclipse.jface.text.TextUtilities; |
| import org.eclipse.text.edits.DeleteEdit; |
| import org.eclipse.text.edits.MultiTextEdit; |
| import org.eclipse.text.edits.ReplaceEdit; |
| import org.eclipse.text.edits.TextEdit; |
| |
| import org.eclipse.statet.jcommons.collections.ImCollections; |
| import org.eclipse.statet.jcommons.collections.ImList; |
| import org.eclipse.statet.jcommons.text.core.TextRegion; |
| |
| import org.eclipse.statet.ecommons.text.IndentUtil; |
| import org.eclipse.statet.ecommons.text.core.sections.DocContentSections; |
| |
| import org.eclipse.statet.docmlet.wikitext.core.WikitextCoreAccess; |
| import org.eclipse.statet.docmlet.wikitext.core.ast.Block; |
| import org.eclipse.statet.docmlet.wikitext.core.ast.Control; |
| import org.eclipse.statet.docmlet.wikitext.core.ast.Heading; |
| import org.eclipse.statet.docmlet.wikitext.core.ast.SourceComponent; |
| import org.eclipse.statet.docmlet.wikitext.core.ast.Span; |
| import org.eclipse.statet.docmlet.wikitext.core.ast.Text; |
| import org.eclipse.statet.docmlet.wikitext.core.ast.WikitextAstVisitor; |
| import org.eclipse.statet.docmlet.wikitext.core.markup.WikitextMarkupLanguage; |
| import org.eclipse.statet.docmlet.wikitext.core.source.extdoc.ExtdocMarkupLanguage; |
| import org.eclipse.statet.ltk.ast.core.AstInfo; |
| |
| |
| public class HardLineWrap { |
| |
| |
| public static final byte SELECTION_STRICT= 1; |
| public static final byte SELECTION_WITH_TAIL= 2; |
| public static final byte SELECTION_MERGE1= 3; |
| public static final byte SELECTION_MERGE= 4; |
| public static final byte PARAGRAPH= 5; |
| |
| |
| private static final class BlockData { |
| |
| |
| private final Block node; |
| |
| private final ImList<LineData> lines; |
| |
| private final String indentCont; |
| |
| |
| public BlockData(final Block node, final ImList<LineData> lines, final String indentCont) { |
| this.node= node; |
| this.lines= lines; |
| this.indentCont= indentCont; |
| } |
| |
| } |
| |
| private static final class LineData { |
| |
| private static final byte HARD_LINE_BREAK= 1; |
| |
| |
| private final int startOffset; |
| private final int endOffset; |
| |
| private final String textSource; |
| |
| private final List<Text> textNodes; |
| |
| private byte end; |
| |
| |
| public LineData(final int startOffset, final int endOffset, final String textSource) { |
| this.startOffset= startOffset; |
| this.endOffset= endOffset; |
| this.textSource= textSource; |
| this.textNodes= new ArrayList<>(8); |
| } |
| |
| |
| @Override |
| public String toString() { |
| final StringBuilder sb= new StringBuilder(); |
| sb.append('['); |
| sb.append(this.startOffset); |
| sb.append(", "); //$NON-NLS-1$ |
| sb.append(this.endOffset); |
| sb.append("): "); //$NON-NLS-1$ |
| sb.append(this.textSource); |
| return sb.toString(); |
| } |
| |
| } |
| |
| |
| private static final class Task extends WikitextAstVisitor { |
| |
| |
| private final byte mode; |
| |
| private final IDocument document; |
| |
| private final MarkupSourceFormatAdapter formatAdapter; |
| private final SourceComponent sourceNode; |
| private final TextRegion region; |
| |
| private final List<BlockData> blocks= new ArrayList<>(); |
| |
| private boolean createLineContent; |
| private final List<LineData> lines= new ArrayList<>(); |
| private int currentLineIdx; |
| |
| private final int lineWidth; |
| private final IndentUtil indentUtil; |
| |
| |
| public Task(final byte mode, final IDocument document, |
| final TextRegion region, |
| final MarkupSourceFormatAdapter sourceAdapter, final SourceComponent sourceNode, |
| final int lineWidth, final IndentUtil indentUtil) { |
| this.mode= mode; |
| this.document= document; |
| this.formatAdapter= sourceAdapter; |
| this.sourceNode= sourceNode; |
| this.region= region; |
| this.lineWidth= lineWidth; |
| this.indentUtil= indentUtil; |
| } |
| |
| |
| @Override |
| public void visit(final Block node) throws InvocationTargetException { |
| if (this.createLineContent || !this.region.intersectsNonEmpty(node)) { |
| return; |
| } |
| switch (node.getBlockType()) { |
| case PARAGRAPH: |
| createTextBlockNode(node); |
| return; |
| case QUOTE: |
| case NUMERIC_LIST: |
| case BULLETED_LIST: |
| case LIST_ITEM: |
| case DEFINITION_LIST: |
| case DEFINITION_ITEM: |
| node.acceptInWikitextChildren(this); |
| return; |
| default: |
| return; |
| } |
| } |
| |
| private void createTextBlockNode(final Block node) throws InvocationTargetException { |
| try { |
| final ImList<? extends TextRegion> textRegions= node.getTextRegions(); |
| |
| int i= 0; |
| while (i < textRegions.size()) { |
| final TextRegion textRegion= textRegions.get(i); |
| if (textRegion.getEndOffset() <= this.region.getStartOffset()) { |
| i++; |
| continue; |
| } |
| else { |
| break; |
| } |
| } |
| |
| while (i < textRegions.size()) { |
| this.lines.clear(); |
| int line= Integer.MIN_VALUE; |
| while (i < textRegions.size()) { |
| final TextRegion textRegion= textRegions.get(i); |
| if (this.mode >= SELECTION_MERGE1 |
| || this.region.intersectsNonEmpty(textRegion) ) { |
| final int currentLine= this.document.getLineOfOffset(textRegion.getStartOffset()); |
| if (line == Integer.MIN_VALUE || currentLine - line <= 1) { |
| line= currentLine; |
| this.lines.add(new LineData( |
| textRegion.getStartOffset(), textRegion.getEndOffset(), |
| this.document.get(textRegion.getStartOffset(), textRegion.getLength()) )); |
| i++; |
| continue; |
| } |
| // i unchanged |
| break; |
| } |
| else { |
| i= Integer.MAX_VALUE; |
| break; |
| } |
| } |
| |
| if (!this.lines.isEmpty()) { |
| final String indentCont= getBlockWrapIndent(node); |
| if (indentCont != null) { |
| this.createLineContent= true; |
| this.currentLineIdx= 0; |
| node.acceptInWikitextChildren(this); |
| |
| this.blocks.add(new BlockData(node, ImCollections.toList(this.lines), indentCont)); |
| } |
| } |
| } |
| } |
| catch (final Exception e) { |
| throw new InvocationTargetException(e); |
| } |
| finally { |
| this.createLineContent= false; |
| } |
| } |
| |
| |
| @Override |
| public void visit(final Heading node) throws InvocationTargetException { |
| } |
| |
| @Override |
| public void visit(final Span node) throws InvocationTargetException { |
| if (!this.createLineContent) { |
| return; |
| } |
| switch (node.getSpanType()) { |
| case CODE: |
| return; |
| default: |
| node.acceptInWikitextChildren(this); |
| return; |
| } |
| } |
| |
| @Override |
| public void visit(final Text node) throws InvocationTargetException { |
| if (!this.createLineContent) { |
| return; |
| } |
| if (node.getLength() > 0) { |
| while (this.currentLineIdx < this.lines.size()) { |
| final LineData lineData= this.lines.get(this.currentLineIdx); |
| if (node.getEndOffset() <= lineData.startOffset) { |
| return; |
| } |
| if (node.getStartOffset() < lineData.endOffset) { |
| lineData.textNodes.add(node); |
| } |
| if (node.getEndOffset() > lineData.endOffset) { |
| this.currentLineIdx++; |
| } |
| else { |
| break; |
| } |
| } |
| } |
| } |
| |
| @Override |
| public void visit(final Control node) throws InvocationTargetException { |
| if (!this.createLineContent) { |
| return; |
| } |
| if (node.getText() == Control.LINE_BREAK) { |
| while (this.currentLineIdx < this.lines.size()) { |
| final LineData lineData= this.lines.get(this.currentLineIdx); |
| if (node.getEndOffset() <= lineData.startOffset) { |
| return; |
| } |
| if (node.getStartOffset() < lineData.endOffset) { |
| lineData.end= LineData.HARD_LINE_BREAK; |
| break; |
| } |
| else { |
| this.currentLineIdx++; |
| } |
| } |
| } |
| } |
| |
| |
| public void collect() throws Exception { |
| try { |
| this.sourceNode.acceptInWikitextChildren(this); |
| |
| if (this.blocks.isEmpty()) { |
| return; |
| } |
| } |
| catch (final InvocationTargetException e) { |
| throw (Exception) e.getTargetException(); |
| } |
| } |
| |
| private String getBlockWrapIndent(final Block node) throws Exception { |
| return this.formatAdapter.getPrefixCont(node, this.indentUtil); |
| } |
| |
| |
| private final static byte TEXT= 0; |
| private final static byte ESCAPE= 1; |
| private final static byte BREAK= 2; |
| |
| private IRegion trimText(final LineData lineData) { |
| int beginIdx= 0; |
| int endIdx= lineData.endOffset - lineData.startOffset; |
| ITER_OFFSET: for (; endIdx > beginIdx; endIdx--) { |
| switch (lineData.textSource.charAt(endIdx - 1)) { |
| case '\n': |
| case '\r': |
| continue ITER_OFFSET; |
| default: |
| break ITER_OFFSET; |
| } |
| } |
| if (!lineData.textNodes.isEmpty()) { |
| { final Text node= lineData.textNodes.get(0); |
| if (node.getStartOffset() <= lineData.startOffset) { |
| final int bound= Math.min(node.getEndOffset() - lineData.startOffset, endIdx); |
| ITER_OFFSET: for (; beginIdx < bound; beginIdx++) { |
| switch (lineData.textSource.charAt(beginIdx)) { |
| case ' ': |
| case '\t': |
| continue ITER_OFFSET; |
| default: |
| break ITER_OFFSET; |
| } |
| } |
| } |
| } |
| { final Text node= lineData.textNodes.get(lineData.textNodes.size() - 1); |
| if (node.getEndOffset() >= lineData.endOffset) { |
| final int bound= Math.max(node.getStartOffset() - lineData.startOffset, beginIdx); |
| final int savedOffset= endIdx; |
| ITER_OFFSET: for (; endIdx > bound; endIdx--) { |
| switch (lineData.textSource.charAt(endIdx - 1)) { |
| case ' ': |
| case '\t': |
| continue ITER_OFFSET; |
| default: |
| break ITER_OFFSET; |
| } |
| } |
| if (endIdx < savedOffset) { |
| int count= 0; |
| ITER_OFFSET: for (; endIdx - count > bound; count++) { |
| switch (lineData.textSource.charAt(endIdx - count - 1)) { |
| case '\\': |
| continue ITER_OFFSET; |
| default: |
| break ITER_OFFSET; |
| } |
| } |
| if (count % 2 == 1) { |
| endIdx++; |
| } |
| } |
| } |
| } |
| } |
| return new Region(beginIdx, endIdx - beginIdx); |
| } |
| |
| /** |
| * |
| * @param lineData the line |
| * @param beginIdx begin index in line |
| * @param endIdx end index (exclusive) in line |
| * @param beginColumn column at beginIdx |
| * @param fallback |
| * @return region of break in line |
| */ |
| private IRegion getBreak(final LineData lineData, final int beginIdx, final int endIdx, |
| final int beginColumn, final boolean fallback) { |
| int brIdx= -1; |
| int brLength= 0; |
| byte state= TEXT; |
| |
| int chIdx= beginIdx; |
| int column= beginColumn; |
| for (; chIdx < endIdx && (column <= this.lineWidth || (fallback && brIdx < 0) ); |
| chIdx++) { |
| switch (lineData.textSource.charAt(chIdx)) { |
| case ' ': |
| if (isTextOffset(lineData, chIdx)) { |
| switch (state) { |
| case TEXT: |
| brIdx= chIdx; |
| brLength= 1; |
| state= BREAK; |
| break; |
| case ESCAPE: |
| state= TEXT; |
| break; |
| case BREAK: |
| brLength++; |
| break; |
| } |
| } |
| else { |
| state= TEXT; |
| } |
| column++; |
| continue; |
| case '\t': |
| if (isTextOffset(lineData, chIdx)) { |
| switch (state) { |
| case TEXT: |
| brIdx= chIdx; |
| brLength= 1; |
| state= BREAK; |
| break; |
| case ESCAPE: |
| state= TEXT; |
| break; |
| case BREAK: |
| brLength++; |
| break; |
| } |
| } |
| else { |
| state= TEXT; |
| } |
| column+= this.indentUtil.getTabWidth() - (column % this.indentUtil.getTabWidth()); |
| continue; |
| case '\\': |
| if (isTextOffset(lineData, chIdx)) { |
| switch (state) { |
| case ESCAPE: |
| state= TEXT; |
| break; |
| default: |
| state= ESCAPE; |
| break; |
| } |
| } |
| else { |
| state= TEXT; |
| } |
| column++; |
| continue; |
| default: |
| state= TEXT; |
| column++; |
| continue; |
| } |
| } |
| if (chIdx == endIdx && column <= this.lineWidth) { |
| return new Region(endIdx, 0); |
| } |
| return (brIdx >= 0) ? new Region(brIdx, brLength) : null; |
| } |
| |
| private boolean isTextOffset(final LineData lineData, final int chIdx) { |
| final int offset= lineData.startOffset + chIdx; |
| for (final Text node : lineData.textNodes) { |
| if (node.getStartOffset() > offset) { |
| break; |
| } |
| if (node.getEndOffset() > offset) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| } |
| |
| |
| private final DocContentSections documentContentInfo; |
| |
| private final WikitextCoreAccess wikitextCoreAccess; |
| |
| |
| public HardLineWrap(final DocContentSections documentContentInfo, final WikitextCoreAccess coreAccess) { |
| this.documentContentInfo= documentContentInfo; |
| this.wikitextCoreAccess= coreAccess; |
| } |
| |
| |
| public WikitextCoreAccess getWikitextCoreAccess() { |
| return this.wikitextCoreAccess; |
| } |
| |
| public void addTextEdits(final IDocument document, final SourceComponent sourceNode, |
| final TextRegion region, final byte mode, final MarkupSourceFormatAdapter formatAdapter, |
| final TextEdit rootEdit, |
| IndentUtil indentUtil) throws Exception { |
| if (indentUtil != null) { |
| if (indentUtil.getDocument() != document) { |
| throw new IllegalArgumentException("indentUtil.document != document"); //$NON-NLS-1$ |
| } |
| } |
| else { |
| indentUtil= new IndentUtil(document, this.wikitextCoreAccess.getWikitextCodeStyle()); |
| } |
| final Task task= new Task(mode, document, region, |
| formatAdapter, sourceNode, |
| this.wikitextCoreAccess.getWikitextCodeStyle().getLineWidth(), indentUtil ); |
| task.collect(); |
| |
| processBlocks(task, rootEdit); |
| } |
| |
| public TextEdit createTextEdit(final IDocument document, final SourceComponent sourceNode, |
| final TextRegion region, final byte mode, final MarkupSourceFormatAdapter formatAdapter, |
| final IndentUtil indentUtil) throws Exception { |
| final MultiTextEdit rootEdit= new MultiTextEdit(); |
| addTextEdits(document, sourceNode, region, mode, formatAdapter, rootEdit, indentUtil); |
| return (rootEdit.getChildrenSize() > 0) ? rootEdit : null; |
| } |
| |
| public void addTextEdits(final IDocument document, |
| final AstInfo ast, final TextRegion region, final byte mode, |
| final TextEdit rootEdit, |
| final IndentUtil indentUtil) throws Exception { |
| final ExtdocMarkupLanguage markupLanguage= getMarkupLanguage(document); |
| final MarkupSourceFormatAdapter formatAdapter; |
| if (markupLanguage == null |
| || (formatAdapter= markupLanguage.getSourceFormatAdapter()) == null |
| || !(ast.getRoot() instanceof SourceComponent) ) { |
| return; |
| } |
| |
| addTextEdits(document, (SourceComponent) ast.getRoot(), region, mode, formatAdapter, |
| rootEdit, indentUtil); |
| } |
| |
| public TextEdit createTextEdit(final IDocument document, |
| final AstInfo ast, final TextRegion region, final byte mode, |
| final IndentUtil indentUtil) throws Exception { |
| final MultiTextEdit rootEdit= new MultiTextEdit(); |
| addTextEdits(document, ast, region, mode, rootEdit, indentUtil); |
| return (rootEdit.getChildrenSize() > 0) ? rootEdit : null; |
| } |
| |
| |
| protected final ExtdocMarkupLanguage getMarkupLanguage(final IDocument document) { |
| final WikitextMarkupLanguage markupLanguage= WikidocDocumentSetupParticipant.getMarkupLanguage(document, |
| this.documentContentInfo.getPartitioning() ); |
| return (markupLanguage instanceof ExtdocMarkupLanguage) ? (ExtdocMarkupLanguage) markupLanguage : null; |
| } |
| |
| |
| private void processBlocks(final Task task, final TextEdit rootEdit) throws BadLocationException { |
| ITER_BLOCKS: for (final BlockData blockData : task.blocks) { |
| String lineWrap= null; |
| int lineWrapColumns= -1; |
| int openOffset= -1; |
| int beginColumn= -1; |
| int endColumn= -1; |
| byte lastChange= 1; |
| boolean lineInRegion= true; |
| ITER_LINES: for (final LineData lineData : blockData.lines) { |
| if (lineInRegion) { |
| lineInRegion= (lineData.startOffset < task.region.getEndOffset()); |
| } |
| else if (task.mode <= SELECTION_MERGE && lastChange == 0) { |
| break ITER_BLOCKS; |
| } |
| if (!lineInRegion && task.mode <= SELECTION_MERGE1 && lastChange <= 1) { |
| break ITER_BLOCKS; |
| } |
| final IRegion textRegion= task.trimText(lineData); |
| int textIdx= textRegion.getOffset(); |
| final int textEndIdx= textRegion.getOffset() + textRegion.getLength(); |
| String remainingSource= lineData.textSource; |
| IRegion br= null; |
| if (openOffset >= 0 |
| && (br= task.getBreak(lineData, textIdx, textEndIdx, endColumn + 1, false)) != null) { |
| if (task.mode <= SELECTION_WITH_TAIL |
| && !isInRegion(task.region, lineData.startOffset + textRegion.getOffset())) { |
| break ITER_BLOCKS; |
| } |
| |
| if (task.document.getChar(openOffset) == ' ') { |
| rootEdit.addChild(new DeleteEdit( |
| openOffset + 1, lineData.startOffset - openOffset - 1 )); |
| } |
| else { |
| rootEdit.addChild(new ReplaceEdit( |
| openOffset, lineData.startOffset - openOffset, " " )); //$NON-NLS-1$ |
| } |
| beginColumn= endColumn + 1; |
| endColumn= task.indentUtil.getColumn(remainingSource, textEndIdx - textIdx, beginColumn); |
| lastChange= 1; |
| } |
| else { |
| beginColumn= task.indentUtil.getColumn(lineData.startOffset + textIdx); |
| endColumn= task.indentUtil.getColumn(remainingSource, textEndIdx - textIdx, beginColumn); |
| lastChange= 0; |
| if (beginColumn >= task.lineWidth) { |
| openOffset= -1; |
| continue ITER_LINES; |
| } |
| } |
| while (endColumn > task.lineWidth) { |
| if (br == null) { |
| br= task.getBreak(lineData, textIdx, textEndIdx, beginColumn, true); |
| } |
| if (br == null || br.getLength() == 0) { |
| break; |
| } |
| if (task.mode <= SELECTION_STRICT && !isInRegion(task.region, lineData.startOffset + br.getOffset())) { |
| break ITER_BLOCKS; |
| } |
| |
| if (lineWrap == null) { |
| lineWrapColumns= task.indentUtil.getColumn(blockData.indentCont, blockData.indentCont.length()); |
| lineWrap= TextUtilities.getDefaultLineDelimiter(task.document) + blockData.indentCont; |
| } |
| rootEdit.addChild(new ReplaceEdit( |
| lineData.startOffset + br.getOffset(), br.getLength(), lineWrap )); |
| textIdx= br.getOffset() + br.getLength(); |
| remainingSource= lineData.textSource.substring(textIdx); |
| beginColumn= lineWrapColumns; |
| endColumn= task.indentUtil.getColumn(remainingSource, textEndIdx - textIdx, beginColumn); |
| lastChange= 2; |
| |
| br= null; |
| } |
| openOffset= (endColumn < task.lineWidth && lineData.end != LineData.HARD_LINE_BREAK) ? |
| (lineData.startOffset + textEndIdx) : -1; |
| } |
| } |
| } |
| |
| private boolean isInRegion(final TextRegion region, final int offset) { |
| return (offset >= region.getStartOffset() && offset < region.getEndOffset()); |
| } |
| |
| } |