All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.netbeans.modules.jumpto.file.FileSearchAction Maven / Gradle / Ivy

The newest version!
/*
 * 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.
 */
/*
 * Contributor(s): [email protected]
 */

package org.netbeans.modules.jumpto.file;

import org.netbeans.modules.jumpto.common.CurrentSearch;
import java.awt.Dialog;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.AbstractAction;
import javax.swing.ButtonModel;
import javax.swing.DefaultListModel;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JEditorPane;
import javax.swing.JList;
import javax.swing.ListCellRenderer;
import javax.swing.ListModel;
import javax.swing.SwingUtilities;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.annotations.common.StaticResource;
import org.netbeans.api.project.FileOwnerQuery;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.ui.OpenProjects;
import org.netbeans.editor.JumpList;
import org.netbeans.modules.jumpto.EntityComparator;
import org.netbeans.modules.jumpto.common.AbstractModelFilter;
import org.netbeans.modules.jumpto.common.ItemRenderer;
import org.netbeans.modules.jumpto.common.Models;
import org.netbeans.modules.jumpto.common.Utils;
import org.netbeans.modules.jumpto.settings.GoToSettings;
import org.netbeans.modules.parsing.lucene.support.Queries;
import org.netbeans.modules.parsing.spi.indexing.support.QuerySupport;
import org.netbeans.spi.jumpto.file.FileDescriptor;
import org.netbeans.spi.jumpto.type.SearchType;
import org.openide.DialogDescriptor;
import org.openide.DialogDisplayer;
import org.openide.awt.Mnemonics;
import org.openide.cookies.EditorCookie;
import org.openide.filesystems.FileObject;
import org.openide.loaders.DataObject;
import org.openide.nodes.Node;
import org.openide.text.NbDocument;
import org.openide.util.HelpCtx;
import org.openide.util.ImageUtilities;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.util.Pair;
import org.openide.util.Parameters;
import org.openide.util.RequestProcessor;
import org.openide.util.Utilities;
import org.openide.windows.TopComponent;
/**
 *
 * @author Andrei Badea, Petr Hrebejk, Tomas Zezula
 */
public class FileSearchAction extends AbstractAction implements FileSearchPanel.ContentProvider {

    /* package */ static final Logger LOGGER = Logger.getLogger(FileSearchAction.class.getName());
    /* package */ static final String CAMEL_CASE_SEPARATOR = "\\p{javaUpperCase}|-|_|\\.";    //NOI18N
    /* package */ static final String CAMEL_CASE_PART_CASE_SENSITIVE = "\\p{javaLowerCase}|\\p{Digit}|\\$";         //NOI18N
    /* package */ static final String CAMEL_CASE_PART_CASE_INSENSITIVE = "\\p{javaLowerCase}|\\p{Digit}|\\p{javaUpperCase}|\\$";         //NOI18N
    /* package */ static final Map SEARCH_OPTIONS_CASE_SENSITIVE;
    /* package */ static final Map SEARCH_OPTIONS_CASE_INSENSITIVE;
    static {
        Map m = new HashMap<>();
        m.put(Queries.OPTION_CAMEL_CASE_SEPARATOR, CAMEL_CASE_SEPARATOR);
        m.put(Queries.OPTION_CAMEL_CASE_PART, CAMEL_CASE_PART_CASE_SENSITIVE);
        SEARCH_OPTIONS_CASE_SENSITIVE = Collections.unmodifiableMap(m);
        m = new HashMap<>();
        m.put(Queries.OPTION_CAMEL_CASE_SEPARATOR, CAMEL_CASE_SEPARATOR);
        m.put(Queries.OPTION_CAMEL_CASE_PART, CAMEL_CASE_PART_CASE_INSENSITIVE);
        SEARCH_OPTIONS_CASE_INSENSITIVE = Collections.unmodifiableMap(m);
    }
    private static final char LINE_NUMBER_SEPARATOR = ':';    //NOI18N
    private static final Pattern PATTERN_WITH_LINE_NUMBER = Pattern.compile("(.*)"+LINE_NUMBER_SEPARATOR+"(\\d*)");    //NOI18N

