blob: a5e6998ff652300534c7bb5573101282955bd189 [file] [log] [blame]
/*=============================================================================#
# 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());
}
}