/*=============================================================================#
# Copyright (c) 2015-2016 Stephan Wahlbrink (WalWare.de) and others.
# All rights reserved. This program and the accompanying materials
# are made available under the terms of the Eclipse Public License v1.0
# which accompanies this distribution, and is available at
# http://www.eclipse.org/legal/epl-v10.html
#
# Contributors:
# Stephan Wahlbrink - initial API and implementation
#=============================================================================*/
package de.walware.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 de.walware.jcommons.collections.ImCollections;
import de.walware.jcommons.collections.ImList;
import de.walware.ecommons.ltk.AstInfo;
import de.walware.ecommons.text.IndentUtil;
import de.walware.ecommons.text.core.sections.IDocContentSections;
import de.walware.docmlet.wikitext.core.IWikitextCoreAccess;
import de.walware.docmlet.wikitext.core.ast.Block;
import de.walware.docmlet.wikitext.core.ast.Control;
import de.walware.docmlet.wikitext.core.ast.Heading;
import de.walware.docmlet.wikitext.core.ast.SourceComponent;
import de.walware.docmlet.wikitext.core.ast.Span;
import de.walware.docmlet.wikitext.core.ast.Text;
import de.walware.docmlet.wikitext.core.ast.WikitextAstVisitor;
import de.walware.docmlet.wikitext.core.markup.IMarkupLanguage;
import de.walware.docmlet.wikitext.core.source.extdoc.IExtdocMarkupLanguage;
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 beginOffset;
private final int endOffset;
private final String textSource;
private final List<Text> textNodes;
private byte end;
public LineData(final int beginOffset, final int endOffset, final String textSource) {
this.beginOffset= beginOffset;
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.beginOffset);
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 IMarkupSourceFormatAdapter formatAdapter;
private final SourceComponent sourceNode;
private final IRegion 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 IRegion region,
final IMarkupSourceFormatAdapter 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 || !TextUtilities.overlaps(this.region, 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 {
this.lines.clear();
final ImList<? extends IRegion> textRegions= node.getTextRegions();
{ int i= 0;
for (; i < textRegions.size(); i++) {
final IRegion textRegion= textRegions.get(i);
if (textRegion.getOffset() + textRegion.getLength() <= this.region.getOffset()) {
continue;
}
else {
break;
}
}
for (; i < textRegions.size(); i++) {
final IRegion textRegion= textRegions.get(i);
if (this.mode >= SELECTION_MERGE1
|| TextUtilities.overlaps(this.region, textRegion) ) {
this.lines.add(new LineData(
textRegion.getOffset(), textRegion.getOffset() + textRegion.getLength(),
this.document.get(textRegion.getOffset(), textRegion.getLength()) ));
}
else {
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.beginOffset) {
return;
}
if (node.getOffset() < 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.beginOffset) {
return;
}
if (node.getOffset() < 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.beginOffset;
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.getOffset() <= lineData.beginOffset) {
final int bound= Math.min(node.getEndOffset() - lineData.beginOffset, 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.getOffset() - lineData.beginOffset, 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.beginOffset + chIdx;
for (final Text node : lineData.textNodes) {
if (node.getOffset() > offset) {
break;
}
if (node.getEndOffset() > offset) {
return true;
}
}
return false;
}
}
private final IDocContentSections documentContentInfo;
private final IWikitextCoreAccess wikitextCoreAccess;
public HardLineWrap(final IDocContentSections documentContentInfo, final IWikitextCoreAccess coreAccess) {
this.documentContentInfo= documentContentInfo;
this.wikitextCoreAccess= coreAccess;
}
public IWikitextCoreAccess getWikitextCoreAccess() {
return this.wikitextCoreAccess;
}
public void addTextEdits(final IDocument document, final SourceComponent sourceNode,
final IRegion region, final byte mode, final IMarkupSourceFormatAdapter 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 IRegion region, final byte mode, final IMarkupSourceFormatAdapter 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 IRegion region, final byte mode,
final TextEdit rootEdit,
final IndentUtil indentUtil) throws Exception {
final IExtdocMarkupLanguage markupLanguage= getMarkupLanguage(document);
final IMarkupSourceFormatAdapter formatAdapter;
if (markupLanguage == null
|| (formatAdapter= markupLanguage.getSourceFormatAdapter()) == null
|| !(ast.root instanceof SourceComponent) ) {
return;
}
addTextEdits(document, (SourceComponent) ast.root, region, mode, formatAdapter,
rootEdit, indentUtil);
}
public TextEdit createTextEdit(final IDocument document,
final AstInfo ast, final IRegion 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 IExtdocMarkupLanguage getMarkupLanguage(final IDocument document) {
final IMarkupLanguage markupLanguage= WikidocDocumentSetupParticipant.getMarkupLanguage(document,
this.documentContentInfo.getPartitioning() );
return (markupLanguage instanceof IExtdocMarkupLanguage) ? (IExtdocMarkupLanguage) 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.beginOffset < task.region.getOffset() + task.region.getLength());
}
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.beginOffset + textRegion.getOffset())) {
break ITER_BLOCKS;
}
if (task.document.getChar(openOffset) == ' ') {
rootEdit.addChild(new DeleteEdit(
openOffset + 1, lineData.beginOffset - openOffset - 1 ));
}
else {
rootEdit.addChild(new ReplaceEdit(
openOffset, lineData.beginOffset - openOffset, " " )); //$NON-NLS-1$
}
beginColumn= endColumn + 1;
endColumn= task.indentUtil.getColumn(remainingSource, textEndIdx - textIdx, beginColumn);
lastChange= 1;
}
else {
beginColumn= task.indentUtil.getColumn(lineData.beginOffset + 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.beginOffset + 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.beginOffset + 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.beginOffset + textEndIdx) : -1;
}
}
}
private boolean isInRegion(final IRegion region, final int offset) {
return (offset >= region.getOffset()
&& offset < region.getOffset() + region.getLength() );
}
}