| /* |
| * 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; |
| } |
| } |
| } |