blob: 351ee7707ab475fd08094cc2127eea1d78e57c0a [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.modules.jshell.model;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
/**
* Describes a part of the console I/O. A section can be either input or output one.
* Input sections are entered by the user (or at least contain jshell commands or java
* code), output sections are produced by the JShell.
* <p/>
* Each section <b>consists of full lines</b>, each section contents must be terminated
* by newline - <b>except</b> the current input section, which may be terminated
* by EOT.
* <p/>
* Each section can be subdivided into <i>snippets</i> each representing a whole command
* or java code to be executed by JShell. Non-java sections have no 'snippets' in the terms
* of JShell API, but the entire contents form a 'snippet' to help the user to avoid
* JShell output markers (prompt, message prefixes, etc).
* <p/>
* Once created and produced by ConsoleModel, the ConsoleSection instance is immutable.
*/
public final class ConsoleSection {
private final int start;
private int len;
private final Type type;
private boolean incomplete;
private int contentStart = -1;
/**
* Possibly null, but may contain offsets of individual snippets in this
* section. If null, the entire section forms a single snippet. The last
* snippet in the section may be unterminated.
* <p/>
* Positions are relative to the start of this section.
*/
private Rng[] partOffsets;
/**
* For java sections, boundaries of individual snippets found in the text.
*/
private Rng[] snippetOffsets;
public ConsoleSection(int start, Type type, int len) {
this.start = start;
this.type = type;
this.len = len;
}
public ConsoleSection(int start, Type type) {
this.start = start;
this.type = type;
}
public int getStart() {
return start;
}
public int getEnd() {
return start + len;
}
public int getLen() {
return len;
}
public ConsoleSection.Type getType() {
return type;
}
public boolean hasMoreParts() {
return partOffsets != null;
}
public boolean isIncomplete() {
return incomplete;
}
public int getPartBegin() {
return contentStart;
}
public int getPartLen() {
return contentStart == -1 ? len : len - (contentStart - start);
}
public Rng[] getPartRanges() {
if (hasMoreParts()) {
return partOffsets;
} else if (contentStart == -1) {
return new Rng[0];
} else {
return new Rng[]{new Rng(getPartBegin(), start + len)};
}
}
/**
* Return bounds for an individual snippet. See
* {@link #getAllSnippetBounds()} for discussion.
* <p/>
* It is permitted to use index 0 for Sections with no snippets. Bounds for
* the section contents will be returned then.
*
* @param index snippet index
* @return bounds
*/
public Rng getSnippetBounds(int index) {
if (snippetOffsets == null) {
if (index > 0) {
throw new ArrayIndexOutOfBoundsException(index);
}
return new Rng(getPartBegin(), start + len);
} else {
return snippetOffsets[index];
}
}
/**
* Returns ranges for all snippets. If there are no snippets (command,
* output section), returns entire contents of the section.
* <p/>
* Each of the returned ranges contains start and end position, in the
* document offsets. The text between snippet bounds may be discontinuous -
* use {@link
* #computeFragments(org.netbeans.modules.jshell.support.Rng)} to get
* continuous fragments of the text.
* <p/>
* @return boundaries of all snippets or boundaries for the contents.
*/
public Rng[] getAllSnippetBounds() {
if (snippetOffsets == null) {
return new Rng[]{new Rng(getPartBegin(), start + len)};
} else {
return snippetOffsets;
}
}
void extendTo(int endOffset) {
this.len = endOffset - start;
}
void extendToWithRanges(List<Rng> ranges) {
Rng last = ranges.get(ranges.size() - 1);
this.len = last.end - start;
Rng first = ranges.get(0);
this.contentStart = first.start;
if (ranges.size() > 1) {
this.partOffsets = ranges.toArray(new Rng[ranges.size()]);
}
}
void setSnippetRanges(List<Rng> ranges) {
if (ranges.size() > 1) {
this.snippetOffsets = ranges.toArray(new Rng[ranges.size()]);
}
}
void extendWithPart(int s, int e) {
if (contentStart == -1) {
this.contentStart = s;
this.len = e - this.start;
return;
} else if (partOffsets == null) {
// there MAY be a preceding part: from the contentStart to the end of the section:
int x = contentStart + getPartLen();
if (x >= s) {
if (e < x) {
return;
}
this.len = e - start;
return;
}
partOffsets = new Rng[2];
partOffsets[0] = new Rng(contentStart, getEnd());
} else {
partOffsets = Arrays.copyOf(partOffsets, partOffsets.length + 1);
}
Rng r = new Rng(s, e);
assert this.start + this.len <= e;
partOffsets[partOffsets.length - 1] = r;
}
void setComplete(boolean complete) {
this.incomplete = !complete;
}
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(type.toString() + "[").append(start).append("-").append(start + len).append("");
if (partOffsets != null) {
sb.append("; snippets = ").append(Arrays.asList(partOffsets).toString());
}
sb.append("]");
return sb.toString();
}
public String getRangeContents(Document d, Rng snippetRange) {
try {
StringBuilder sb = new StringBuilder();
Rng[] ranges = computeFragments(snippetRange);
for (Rng r : ranges) {
sb.append(d.getText(r.start, r.len()));
}
return sb.toString();
} catch (BadLocationException ex) {
return "";
}
}
public String getRangeContents(CharSequence s, Rng snippetRange) {
StringBuilder sb = new StringBuilder();
Rng[] ranges = computeFragments(snippetRange);
for (Rng r : ranges) {
sb.append(s.subSequence(r.start, r.start + r.len()));
}
return sb.toString();
}
/**
* Gets text contents of the section. Respect fragment boundaries. Must be
* called under document read-lock.
*
* @param d the document
* @return text
*/
public String getContents(Document d) {
return getContents(d, 0);
}
private String getContents(Document d, int startFrom) {
try {
int l = d.getLength() + 1;
if (startFrom > l) {
startFrom = l;
}
if (hasMoreParts()) {
StringBuilder sb = new StringBuilder();
for (Rng r : partOffsets) {
if (startFrom >= r.end) {
continue;
} else if (startFrom > r.start) {
sb.append(d.getText(startFrom, Math.min(l, r.end) - startFrom));
} else {
startFrom = Math.min(l, r.start);
sb.append(d.getText(r.start, Math.min(l, r.end) - startFrom));
}
}
return sb.toString();
} else if (startFrom >= getEnd()) {
return ""; // NOI18N
} else if (startFrom > contentStart) {
int e = Math.min(l, contentStart + getPartLen());
return d.getText(startFrom, e - startFrom);
} else {
startFrom = Math.min(l, contentStart);
return d.getText(startFrom, Math.min(l - startFrom, getPartLen()));
}
} catch (BadLocationException ex) {
ex.printStackTrace();
}
return "";
}
/**
* Computes fragments of possibly discontinuous contents. Given start/end of
* a part of contents, and known start/end of section fragments (that is
* excluding prompts or starting marks), computes fragments of the passed
* content area; each of the fragments forms a continuous part of the
* document.
*
* @param jr content range
* @return fragments.
*/
public Rng[] computeFragments(Rng jr) {
Rng[] parts = getPartRanges();
int partIndex = 0;
List<Rng> res = new ArrayList<>();
Rng pr = parts[partIndex];
assert pr.start <= jr.start;
OUT:
if (jr.end <= pr.end) {
res.add(jr);
} else {
int l = pr.end - jr.start;
res.add(new Rng(jr.start, pr.end));
partIndex++;
while (jr.end > pr.end) {
if (partIndex >= parts.length) {
break OUT;
}
res.add(parts[partIndex++]);
}
assert partIndex < parts.length;
pr = parts[partIndex];
if (jr.end == pr.end) {
res.add(pr);
} else {
res.add(new Rng(pr.start, jr.end));
}
}
return res.toArray(new Rng[res.size()]);
}
/**
* Converts document offset into section content offset. If `acceptOutside'
* is true, positions outside section contents are treated as first or
* position just past the section contents respectively. Returns -1 if the
* offset cannot be converted.
*
* @param offset document offset
* @param acceptOutside if true, accepts offsets outside the section. Never
* returns -1
* @return offset into section contents which corresponds to the original
* document offset.
*/
public int offsetToContents(int offset, boolean acceptOutside) {
int idx = 0;
for (Rng r : getPartRanges()) {
if (offset < r.start) {
return acceptOutside ? idx : -1;
} else if (offset < r.end) {
return idx + offset - r.start;
}
idx += r.len();
}
return acceptOutside ? idx : -1;
}
public int offsetFromContents(int offset) {
for (Rng r : getPartRanges()) {
if (offset < r.len()) {
return r.start + offset;
}
offset -= r.len();
}
return getEnd();
}
/**
*
* @author sdedic
*/
public static enum Type {
/**
* Java input
*/
JAVA(true, true),
/**
* Incomplete section, must extend
*/
JAVA_INCOMPLETE(true, true), /**
* Message from JShell, such as error message, or informational message
*/
MESSAGE(false, false), /**
* Output from JShell engine
*/
OUTPUT(false, false), /**
* JShell console command
*/
COMMAND(true, false);
public final boolean input;
public final boolean java;
private Type(boolean input, boolean java) {
this.input = input;
this.java = java;
}
}
}