You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@netbeans.apache.org by jl...@apache.org on 2019/12/17 22:01:39 UTC

[netbeans] branch master updated: A first version of mark occurrences via the language server protocol.

This is an automated email from the ASF dual-hosted git repository.

jlahoda pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/netbeans.git


The following commit(s) were added to refs/heads/master by this push:
     new 6debcdd  A first version of mark occurrences via the language server protocol.
6debcdd is described below

commit 6debcdd72693aa4c5a10d6f99a2389a51d311b8f
Author: Jan Lahoda <jl...@netbeans.org>
AuthorDate: Fri Nov 22 08:47:03 2019 +0100

    A first version of mark occurrences via the language server protocol.
---
 .../netbeans/modules/lsp/client/LSPBindings.java   |  14 ++
 .../client/bindings/GoToMarkOccurrencesAction.java | 139 ++++++++++++++++
 .../lsp/client/bindings/MarkOccurrences.java       | 182 +++++++++++++++++++++
 .../TextDocumentSyncServerCapabilityHandler.java   |   5 +
 4 files changed, 340 insertions(+)

diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/LSPBindings.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/LSPBindings.java
index 5a4d60a..206b9b9 100644
--- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/LSPBindings.java
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/LSPBindings.java
@@ -24,6 +24,7 @@ import java.io.OutputStream;
 import java.net.InetAddress;
 import java.net.Socket;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.IdentityHashMap;
 import java.util.LinkedHashMap;
@@ -235,6 +236,19 @@ public class LSPBindings {
         WORKER.post(req, DELAY);
     }
 