    private static final ListModel EMPTY_LIST_MODEL = new DefaultListModel();
    private static final RequestProcessor slidingRp = new RequestProcessor("FileSearchAction-sliding",1);
    //Threading: Throughput 1 required due to inherent sequential code in Work.Request.exclude
    private static final RequestProcessor rp = new RequestProcessor ("FileSearchAction-worker",1);
    private final FilterFactory filterFactory = new FilterFactory();
    private final CurrentSearch currentSearch = new CurrentSearch(filterFactory);
    private final RequestProcessor.Task slidingTask = slidingRp.create(this::invokeProviders);
    //@GuardedBy("this")
    private FileComarator itemsComparator;
    //@GuardedBy("this")
    private Worker[] running;
    //@GuardedBy("this")
    private RequestProcessor.Task[] scheduledTasks;
    //@GuardedBy("this")
    private Request slidingTaskData;
    private Dialog dialog;
    private JButton openBtn;
    private FileSearchPanel panel;
    private Dimension initialDimension;

    public FileSearchAction() {
        super( NbBundle.getMessage(FileSearchAction.class, "CTL_FileSearchAction") );
        // XXX this should be in initialize()?
        putValue("PopupMenuText", NbBundle.getBundle(FileSearchAction.class).getString("editor-popup-CTL_FileSearchAction")); // NOI18N
        putValue("noIconInMenu", Boolean.TRUE); // NOI18N
    }

    @Override
    public boolean isEnabled() {
        return OpenProjects.getDefault().getOpenProjects().length > 0;
    }

    @Override
    public void actionPerformed(ActionEvent arg0) {
        FileDescriptor[] typeDescriptors = getSelectedFiles();
        if (typeDescriptors != null) {
            JumpList.checkAddEntry();
            for(FileDescriptor td: typeDescriptors){
                td.open();
            }
        }
    }

    // Implementation of content provider --------------------------------------


    @Override
    @NonNull
    public ListCellRenderer getListCellRenderer(
            @NonNull final JList list,
            @NonNull final Document nameDocument,
            @NonNull final ButtonModel caseSensitive,
            @NonNull final ButtonModel colorPrefered,
            @NonNull final ButtonModel searchFolders) {
        Parameters.notNull("list", list);   //NOI18N
        Parameters.notNull("nameDocument", nameDocument);   //NOI18N
        Parameters.notNull("caseSensitive", caseSensitive); //NOI18N
        return ItemRenderer.Builder.create(
                    list,
                    caseSensitive,
                    new FileDescriptorConvertor(nameDocument)).
                setCamelCaseSeparator(CAMEL_CASE_SEPARATOR).
                setColorPreferedProject(colorPrefered).
                setSearchFolders(searchFolders).
                build();
    }


    @Override
    public boolean setListModel(final FileSearchPanel panel, String text ) {
        enableOK(false);

        cancel();

        if ( text == null ) {
            panel.setModel(EMPTY_LIST_MODEL, true);
            currentSearch.resetFilter();
            return false;
        }
        boolean exact = text.endsWith(" "); // NOI18N
        text = text.trim();
        if ( text.length() == 0 || !Utils.isValidInput(text)) {
            currentSearch.filter(
                    SearchType.EXACT_NAME,
                    text,
                    Collections.singletonMap(AbstractModelFilter.OPTION_CLEAR, Boolean.TRUE));
            panel.revalidateModel(true);
            return false;
        }

        //Extract linenumber from search text
        //Pattern is like 'My*Object.java:123'
        final Pair nameLinePair = splitNameLine(text);
        text = nameLinePair.first();
        final int lineNr = nameLinePair.second();
        final QuerySupport.Kind nameKind = Utils.toQueryKind(Utils.getSearchType(
                text,
                exact,
                panel.isCaseSensitive(),
                CAMEL_CASE_SEPARATOR,
                CAMEL_CASE_PART_CASE_INSENSITIVE));
        if (nameKind == QuerySupport.Kind.REGEXP || nameKind == QuerySupport.Kind.CASE_INSENSITIVE_REGEXP) {
            text = Utils.removeNonNeededWildCards(text);
        }

        // Compute in other thread
        synchronized(this) {
            final SearchType searchType = Utils.toSearchType(nameKind);
            if (currentSearch.isNarrowing(searchType, text, getSearchScope(panel), true)) {
                itemsComparator.setUsePreferred(panel.isPreferedProject());
                itemsComparator.setText(text);
                filterFactory.setLineNumber(lineNr);
                filterFactory.setSearchByFolders(panel.isSearchByFolders());
                currentSearch.filter(
                        searchType,
                        text,
                        panel.isCaseSensitive() ? SEARCH_OPTIONS_CASE_SENSITIVE : SEARCH_OPTIONS_CASE_INSENSITIVE);
                enableOK(panel.searchCompleted(true));
                return false;
            } else {
                slidingTaskData = new Request(text, nameKind, searchType, lineNr);
                slidingTask.schedule(500);
                LOGGER.log(Level.FINE, "Scheduled for text: {0}", text);
                return true;
            }
        }
    }

