Skip to content

Commit

Permalink
Better change tracking for Semantic highlighting for CSL
Browse files Browse the repository at this point in the history
  • Loading branch information
lkishalmi committed Jan 1, 2025
1 parent 59bea23 commit 4cc8d6f
Show file tree
Hide file tree
Showing 7 changed files with 82 additions and 114 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.text.AttributeSet;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.editor.mimelookup.MimeLookup;
import org.netbeans.api.editor.mimelookup.MimePath;
Expand Down Expand Up @@ -65,10 +63,6 @@ public final class ColoringManager {
private final String mimeType;
private final Map<Set<ColoringAttributes>, String> type2Coloring;

//private static final Font ITALIC = SettingsDefaults.defaultFont.deriveFont(Font.ITALIC);
//private static final Font BOLD = SettingsDefaults.defaultFont.deriveFont(Font.BOLD);


public ColoringManager(String mimeType) {
this.mimeType = mimeType;

Expand Down Expand Up @@ -159,12 +153,10 @@ public AttributeSet getColoringImpl(Coloring colorings) {
es.addAll(colorings);

if (colorings.contains(UNUSED)) {
attribs.add(AttributesUtilities.createImmutable(EditorStyleConstants.Tooltip, new UnusedTooltipResolver()));
attribs.add(AttributesUtilities.createImmutable(EditorStyleConstants.Tooltip, UNUSED_TOOLTIP_RESOLVER));
attribs.add(AttributesUtilities.createImmutable("unused-browseable", Boolean.TRUE));
}

//colorings = colorings.size() > 0 ? EnumSet.copyOf(colorings) : EnumSet.noneOf(ColoringAttributes.class);

for (Entry<Set<ColoringAttributes>, String> attribs2Colorings : type2Coloring.entrySet()) {
if (es.containsAll(attribs2Colorings.getKey())) {
String key = attribs2Colorings.getValue();
Expand Down Expand Up @@ -204,12 +196,8 @@ private static AttributeSet adjustAttributes(AttributeSet as) {

return AttributesUtilities.createImmutable(attrs.toArray());
}

private final class UnusedTooltipResolver implements HighlightAttributeValue<String> {

@Override
public String getValue(JTextComponent component, Document document, Object attributeKey, int startOffset, final int endOffset) {
return NbBundle.getMessage(ColoringManager.class, "LBL_UNUSED");
}
}
private final static HighlightAttributeValue<String> UNUSED_TOOLTIP_RESOLVER =
(component, document, attributeKey, startOffset, endOffset) -> NbBundle.getMessage(ColoringManager.class, "LBL_UNUSED");

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,11 @@
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.AttributeSet;
import javax.swing.text.Document;
import org.netbeans.api.editor.mimelookup.MimeLookup;
import org.netbeans.api.editor.mimelookup.MimePath;
import org.netbeans.api.editor.settings.FontColorSettings;
import org.netbeans.lib.editor.util.swing.DocumentListenerPriority;
import org.netbeans.lib.editor.util.swing.DocumentUtilities;
import org.netbeans.modules.csl.core.Language;
import org.netbeans.modules.csl.api.ColoringAttributes.Coloring;
import org.netbeans.spi.editor.highlighting.HighlightsSequence;
Expand All @@ -49,10 +45,9 @@
*
* @author Tor Norbye
*/
public final class GsfSemanticLayer extends AbstractHighlightsContainer implements DocumentListener {
public final class GsfSemanticLayer extends AbstractHighlightsContainer {

private List<SequenceElement> colorings = List.of();
private List<Edit> edits;
private final Map<Language,Map<Coloring, AttributeSet>> cache = new HashMap<>();
private final Document doc;

Expand Down Expand Up @@ -81,12 +76,8 @@ void setColorings(final SortedSet<SequenceElement> colorings) {
doc.render(() -> {
synchronized (GsfSemanticLayer.this) {
GsfSemanticLayer.this.colorings = List.copyOf(colorings);
GsfSemanticLayer.this.edits = new ArrayList<>();

fireHighlightsChange(0, doc.getLength()); //XXX: locking

DocumentUtilities.removeDocumentListener(doc, GsfSemanticLayer.this, DocumentListenerPriority.LEXER);
DocumentUtilities.addDocumentListener(doc, GsfSemanticLayer.this, DocumentListenerPriority.LEXER);
}
});
}
Expand Down Expand Up @@ -131,56 +122,7 @@ private void registerColoringChangeListener(Language language) {
);
coloringListeners.add(l);
}

@Override
public void insertUpdate(DocumentEvent e) {
synchronized (GsfSemanticLayer.this) {
edits.add(new Edit(e.getOffset(), e.getLength(), true));
}
}

@Override
public void removeUpdate(DocumentEvent e) {
synchronized (GsfSemanticLayer.this) {
edits.add(new Edit(e.getOffset(), e.getLength(), false));
}
}

@Override
public void changedUpdate(DocumentEvent e) {
}

// Compute an adjusted offset
public int getShiftedPos(int pos) {
int ret = pos;

for (Edit edit: edits) {
if (ret > edit.offset()) {
if (edit.insert()) {
ret += edit.len();
} else if (ret < edit.offset() + edit.len()) {
ret = edit.offset();
} else {
ret -= edit.len();
}
}
}
return ret;
}

/**
* An Edit is a modification (insert/remove) we've been notified about from the document
* since the last time we updated our "colorings" object.
* The list of Edits lets me quickly compute the current position of an original
* position in the "colorings" object. This is typically going to involve only a couple
* of edits (since the colorings object is updated as soon as the user stops typing).
* This is probably going to be more efficient than updating all the colorings offsets
* every time the document is updated, since the colorings object can contain thousands
* of ranges (e.g. for every highlight in the whole document) whereas asking for the
* current positions is typically only done for the highlights visible on the screen.
*/
private record Edit(int offset, int len, boolean insert) {}


/**
* An implementation of a HighlightsSequence which can show OffsetRange
* sections and keep them up to date during edits.
Expand All @@ -197,18 +139,27 @@ private final class GsfHighlightSequence implements HighlightsSequence {

@Override
public boolean moveNext() {
element = iterator.hasNext() ? iterator.next() : null;
return element != null;
while (iterator.hasNext()) {
SequenceElement i = iterator.next();
// Skip empty highlights, the editor can handle them, though not happy about it
// this could happen on deleting large portion of code
if (i.start().getOffset() != i.end().getOffset()) {
element = i;
return true;
}
}
element = null;
return false;
}

@Override
public int getStartOffset() {
return getShiftedPos(element.range().getStart());
return element.start().getOffset();
}

@Override
public int getEndOffset() {
return getShiftedPos(element.range().getEnd());
return element.end().getOffset();
}

@Override
Expand All @@ -233,7 +184,7 @@ static int firstSequenceElement(List<SequenceElement> l, int offset) {
while (low <= high) {
int mid = (low + high) >>> 1;
SequenceElement midVal = l.get(mid);
int cmp = midVal.range().getStart() - offset;
int cmp = midVal.start().getOffset() - offset;

if (cmp == 0) {
return mid;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ public HighlightsLayer[] createLayers(Context context) {

return new HighlightsLayer[] {
HighlightsLayer.create(SemanticHighlighter.class.getName() + "-1", ZOrder.SYNTAX_RACK.forPosition(1000), false, semantic),
// HighlightsLayer.create(SemanticHighlighter.class.getName() + "-2", ZOrder.SYNTAX_RACK.forPosition(1500), false, SemanticHighlighter.getImportHighlightsBag(context.getDocument())),
//the mark occurrences layer should be "above" current row and "below" the search layers:
HighlightsLayer.create(MarkOccurrencesHighlighter.class.getName(), ZOrder.CARET_RACK.forPosition(50), false, occurrences),
//"above" mark occurrences, "below" search layers:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.modules.csl.api.OffsetRange;
Expand Down Expand Up @@ -116,10 +117,13 @@ public void run(ParserResult info, SchedulerEvent event) {
GsfSemanticLayer layer = GsfSemanticLayer.getLayer(MarkOccurrencesHighlighter.class, doc);
SortedSet seqs = new TreeSet<SequenceElement>();

bag.stream()
.filter(range -> range != OffsetRange.NONE)
.forEach(range -> seqs.add(new SequenceElement(language, range, MO)));

for (OffsetRange range : bag) {
if (range != OffsetRange.NONE) {
try {
seqs.add(new SequenceElement(language, doc.createPosition(range.getStart()), doc.createPosition(range.getEnd()), MO));
} catch (BadLocationException ex) {}
}
}
layer.setColorings(seqs);

OccurrencesMarkProvider.get(doc).setOccurrences(OccurrencesMarkProvider.createMarks(doc, bag, ES_COLOR, NbBundle.getMessage(MarkOccurrencesHighlighter.class, "LBL_ES_TOOLTIP")));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.SwingUtilities;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import org.netbeans.modules.csl.api.ColoringAttributes;
import org.netbeans.modules.csl.api.ColoringAttributes.Coloring;
Expand Down Expand Up @@ -89,7 +90,7 @@ public final void cancel() {
long startTime = System.currentTimeMillis();

Source source = info.getSnapshot().getSource();
final SortedSet<SequenceElement> newColoring = new TreeSet<>();
final SortedSet<SequenceElement> newColoring = new TreeSet<>(SequenceElement.POSITION_ORDER);
try {
ParserManager.parse(Collections.singleton(source), (ResultIterator ri) -> processColorings(ri, newColoring));
} catch (ParseException e) {
Expand Down Expand Up @@ -157,7 +158,7 @@ private void process(Language language, ParserResult result, Set<SequenceElement
task.cancel();
return;
}

Document doc = result.getSnapshot().getSource().getDocument(false);
Map<OffsetRange,Set<ColoringAttributes>> highlights = task.getHighlights();
for (Map.Entry<OffsetRange, Set<ColoringAttributes>> entry : highlights.entrySet()) {

Expand All @@ -168,8 +169,11 @@ private void process(Language language, ParserResult result, Set<SequenceElement
}

Coloring c = manager.getColoring(colors);
try {
newColoring.add(new SequenceElement(language, doc.createPosition(range.getStart()), doc.createPosition(range.getEnd()), c));
} catch (BadLocationException ex) {

newColoring.add(new SequenceElement(language, range, c));
}

if (cancel.isCancelled()) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@

package org.netbeans.modules.csl.editor.semantic;

import java.util.Comparator;
import javax.swing.text.Position;
import org.netbeans.modules.csl.core.Language;
import org.netbeans.modules.csl.api.ColoringAttributes.Coloring;
import org.netbeans.modules.csl.api.OffsetRange;

/**
* Each SequeneceElement represents a OffsetRange/Coloring/Language tuple that
Expand All @@ -30,24 +31,9 @@
*
* @author Tor Norbye
*/
record SequenceElement(Language language, OffsetRange range, Coloring coloring) implements Comparable<SequenceElement> {
record SequenceElement(Language language, Position start, Position end, Coloring coloring) {

@Override
public int compareTo(SequenceElement o) {
assert o.range() != null;
return range.compareTo(o.range());
}

@Override
public boolean equals(Object obj) {
if (obj instanceof SequenceElement other) {
return range.equals(other.range());
}
return false;
}

@Override
public int hashCode() {
return range.hashCode();
}
public static final Comparator<? super SequenceElement> POSITION_ORDER =
(e1, e2) -> e1.start.getOffset() != e2.start.getOffset() ? e1.start.getOffset() - e2.start.getOffset()
: e1.end.getOffset() - e2.end.getOffset();
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,28 @@
package org.netbeans.modules.csl.editor.semantic;

import java.util.List;
import java.util.TreeSet;
import javax.swing.text.DefaultEditorKit;
import javax.swing.text.Document;
import javax.swing.text.SimpleAttributeSet;
import org.junit.Test;
import org.netbeans.modules.csl.api.ColoringAttributes;
import org.netbeans.modules.csl.api.OffsetRange;
import org.netbeans.modules.csl.core.Language;

import static org.junit.Assert.assertEquals;
import org.netbeans.spi.editor.highlighting.HighlightsSequence;


public class GsfSemanticLayerTest {

@Test
public void testFirstSequenceElement() {
public void testFirstSequenceElement() throws Exception {
Document doc = new DefaultEditorKit().createDefaultDocument();
doc.insertString(0, "Hello World!/n".repeat(10), SimpleAttributeSet.EMPTY);
List<SequenceElement> elements = List.of(
new SequenceElement(new Language("text/x-dummy"), new OffsetRange(10, 20), ColoringAttributes.empty()),
new SequenceElement(new Language("text/x-dummy"), new OffsetRange(30, 40), ColoringAttributes.empty()),
new SequenceElement(new Language("text/x-dummy"), new OffsetRange(50, 60), ColoringAttributes.empty())
new SequenceElement(new Language("text/x-dummy"), doc.createPosition(10), doc.createPosition(20), ColoringAttributes.empty()),
new SequenceElement(new Language("text/x-dummy"), doc.createPosition(30), doc.createPosition(40), ColoringAttributes.empty()),
new SequenceElement(new Language("text/x-dummy"), doc.createPosition(50), doc.createPosition(60), ColoringAttributes.empty())
);

assertEquals(0, GsfSemanticLayer.firstSequenceElement(elements, -1));
Expand All @@ -61,4 +67,34 @@ public void testFirstSequenceElement() {
assertEquals(0, GsfSemanticLayer.firstSequenceElement(List.of(), 120));
}

@Test
public void testHighlightSequence() throws Exception {
Document doc = new DefaultEditorKit().createDefaultDocument();
doc.insertString(0, "Hello World!/n".repeat(10), SimpleAttributeSet.EMPTY);

GsfSemanticLayer layer = GsfSemanticLayer.getLayer(GsfSemanticLayer.class, doc);
Language lang = new Language("text/x-dummy");

TreeSet<SequenceElement> highlights = new TreeSet<>(SequenceElement.POSITION_ORDER);

highlights.add(new SequenceElement(lang, doc.createPosition(10), doc.createPosition(20), ColoringAttributes.empty()));
highlights.add(new SequenceElement(lang, doc.createPosition(50), doc.createPosition(60), ColoringAttributes.empty()));
highlights.add(new SequenceElement(lang, doc.createPosition(30), doc.createPosition(40), ColoringAttributes.empty()));
highlights.add(new SequenceElement(lang, doc.createPosition(70), doc.createPosition(80), ColoringAttributes.empty()));

layer.setColorings(highlights);

HighlightsSequence seq = layer.getHighlights(0, doc.getLength());
assertEquals(4, countSequenceElements(seq));

doc.remove(30, 40); //remove thw two highlighted area in the middle
seq = layer.getHighlights(0, doc.getLength());
assertEquals(2, countSequenceElements(seq));
}

private static int countSequenceElements(HighlightsSequence seq) {
int ret = 0;
while (seq.moveNext()) ret++;
return ret;
}
}

0 comments on commit 4cc8d6f

Please sign in to comment.