+    public static void rescheduleBackgroundTask(FileObject file, BackgroundTask task) {
+        LSPBindings bindings = getBindings(file);
+
+        if (bindings == null)
+            return ;
+
+        RequestProcessor.Task req = bindings.backgroundTasks.computeIfAbsent(file, f -> Collections.emptyMap()).get(task);
+
+        if (req != null) {
+            WORKER.post(req, DELAY);
+        }
+    }
+
     public void scheduleBackgroundTasks(FileObject file) {
         backgroundTasks.computeIfAbsent(file, f -> new IdentityHashMap<>()).values().stream().forEach(this::scheduleBackgroundTask);
     }
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/GoToMarkOccurrencesAction.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/GoToMarkOccurrencesAction.java
new file mode 100644
index 0000000..4db9dd7
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/GoToMarkOccurrencesAction.java
@@ -0,0 +1,139 @@
+/*
+ * 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.lsp.client.bindings;
+
+import java.awt.event.ActionEvent;
+import javax.swing.text.Document;
+import javax.swing.text.JTextComponent;
+import org.netbeans.api.editor.EditorActionNames;
+import org.netbeans.api.editor.EditorActionRegistration;
+import org.netbeans.editor.BaseAction;
+import org.netbeans.lib.editor.util.swing.DocumentUtilities;
+import org.netbeans.spi.editor.highlighting.HighlightsSequence;
+import org.netbeans.spi.editor.highlighting.support.OffsetsBag;
+import org.openide.awt.StatusDisplayer;
+import org.openide.util.NbBundle;
+
+/**
+ *
+ * @author Vladimir Voskresensky
+ */
+@NbBundle.Messages({
+    "next-marked-occurrence=Navigate to Next Occurrence",
+    "prev-marked-occurrence=Navigate to Previous Occurrence",
+    "no-marked-occurrence=There is no occurrence to navigate to."
+})
+public class GoToMarkOccurrencesAction extends BaseAction {
+
+    private static final String prevActionName = EditorActionNames.gotoPrevOccurrence;
+    private static final String nextActionName = EditorActionNames.gotoNextOccurrence;
+    
+    static final String markedOccurence = "marked-occurrence"; // NOI18N
+
+    private final boolean next;
+
+    public GoToMarkOccurrencesAction(boolean nextOccurrence) {
+        super(getNameString(nextOccurrence));
+        this.next = nextOccurrence;
+        putValue(SHORT_DESCRIPTION, getDefaultShortDescription());
+    }
+
+    public void actionPerformed(ActionEvent evt, JTextComponent txt) {
+        navigateToOccurence(next, txt);
+    }
+
+    @Override
+    protected Object getDefaultShortDescription() {
+        return NbBundle.getMessage(GoToMarkOccurrencesAction.class, getNameString(next));
+    }
+    
+    private static String getNameString(boolean nextOccurrence) {
+        return nextOccurrence ? nextActionName : prevActionName;
+    }
+    
+    @SuppressWarnings("empty-statement")
+    private static int findOccurrencePosition(boolean directionForward, Document doc, int curPos) {
+        OffsetsBag bag = MarkOccurrences.getHighlightsBag(doc);
+        HighlightsSequence hs = bag.getHighlights(0, doc.getLength());
+
+        if (hs.moveNext()) {
+            if (directionForward) {
+                int firstStart = hs.getStartOffset(), firstEnd = hs.getEndOffset();
+
+                while (hs.getStartOffset() <= curPos && hs.moveNext());
+
+                if (hs.getStartOffset() > curPos) {
+                    // we found next occurrence
+                    return hs.getStartOffset();
+                } else if (!(firstEnd >= curPos && firstStart <= curPos)) {
+                    // cyclic jump to first occurrence unless we already there
+                    return firstStart;
+                }
+            } else {
+                int current = hs.getStartOffset(), last;
+                boolean stuck = false;
+                do {
+                    last = current;
+                    current = hs.getStartOffset();
+                } while (hs.getEndOffset() < curPos && (stuck = hs.moveNext()));
+
+                if (last == current) {
+                    // we got no options to jump, cyclic jump to last in file unless we already there
+                    while (hs.moveNext());
+                    if (!(hs.getEndOffset() >= curPos && hs.getStartOffset() <= curPos)) {
+                        return hs.getStartOffset();
+                    }
+                } else if (stuck) {
+                    // just move to previous occurence
+                    return last;
+                } else {
+                    // it was last occurence in the file
+                    return current;
+                }
+            }
+        }
+        return -1;
+    }
+
+    private static void navigateToOccurence(boolean next, JTextComponent txt) {
+        if (txt != null && txt.getDocument() != null) {
+            Document doc = txt.getDocument();
+            int position = txt.getCaretPosition();
+            int goTo = findOccurrencePosition(next, doc, position);
+            if (goTo > 0) {
+                txt.setCaretPosition(goTo);
+                doc.putProperty(markedOccurence, new long[] {DocumentUtilities.getDocumentVersion(doc), goTo});
+            } else {
+                StatusDisplayer.getDefault().setStatusText(NbBundle.getMessage(GoToMarkOccurrencesAction.class, "no-marked-occurrence"));
+                doc.putProperty(markedOccurence, null);
+            }
+        }
+    }
+    
+    @EditorActionRegistration(name=nextActionName)
+    public static GoToMarkOccurrencesAction createNextAction() {
+        return new GoToMarkOccurrencesAction(true);
+    }
+
+    @EditorActionRegistration(name=prevActionName)
+    public static GoToMarkOccurrencesAction createPrevAction() {
+        return new GoToMarkOccurrencesAction(false);
+    }
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/MarkOccurrences.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/MarkOccurrences.java
new file mode 100644
index 0000000..4e83557
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/MarkOccurrences.java
@@ -0,0 +1,182 @@
+/*
+ * 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.lsp.client.bindings;
+
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.swing.event.CaretEvent;
+import javax.swing.event.CaretListener;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import javax.swing.text.AttributeSet;
+import javax.swing.text.BadLocationException;
+import javax.swing.text.Document;
+import javax.swing.text.JTextComponent;
+import org.eclipse.lsp4j.DocumentHighlight;
+import org.eclipse.lsp4j.TextDocumentIdentifier;
+import org.eclipse.lsp4j.TextDocumentPositionParams;
+import org.netbeans.api.editor.mimelookup.MimeLookup;
+import org.netbeans.api.editor.mimelookup.MimeRegistration;
+import org.netbeans.api.editor.settings.AttributesUtilities;
+import org.netbeans.api.editor.settings.FontColorSettings;
+import org.netbeans.modules.editor.NbEditorUtilities;
+import org.netbeans.modules.lsp.client.LSPBindings;
+import org.netbeans.modules.lsp.client.LSPBindings.BackgroundTask;
+import org.netbeans.modules.lsp.client.Utils;
+import org.netbeans.spi.editor.highlighting.HighlightsLayer;
+import org.netbeans.spi.editor.highlighting.HighlightsLayerFactory;
+import org.netbeans.spi.editor.highlighting.ZOrder;
+import org.netbeans.spi.editor.highlighting.support.OffsetsBag;
+import org.openide.filesystems.FileObject;
+import org.openide.loaders.DataObject;
+import org.openide.util.Exceptions;
+import org.openide.util.RequestProcessor;
+
+/**
+ *
+ * @author lahvac
+ */
+public class MarkOccurrences implements BackgroundTask, CaretListener, PropertyChangeListener {
+
+    private static final RequestProcessor WORKER = new RequestProcessor(MarkOccurrences.class.getName(), 1, false, false);
+    private final JTextComponent component;
+    private Document doc;
+    private int caretPos;
+
+    public MarkOccurrences(JTextComponent component) {
+        this.component = component;
+        component.addCaretListener(this);
+        component.addPropertyChangeListener(this);
+        doc = component.getDocument();
+        caretPos = component.getCaretPosition();
+    }
+
+    @Override
+    public void run(LSPBindings bindings, FileObject file) {
+        Document doc;
+        int caretPos;
+        synchronized (this) {
+            doc = this.doc;
+            caretPos = this.caretPos;
+        }
+        getHighlightsBag(doc).setHighlights(computeHighlights(doc, caretPos));
+    }
+
+    private OffsetsBag computeHighlights(Document doc, int caretPos) {
+        AttributeSet attr = getColoring(doc);
+        OffsetsBag result = new OffsetsBag(doc);
+        FileObject file = NbEditorUtilities.getFileObject(doc);
+        if (file == null) {
+            return result;
+        }
+        LSPBindings server = LSPBindings.getBindings(file);
+        if (server == null) {
+            return result;
+        }
+        String uri = Utils.toURI(file);
+        try {
+            List<? extends DocumentHighlight> highlights =
+                    server.getTextDocumentService().documentHighlight(new TextDocumentPositionParams(new TextDocumentIdentifier(uri), Utils.createPosition(doc, caretPos))).get();
+            for (DocumentHighlight h : highlights) {
+                result.addHighlight(Utils.getOffset(doc, h.getRange().getStart()), Utils.getOffset(doc, h.getRange().getEnd()), attr);
+            }
+            return result;
+        } catch (BadLocationException | InterruptedException | ExecutionException ex) {
+            Exceptions.printStackTrace(ex);
+            return result;
+        }
+    }
+
+    private AttributeSet getColoring(Document doc) {
+        FontColorSettings fcs = MimeLookup.getLookup(NbEditorUtilities.getMimeType(doc)).lookup(FontColorSettings.class);
+        
+        if (fcs == null) {
+            //in tests:
+            return AttributesUtilities.createImmutable();
+        }
+        
+        assert fcs != null;
+        
+        return fcs.getTokenFontColors("mark-occurrences");
+    }
+
+    @Override
+    public synchronized void caretUpdate(CaretEvent e) {
+        caretPos = e.getDot();
+        WORKER.post(() -> {
+            FileObject file = NbEditorUtilities.getFileObject(doc);
+
+            if (file != null) {
+                LSPBindings.rescheduleBackgroundTask(file, this);
+            }
+        });
+    }
+
+    @Override
+    public synchronized void propertyChange(PropertyChangeEvent evt) {
+        if (evt.getPropertyName() == null || "document".equals(evt.getPropertyName())) {
+            doc = component.getDocument();
+        }
+    }
+
+    static OffsetsBag getHighlightsBag(Document doc) {
+        OffsetsBag bag = (OffsetsBag) doc.getProperty(MarkOccurrences.class);
+
+        if (bag == null) {
+            doc.putProperty(MarkOccurrences.class, bag = new OffsetsBag(doc, false));
+
+            Object stream = doc.getProperty(Document.StreamDescriptionProperty);
+            final OffsetsBag bagFin = bag;
+            DocumentListener l = new DocumentListener() {
+                public void insertUpdate(DocumentEvent e) {
+                    bagFin.removeHighlights(e.getOffset(), e.getOffset(), false);
+                }
+                public void removeUpdate(DocumentEvent e) {
+                    bagFin.removeHighlights(e.getOffset(), e.getOffset(), false);
+                }
+                public void changedUpdate(DocumentEvent e) {}
+            };
+
+            doc.addDocumentListener(l);
+
+            if (stream instanceof DataObject) {
+                Logger.getLogger("TIMER").log(Level.FINE, "LSP Client MarkOccurrences Highlights Bag", new Object[] {((DataObject) stream).getPrimaryFile(), bag}); //NOI18N
+                Logger.getLogger("TIMER").log(Level.FINE, "LSP Client MarkOccurrences Highlights Bag Listener", new Object[] {((DataObject) stream).getPrimaryFile(), l}); //NOI18N
+            }
+        }
+
+        return bag;
+    }
+    
+    @MimeRegistration(mimeType="", service=HighlightsLayerFactory.class)
+    public static class HighlightsLayerFactoryImpl implements HighlightsLayerFactory {
+
+        public HighlightsLayer[] createLayers(HighlightsLayerFactory.Context context) {
+            return new HighlightsLayer[] {
+                //the mark occurrences layer should be "above" current row and "below" the search layers:
+                HighlightsLayer.create(MarkOccurrences.class.getName(), ZOrder.SHOW_OFF_RACK.forPosition(20), true, MarkOccurrences.getHighlightsBag(context.getDocument())),
+            };
+        }
+
+    }
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/TextDocumentSyncServerCapabilityHandler.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/TextDocumentSyncServerCapabilityHandler.java
index c4af00f..bab8e44 100644
--- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/TextDocumentSyncServerCapabilityHandler.java
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/TextDocumentSyncServerCapabilityHandler.java
@@ -112,6 +112,11 @@ public class TextDocumentSyncServerCapabilityHandler {
                                                                          text[0]);
 
                 server.getTextDocumentService().didOpen(new DidOpenTextDocumentParams(textDocumentItem));
+                if (opened.getClientProperty(MarkOccurrences.class) == null) {
+                    MarkOccurrences mo = new MarkOccurrences(opened);
+                    LSPBindings.addBackgroundTask(file, mo);
+                    opened.putClientProperty(MarkOccurrences.class, mo);
+                }
                 server.scheduleBackgroundTasks(file);
             });
 


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@netbeans.apache.org
For additional commands, e-mail: commits-help@netbeans.apache.org

For further information about the NetBeans mailing lists, visit:
https://cwiki.apache.org/confluence/display/NETBEANS/Mailing+lists