    @Override
    public void closeDialog() {
        cleanup(true);
    }

    @Override
    public boolean hasValidContent () {
        return this.openBtn != null && this.openBtn.isEnabled();
    }

    // Private methods ---------------------------------------------------------

    private synchronized void invokeProviders() {
        if (slidingTaskData == null) {
            return;
        }
        final String searchText = slidingTaskData.text;
        LOGGER.log(Level.FINE, "Calling providers for text: {0}", searchText);
        itemsComparator = FileComarator.create(
                GoToSettings.getDefault().getSortingType(),
                searchText,
                panel.isPreferedProject(),
                panel.isCaseSensitive(),
                GoToSettings.getDefault().isSortingPreferOpenProjects());
        final Models.MutableListModel baseListModel = Models.mutable(
                itemsComparator,
                currentSearch.resetFilter(),
                null);
        panel.setModel(Models.refreshable(
                baseListModel,
                (@NonNull final Pair param) -> new AsyncFileDescriptor(param.first(), param.second())),
                false);
        final Worker.Request request = Worker.newRequest(
            searchText,
            slidingTaskData.kind,
            panel.getCurrentProject(),
            panel.isSearchByFolders(),
            slidingTaskData.lineNo);
        final Worker.Collector collector = Worker.newCollector(
                baseListModel,
                () -> {
                    SwingUtilities.invokeLater(() -> {
                        panel.searchProgress();
                        enableOK(baseListModel.getSize() > 0);
                    });},
                () -> {
                    currentSearch.searchCompleted(slidingTaskData.type, searchText, getSearchScope(panel));
                    SwingUtilities.invokeLater(() -> {
                        panel.searchCompleted(true);
                    });
                },
                panel.time);
        final Worker.Type[] wts = Worker.Type.values();
        final Worker[] workers = new Worker[wts.length];
        //Threading: All workers need to be created before they are scheduled
        for (int i = 0; i < wts.length; i++) {
            workers[i] = Worker.newWorker(request, collector, wts[i]);
        }
        running = workers;
        final RequestProcessor.Task[] tasks = new RequestProcessor.Task[workers.length];
        for (int i = 0; i < workers.length; i++) {
            tasks[i] = rp.post(workers[i]);
        }
        scheduledTasks = tasks;
        if ( panel.time != -1 ) {
            LOGGER.log(
                Level.FINE,
                "Worker posted after {0} ms.",  //NOI18N
                System.currentTimeMillis() - panel.time );
        }
    }

    private static String getSearchScope(final FileSearchPanel panel) {
        //the string constant doesn't really matter, just that they are different values
        return panel.isSearchByFolders() ? "folders-scope" : null;
    }

    private void enableOK(final boolean enable) {
        if (openBtn != null) {
            openBtn.setEnabled (enable);
        }
    }

    private FileDescriptor[] getSelectedFiles() {
        FileDescriptor[] result = null;
        panel = new FileSearchPanel(this, findCurrentProject());
        dialog = createDialog(panel);

        Node[] arr = TopComponent.getRegistry ().getActivatedNodes();
        if (arr.length > 0) {
            EditorCookie ec = arr[0].getCookie (EditorCookie.class);
            if (ec != null) {
                JEditorPane recentPane = NbDocument.findRecentEditorPane(ec);
                if (recentPane != null) {
                    String initSearchText = null;
                    if (org.netbeans.editor.Utilities.isSelectionShowing(recentPane.getCaret())) {
                        initSearchText = recentPane.getSelectedText();
                    }
                    if (initSearchText != null) {
                        if (initSearchText.length() > 256) {
                            initSearchText = initSearchText.substring(0, 256);
                        }
                        panel.setInitialText(initSearchText);
                    } else {
                        FileObject fo = arr[0].getLookup().lookup(FileObject.class);
                        if (fo != null) {
                            panel.setInitialText(fo.getNameExt());
                        }
                    }
                }
            }
        }

        dialog.setVisible(true);
        result = panel.getSelectedFiles();
        return result;
    }

