From 2c140c7e56a43f5f53d75b412aabc1ea554d0f97 Mon Sep 17 00:00:00 2001 From: Laszlo Kishalmi Date: Sun, 29 Dec 2024 15:23:09 -0800 Subject: [PATCH] Better change tracking for Semantic highlighting for CSL --- .../csl/editor/semantic/ColoringManager.java | 20 +---- .../csl/editor/semantic/GsfSemanticLayer.java | 81 ++++--------------- .../semantic/HighlightsLayerFactoryImpl.java | 1 - .../semantic/MarkOccurrencesHighlighter.java | 12 ++- .../editor/semantic/SemanticHighlighter.java | 10 ++- .../csl/editor/semantic/SequenceElement.java | 26 ++---- .../editor/semantic/GsfSemanticLayerTest.java | 46 +++++++++-- 7 files changed, 82 insertions(+), 114 deletions(-) diff --git a/ide/csl.api/src/org/netbeans/modules/csl/editor/semantic/ColoringManager.java b/ide/csl.api/src/org/netbeans/modules/csl/editor/semantic/ColoringManager.java index e13dd0f88e31..3f8bd918f4e5 100644 --- a/ide/csl.api/src/org/netbeans/modules/csl/editor/semantic/ColoringManager.java +++ b/ide/csl.api/src/org/netbeans/modules/csl/editor/semantic/ColoringManager.java @@ -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; @@ -65,10 +63,6 @@ public final class ColoringManager { private final String mimeType; private final Map, 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; @@ -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, String> attribs2Colorings : type2Coloring.entrySet()) { if (es.containsAll(attribs2Colorings.getKey())) { String key = attribs2Colorings.getValue(); @@ -204,12 +196,8 @@ private static AttributeSet adjustAttributes(AttributeSet as) { return AttributesUtilities.createImmutable(attrs.toArray()); } - - private final class UnusedTooltipResolver implements HighlightAttributeValue { - @Override - public String getValue(JTextComponent component, Document document, Object attributeKey, int startOffset, final int endOffset) { - return NbBundle.getMessage(ColoringManager.class, "LBL_UNUSED"); - } - } + private static final HighlightAttributeValue UNUSED_TOOLTIP_RESOLVER = + (component, document, attributeKey, startOffset, endOffset) -> NbBundle.getMessage(ColoringManager.class, "LBL_UNUSED"); + } diff --git a/ide/csl.api/src/org/netbeans/modules/csl/editor/semantic/GsfSemanticLayer.java b/ide/csl.api/src/org/netbeans/modules/csl/editor/semantic/GsfSemanticLayer.java index dda65a3a07ce..a95473dab383 100644 --- a/ide/csl.api/src/org/netbeans/modules/csl/editor/semantic/GsfSemanticLayer.java +++ b/ide/csl.api/src/org/netbeans/modules/csl/editor/semantic/GsfSemanticLayer.java @@ -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; @@ -49,10 +45,9 @@ * * @author Tor Norbye */ -public final class GsfSemanticLayer extends AbstractHighlightsContainer implements DocumentListener { +public final class GsfSemanticLayer extends AbstractHighlightsContainer { private List colorings = List.of(); - private List edits; private final Map> cache = new HashMap<>(); private final Document doc; @@ -81,12 +76,8 @@ void setColorings(final SortedSet 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); } }); } @@ -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. @@ -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 @@ -233,7 +184,7 @@ static int firstSequenceElement(List 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; diff --git a/ide/csl.api/src/org/netbeans/modules/csl/editor/semantic/HighlightsLayerFactoryImpl.java b/ide/csl.api/src/org/netbeans/modules/csl/editor/semantic/HighlightsLayerFactoryImpl.java index 1b97904c17c9..e7efc7ce664d 100644 --- a/ide/csl.api/src/org/netbeans/modules/csl/editor/semantic/HighlightsLayerFactoryImpl.java +++ b/ide/csl.api/src/org/netbeans/modules/csl/editor/semantic/HighlightsLayerFactoryImpl.java @@ -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: diff --git a/ide/csl.api/src/org/netbeans/modules/csl/editor/semantic/MarkOccurrencesHighlighter.java b/ide/csl.api/src/org/netbeans/modules/csl/editor/semantic/MarkOccurrencesHighlighter.java index dd9854400374..302bf517d143 100644 --- a/ide/csl.api/src/org/netbeans/modules/csl/editor/semantic/MarkOccurrencesHighlighter.java +++ b/ide/csl.api/src/org/netbeans/modules/csl/editor/semantic/MarkOccurrencesHighlighter.java @@ -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; @@ -116,10 +117,13 @@ public void run(ParserResult info, SchedulerEvent event) { GsfSemanticLayer layer = GsfSemanticLayer.getLayer(MarkOccurrencesHighlighter.class, doc); SortedSet seqs = new TreeSet(); - 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"))); diff --git a/ide/csl.api/src/org/netbeans/modules/csl/editor/semantic/SemanticHighlighter.java b/ide/csl.api/src/org/netbeans/modules/csl/editor/semantic/SemanticHighlighter.java index 1176d171250a..ac6499cc031f 100644 --- a/ide/csl.api/src/org/netbeans/modules/csl/editor/semantic/SemanticHighlighter.java +++ b/ide/csl.api/src/org/netbeans/modules/csl/editor/semantic/SemanticHighlighter.java @@ -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; @@ -89,7 +90,7 @@ public final void cancel() { long startTime = System.currentTimeMillis(); Source source = info.getSnapshot().getSource(); - final SortedSet newColoring = new TreeSet<>(); + final SortedSet newColoring = new TreeSet<>(SequenceElement.POSITION_ORDER); try { ParserManager.parse(Collections.singleton(source), (ResultIterator ri) -> processColorings(ri, newColoring)); } catch (ParseException e) { @@ -157,7 +158,7 @@ private void process(Language language, ParserResult result, Set> highlights = task.getHighlights(); for (Map.Entry> entry : highlights.entrySet()) { @@ -168,8 +169,11 @@ private void process(Language language, ParserResult result, Set { +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 POSITION_ORDER = + (e1, e2) -> e1.start.getOffset() != e2.start.getOffset() ? e1.start.getOffset() - e2.start.getOffset() + : e1.end.getOffset() - e2.end.getOffset(); } diff --git a/ide/csl.api/test/unit/src/org/netbeans/modules/csl/editor/semantic/GsfSemanticLayerTest.java b/ide/csl.api/test/unit/src/org/netbeans/modules/csl/editor/semantic/GsfSemanticLayerTest.java index cb5bb93c7174..59f590661b38 100644 --- a/ide/csl.api/test/unit/src/org/netbeans/modules/csl/editor/semantic/GsfSemanticLayerTest.java +++ b/ide/csl.api/test/unit/src/org/netbeans/modules/csl/editor/semantic/GsfSemanticLayerTest.java @@ -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 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)); @@ -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 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; + } }