   private Dialog createDialog( final FileSearchPanel panel) {
        openBtn = new JButton();
        Mnemonics.setLocalizedText(openBtn, NbBundle.getMessage(FileSearchAction.class, "CTL_Open"));
        openBtn.getAccessibleContext().setAccessibleDescription(openBtn.getText());
        openBtn.setEnabled( false );

        final Object[] buttons = new Object[] { openBtn, DialogDescriptor.CANCEL_OPTION };

        String title = NbBundle.getMessage(FileSearchAction.class, "MSG_FileSearchDlgTitle");
        DialogDescriptor dialogDescriptor = new DialogDescriptor(
                panel,
                title,
                true,
                buttons,
                openBtn,
                DialogDescriptor.DEFAULT_ALIGN,
                HelpCtx.DEFAULT_HELP,
                new DialogButtonListener(panel));
        dialogDescriptor.setClosingOptions(buttons);

        Dialog d = DialogDisplayer.getDefault().createDialog(dialogDescriptor);
        d.getAccessibleContext().setAccessibleName(NbBundle.getMessage(FileSearchAction.class, "AN_FileSearchDialog"));
        d.getAccessibleContext().setAccessibleDescription(NbBundle.getMessage(FileSearchAction.class, "AD_FileSearchDialog"));

        // Set size
        d.setPreferredSize( new Dimension(  FileSearchOptions.getWidth(),
                                                 FileSearchOptions.getHeight() ) );

        // Center the dialog after the size changed.
        Rectangle r = Utilities.getUsableScreenBounds();
        int maxW = (r.width * 9) / 10;
        int maxH = (r.height * 9) / 10;
        Dimension dim = d.getPreferredSize();
        dim.width = Math.min(dim.width, maxW);
        dim.height = Math.min(dim.height, maxH);
        initialDimension = dim;
        d.setBounds(Utilities.findCenterBounds(dim));
        d.addWindowListener(new WindowAdapter() {
            public @Override void windowClosed(WindowEvent e) {
                cleanup(false);
            }
        });

        return d;
    }

    /** For original of this code look at:
     *  org.netbeans.modules.project.ui.actions.ActionsUtil
     */
    private static Project findCurrentProject( ) {
        Lookup lookup = Utilities.actionsGlobalContext();

        // Maybe the project is in the lookup
        for (Project p : lookup.lookupAll(Project.class)) {
            return p;
        }
        // Now try to guess the project from dataobjects
        for (DataObject dObj : lookup.lookupAll(DataObject.class)) {
            FileObject fObj = dObj.getPrimaryFile();
            Project p = FileOwnerQuery.getOwner(fObj);
            if ( p != null ) {
                return p;
            }
        }

       return OpenProjects.getDefault().getMainProject();
    }

    private void cleanup(final boolean hide) {
        cancel();
        if ( dialog != null ) { // Closing event for some reson sent twice
            // Save dialog size only when changed
            if (hide) {
                dialog.setVisible(false);
            }
            final int currentWidth = dialog.getWidth();
            final int currentHeight = dialog.getHeight();
            if (initialDimension != null && (initialDimension.width != currentWidth || initialDimension.height != currentHeight)) {
                FileSearchOptions.setHeight(currentHeight);
                FileSearchOptions.setWidth(currentWidth);
            }
            initialDimension = null;
            // Clean caches
            dialog.dispose();
            this.dialog = null;
            FileSearchOptions.flush();
        }
    }

    private void cancel() {
        slidingTask.cancel();
        synchronized (this) {
            if ( running != null ) {
                for (Worker w : running) {
                    w.cancel();
                }
                for (RequestProcessor.Task t : scheduledTasks) {
                    t.cancel();
                }
                running = null;
                scheduledTasks = null;
                panel.searchCompleted(false);
            }
        }
    }

    @NonNull
    private static Pair splitNameLine(@NonNull String text) {
        final Matcher matcher = PATTERN_WITH_LINE_NUMBER.matcher(text);
        int lineNr = -1;
        if (matcher.matches()) {
            text = matcher.group(1);
            try {
                lineNr = Integer.parseInt(matcher.group(2));
            } catch (NumberFormatException e) {
               //pass
            }
        }
        return Pair.of(text,lineNr);
    }

    // Private classes ---------------------------------------------------------
    private class DialogButtonListener implements ActionListener {

        private FileSearchPanel panel;

        public DialogButtonListener(FileSearchPanel panel) {
            this.panel = panel;
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            if ( e.getSource() == openBtn) {
                panel.setSelectedFile();
            }
        }
    }

    private static final class Request {
        final String text;
        final QuerySupport.Kind kind;
        final SearchType type;
        final int lineNo;

        Request(
            @NonNull final String text,
            @NonNull final QuerySupport.Kind kind,
            @NonNull final SearchType type,
            final int lineNo) {
            this.text = text;
            this.kind = kind;
            this.type = type;
            this.lineNo = lineNo;
        }
    }

    private static final class AsyncFileDescriptor extends FileDescriptor implements Runnable {

        @StaticResource
        private static final String UNKNOWN_ICON_PATH = "org/netbeans/modules/jumpto/resources/unknown.gif";    //NOI18N
        private static final Icon UNKNOWN_ICON = ImageUtilities.loadImageIcon(UNKNOWN_ICON_PATH, false);
        private static final RequestProcessor LOAD_ICON_RP = new RequestProcessor(AsyncFileDescriptor.class.getName(), 1, false, false);

        private final FileDescriptor delegate;
        private final Runnable refreshCallback;
        private volatile Icon icon;

        AsyncFileDescriptor(
            @NonNull final FileDescriptor delegate,
            @NonNull final Runnable refreshCallback) {
            Parameters.notNull("delegate", delegate);   //NOI18N
            Parameters.notNull("refreshCallback", refreshCallback); //NOI18N
            this.delegate = delegate;
            this.refreshCallback = refreshCallback;
            FileProviderAccessor.getInstance().setFromCurrentProject(
                this,
                FileProviderAccessor.getInstance().isFromCurrentProject(delegate));
        }

        @Override
        public String getFileName() {
           return delegate.getFileName();
        }

        @Override
        public String getOwnerPath() {
            return delegate.getOwnerPath();
        }

        @Override
        public Icon getIcon() {
            if (icon != null) {
                return icon;
            }
            LOAD_ICON_RP.execute(this);
            return UNKNOWN_ICON;
        }

        @Override
        public String getProjectName() {
            return delegate.getProjectName();
        }

        @Override
        public Icon getProjectIcon() {
            return delegate.getProjectIcon();
        }

        @Override
        public void open() {
            delegate.open();
        }

        @Override
        public FileObject getFileObject() {
            final FileObject res = delegate.getFileObject();
            if (res == null) {
                LOGGER.log(
                    Level.FINE,
                    "FileDescriptor: {0} : {1} returned null from getFile", //NOI18N
                    new Object[]{
                        delegate,
                        delegate.getClass()
                    });
            }
            return res;
        }

        @Override
        public String getFileDisplayPath() {
            return delegate.getFileDisplayPath();
        }

        @Override
        public void run() {
            icon = delegate.getIcon();
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    refreshCallback.run();
                }
            });
        }

    }

    private static final class FileDescriptorConvertor implements ItemRenderer.Convertor, DocumentListener {
        private String textToFind = "";   //NOI18N

        FileDescriptorConvertor(@NonNull final Document doc) {
            doc.addDocumentListener(this);
        }

        @Override
        public String getName(@NonNull final FileDescriptor item) {
            return item.getFileName();
        }

        @Override
        public String getHighlightText(@NonNull final FileDescriptor item) {
            return textToFind;
        }

        @Override
        public String getOwnerName(@NonNull final FileDescriptor item) {
            return item.getOwnerPath().length() > 0 ? " (" + item.getOwnerPath() + ")" : "";    //NOI18N
        }

        @Override
        public String getProjectName(@NonNull final FileDescriptor item) {
            return item.getProjectName();
        }

        @Override
        public String getFilePath(@NonNull final FileDescriptor item) {
            return item.getFileDisplayPath();
        }

        @Override
        public Icon getItemIcon(@NonNull final FileDescriptor item) {
            return item.getIcon();
        }

        @Override
        public Icon getProjectIcon(@NonNull final FileDescriptor item) {
            return item.getProjectIcon();
        }

        @Override
        public boolean isFromCurrentProject(@NonNull final FileDescriptor item) {
            return FileProviderAccessor.getInstance().isFromCurrentProject(item);
        }

        @Override
        public void insertUpdate(DocumentEvent e) {
            changedUpdate(e);
        }

        @Override
        public void removeUpdate(DocumentEvent e) {
            changedUpdate(e);
        }

        @Override
        public void changedUpdate(DocumentEvent e) {
            try {
                textToFind = splitNameLine(e.getDocument().getText(0, e.getDocument().getLength())).first();
            } catch (BadLocationException ex) {
                textToFind = "";    //NOI18N
            }
        }

    }

    //@NotThreadSafe
    private static final class FilterFactory implements Callable> {
        private int currentLineNo;
        private boolean searchByFolders = false;

        void setSearchByFolders(boolean searchByFolders) {
            this.searchByFolders = searchByFolders;
        }

        void setLineNumber(final int lineNo) {
            this.currentLineNo = lineNo;
        }

        @Override
        public Models.Filter call() throws Exception {
            return new AbstractModelFilter() {
                @NonNull
                @Override
                protected String getItemValue(@NonNull final FileDescriptor item) {
                    return searchByFolders ? item.getOwnerPath() : item.getFileName();
                }
                @Override
                protected void update(@NonNull final FileDescriptor item) {
                    FileProviderAccessor.getInstance().setLineNumber(item, currentLineNo);
                }
            };
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy