blob: 3233b978fc05ff2a5096ce9bb2d1b89ed106d120 [file] [log] [blame]
/*
* 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.lib.editor.codetemplates;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.KeyStroke;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.EventListenerList;
import javax.swing.text.AbstractDocument;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
import org.netbeans.api.editor.mimelookup.MimeLookup;
import org.netbeans.api.editor.mimelookup.MimePath;
import org.netbeans.api.editor.settings.CodeTemplateDescription;
import org.netbeans.api.editor.settings.CodeTemplateSettings;
import org.netbeans.api.lexer.TokenHierarchy;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.lib.editor.codetemplates.api.CodeTemplate;
import org.netbeans.lib.editor.codetemplates.api.CodeTemplateManager;
import org.netbeans.lib.editor.codetemplates.spi.*;
import org.netbeans.lib.editor.codetemplates.storage.CodeTemplateSettingsImpl;
import org.netbeans.modules.editor.NbEditorUtilities;
import org.openide.util.Lookup;
import org.openide.util.LookupEvent;
import org.openide.util.LookupListener;
import org.openide.util.RequestProcessor;
import org.openide.util.WeakListeners;
/**
* Code template allows the client to paste itself into the given
* text component.
*
* @author Miloslav Metelka
*/
public final class CodeTemplateManagerOperation
implements LookupListener, Runnable
{
private static final Logger LOG = Logger.getLogger(CodeTemplateManagerOperation.class.getName());
private static final Map<MimePath, CodeTemplateManagerOperation> mime2operation =
new WeakHashMap<MimePath, CodeTemplateManagerOperation>(8);
private static final KeyStroke DEFAULT_EXPANSION_KEY = KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0);
public static synchronized CodeTemplateManager getManager(Document doc) {
String mimeType = (String)doc.getProperty("mimeType"); //NOI18N
return get(MimePath.parse(mimeType)).getManager();
}
public static synchronized CodeTemplateManagerOperation get(Document document, int offset) {
MimePath mimePath = getFullMimePath(document, offset);
if (mimePath != null) {
return CodeTemplateManagerOperation.get(mimePath);
} else {
return null;
}
}
public static synchronized CodeTemplateManagerOperation get(MimePath mimePath) {
CodeTemplateManagerOperation operation = mime2operation.get(mimePath);
if (operation == null) {
operation = new CodeTemplateManagerOperation(mimePath);
mime2operation.put(mimePath, operation);
}
return operation;
}
private final CodeTemplateManager manager;
private final String mimePath;
private final Lookup.Result<CodeTemplateSettings> ctslr;
private final EventListenerList listenerList = new EventListenerList();
private boolean loaded = false;
private Map<String, CodeTemplate> abbrev2template = Collections.<String, CodeTemplate>emptyMap();
private List<CodeTemplate> sortedTemplatesByAbbrev = Collections.<CodeTemplate>emptyList();
private List<CodeTemplate> sortedTemplatesByParametrizedText = Collections.<CodeTemplate>emptyList();
private List<CodeTemplate> selectionTemplates = Collections.<CodeTemplate>emptyList();
private KeyStroke expansionKey = DEFAULT_EXPANSION_KEY;
private String expansionKeyText = getExpandKeyStrokeText(expansionKey);
// Do not store mimePath in a private field, it would break the WeakHashMap cache
private CodeTemplateManagerOperation(MimePath mimePath) {
this.mimePath = mimePath.getPath();
this.manager = CodeTemplateApiPackageAccessor.get().createCodeTemplateManager(this);
assert manager != null : "Can't creat CodeTemplateManager"; //NOI18N
this.ctslr = MimeLookup.getLookup(mimePath).lookupResult(CodeTemplateSettings.class);
this.ctslr.addLookupListener(WeakListeners.create(LookupListener.class, this, this.ctslr));
// Compute descriptions asynchronously
RequestProcessor.getDefault().post(this);
}
public String getMimePath() {
return mimePath;
}
public CodeTemplateManager getManager() {
return manager;
}
public Collection<? extends CodeTemplate> getCodeTemplates() {
return sortedTemplatesByAbbrev;
}
public Collection<? extends CodeTemplate> findSelectionTemplates() {
return selectionTemplates;
}
public CodeTemplate findByAbbreviation(String abbreviation) {
return abbrev2template.get(abbreviation);
}
public Collection<? extends CodeTemplate> findByAbbreviationPrefix(String prefix, boolean ignoreCase) {
List<CodeTemplate> result = new ArrayList<CodeTemplate>();
int low = 0;
int high = sortedTemplatesByAbbrev.size() - 1;
while (low <= high) {
int mid = (low + high) >> 1;
CodeTemplate t = sortedTemplatesByAbbrev.get(mid);
int cmp = compareTextIgnoreCase(t.getAbbreviation(), prefix);
if (cmp < 0) {
low = mid + 1;
} else if (cmp > 0) {
high = mid - 1;
} else {
low = mid;
break;
}
}
// Go back whether prefix matches the name
int i = low - 1;
while (i >= 0) {
CodeTemplate t = sortedTemplatesByAbbrev.get(i);
int mp = matchPrefix(t.getAbbreviation(), prefix);
if (mp == MATCH_NO) { // not matched
break;
} else if (mp == MATCH_IGNORE_CASE) { // matched when ignoring case
if (ignoreCase) { // do not add if exact match required
result.add(t);
}
} else { // matched exactly
result.add(t);
}
i--;
}
i = low;
while (i < sortedTemplatesByAbbrev.size()) {
CodeTemplate t = sortedTemplatesByAbbrev.get(i);
int mp = matchPrefix(t.getAbbreviation(), prefix);
if (mp == MATCH_NO) { // not matched
break;
} else if (mp == MATCH_IGNORE_CASE) { // matched when ignoring case
if (ignoreCase) { // do not add if exact match required
result.add(t);
}
} else { // matched exactly
result.add(t);
}
i++;
}
return result;
}
public Collection<? extends CodeTemplate> findByParametrizedText(String prefix, boolean ignoreCase) {
List<CodeTemplate> result = new ArrayList<CodeTemplate>();
int low = 0;
int high = sortedTemplatesByParametrizedText.size() - 1;
while (low <= high) {
int mid = (low + high) >> 1;
CodeTemplate t = sortedTemplatesByParametrizedText.get(mid);
int cmp = compareTextIgnoreCase(t.getParametrizedText(), prefix);
if (cmp < 0) {
low = mid + 1;
} else if (cmp > 0) {
high = mid - 1;
} else {
low = mid;
break;
}
}
// Go back whether prefix matches the name
int i = low - 1;
while (i >= 0) {
CodeTemplate t = sortedTemplatesByParametrizedText.get(i);
int mp = matchPrefix(t.getParametrizedText(), prefix);
if (mp == MATCH_NO) { // not matched
break;
} else if (mp == MATCH_IGNORE_CASE) { // matched when ignoring case
if (ignoreCase) { // do not add if exact match required
result.add(t);
}
} else { // matched exactly
result.add(t);
}
i--;
}
i = low;
while (i < sortedTemplatesByParametrizedText.size()) {
CodeTemplate t = sortedTemplatesByParametrizedText.get(i);
int mp = matchPrefix(t.getParametrizedText(), prefix);
if (mp == MATCH_NO) { // not matched
break;
} else if (mp == MATCH_IGNORE_CASE) { // matched when ignoring case
if (ignoreCase) { // do not add if exact match required
result.add(t);
}
} else { // matched exactly
result.add(t);
}
i++;
}
return result;
}
public static Collection<? extends CodeTemplateFilter> getTemplateFilters(JTextComponent component, int offset) {
MimePath mimeType = getFullMimePath(component.getDocument(), offset);
Collection<? extends CodeTemplateFilter.Factory> filterFactories =
MimeLookup.getLookup(mimeType).lookupAll(CodeTemplateFilter.Factory.class);
List<CodeTemplateFilter> result = new ArrayList<CodeTemplateFilter>(filterFactories.size());
for (CodeTemplateFilter.Factory factory : filterFactories) {
result.add(factory.createFilter(component, offset));
}
return result;
}
public static void insert(CodeTemplate codeTemplate, JTextComponent component) {
String mimePath = CodeTemplateApiPackageAccessor.get().getCodeTemplateMimePath(codeTemplate);
Collection<? extends CodeTemplateProcessorFactory> processorFactories =
MimeLookup.getLookup(mimePath).lookupAll(CodeTemplateProcessorFactory.class);
CodeTemplateInsertHandler handler = new CodeTemplateInsertHandler(
codeTemplate, component, processorFactories, CodeTemplateSettingsImpl.get(MimePath.parse(mimePath)).getOnExpandAction());
handler.processTemplate();
}
/**
* Match text against the given prefix.
*
* @param text text to be compared with the prefix.
* @param prefix text to be matched as a prefix of the text parameter.
* @return one of <code>MATCH_NO</code>, <code>MATCH_IGNORE_CASE</code>
* or <code>MATCH</code>
*/
private static final int MATCH_NO = 0;
private static final int MATCH_IGNORE_CASE = 1;
private static final int MATCH = 2;
private static int matchPrefix(CharSequence text, CharSequence prefix) {
boolean matchCase = true;
int prefixLength = prefix.length();
if (prefixLength > text.length()) { // prefix longer than text
return MATCH_NO;
}
int i;
for (i = 0; i < prefixLength; i++) {
char ch1 = text.charAt(i);
char ch2 = prefix.charAt(i);
if (ch1 != ch2) {
matchCase = false;
if (Character.toLowerCase(ch1) != Character.toLowerCase(ch2)) {
break;
}
}
}
if (i == prefixLength) { // compared all
return matchCase ? MATCH : MATCH_IGNORE_CASE;
} else { // not compared all => not matched
return MATCH_NO;
}
}
private static int compareTextIgnoreCase(CharSequence text1, CharSequence text2) {
int len = Math.min(text1.length(), text2.length());
for (int i = 0; i < len; i++) {
char ch1 = Character.toLowerCase(text1.charAt(i));
char ch2 = Character.toLowerCase(text2.charAt(i));
if (ch1 != ch2) {
return ch1 - ch2;
}
}
return text1.length() - text2.length();
}
public boolean isLoaded() {
synchronized (listenerList) {
return loaded;
}
}
public void registerLoadedListener(ChangeListener listener) {
synchronized (listenerList) {
if (!isLoaded()) {
// not yet loaded
listenerList.add(ChangeListener.class, listener);
return;
}
}
// already loaded
listener.stateChanged(new ChangeEvent(manager));
}
public void waitLoaded() {
synchronized (listenerList) {
while(!isLoaded()) {
try {
listenerList.wait();
} catch (InterruptedException e) {
throw new RuntimeException("Interrupted when waiting to load code templates"); //NOI18N
}
}
}
}
private void fireStateChanged(ChangeEvent evt) {
Object[] listeners;
synchronized (listenerList) {
listeners = listenerList.getListenerList();
}
for (int i = 0; i < listeners.length; i += 2) {
if (ChangeListener.class == listeners[i]) {
((ChangeListener)listeners[i + 1]).stateChanged(evt);
}
}
}
public void run() {
rebuildCodeTemplates();
}
private static void processCodeTemplateDescriptions(
CodeTemplateManagerOperation operation,
Collection<? extends CodeTemplateDescription> ctds,
Map<String, CodeTemplate> codeTemplatesMap,
List<CodeTemplate> codeTemplatesWithSelection
) {
for (CodeTemplateDescription ctd : ctds) {
CodeTemplate ct = CodeTemplateApiPackageAccessor.get().createCodeTemplate(
operation,
ctd.getAbbreviation(),
ctd.getDescription(),
ctd.getParametrizedText(),
ctd.getContexts(),
ctd.getMimePath()
);
codeTemplatesMap.put(ct.getAbbreviation(), ct);
if (ct.getParametrizedText().toLowerCase().indexOf("${selection") > -1) { //NOI18N
codeTemplatesWithSelection.add(ct);
}
}
}
private void rebuildCodeTemplates() {
Collection<? extends CodeTemplateSettings> allCts = ctslr.allInstances();
CodeTemplateSettings cts = allCts.isEmpty() ? null : allCts.iterator().next();
Map<String, CodeTemplate> map = new HashMap<String, CodeTemplate>();
List<CodeTemplate> templatesWithSelection = new ArrayList<CodeTemplate>();
KeyStroke keyStroke = DEFAULT_EXPANSION_KEY;
if (cts != null) {
// Load templates
Collection<? extends CodeTemplateDescription> ctds = cts.getCodeTemplateDescriptions();
processCodeTemplateDescriptions(this, ctds, map, templatesWithSelection);
// Load expansion key
keyStroke = patchExpansionKey(cts.getExpandKey());
} else {
if (LOG.isLoggable(Level.WARNING)) {
LOG.warning("Can't find CodeTemplateSettings for '" + mimePath + "'"); //NOI18N
}
}
List<CodeTemplate> byAbbrev = new ArrayList<CodeTemplate>(map.values());
Collections.sort(byAbbrev, CodeTemplateComparator.BY_ABBREVIATION_IGNORE_CASE);
List<CodeTemplate> byText = new ArrayList<CodeTemplate>(map.values());
Collections.sort(byText, CodeTemplateComparator.BY_PARAMETRIZED_TEXT_IGNORE_CASE);
Collections.sort(templatesWithSelection, CodeTemplateComparator.BY_PARAMETRIZED_TEXT_IGNORE_CASE);
boolean fire = false;
synchronized(listenerList) {
fire = abbrev2template == null;
abbrev2template = Collections.unmodifiableMap(map);
sortedTemplatesByAbbrev = Collections.unmodifiableList(byAbbrev);
sortedTemplatesByParametrizedText = Collections.unmodifiableList(byText);
selectionTemplates = Collections.unmodifiableList(templatesWithSelection);
expansionKey = keyStroke;
expansionKeyText = getExpandKeyStrokeText(keyStroke);
loaded = true;
listenerList.notifyAll();
}
if (fire) {
fireStateChanged(new ChangeEvent(manager));
}
}
public KeyStroke getExpansionKey() {
return expansionKey;
}
public String getExpandKeyStrokeText() {
return expansionKeyText;
}
private static String getExpandKeyStrokeText(KeyStroke keyStroke) {
String expandKeyStrokeText;
if (keyStroke.equals(KeyStroke.getKeyStroke(' '))) { //NOI18N
expandKeyStrokeText = "SPACE"; // NOI18N
} else if (keyStroke.equals(KeyStroke.getKeyStroke(new Character(' '), InputEvent.SHIFT_MASK))) { //NOI18N
expandKeyStrokeText = "Shift-SPACE"; // NOI18N
} else if (keyStroke.equals(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0))) {
expandKeyStrokeText = "TAB"; // NOI18N
} else if (keyStroke.equals(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0))) {
expandKeyStrokeText = "ENTER"; // NOI18N
} else {
expandKeyStrokeText = keyStroke.toString();
}
return expandKeyStrokeText;
}
private static KeyStroke patchExpansionKey(KeyStroke eks) {
// Patch the keyPressed => keyTyped to prevent insertion of expand chars into editor
if (eks.equals(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0))) {
eks = KeyStroke.getKeyStroke(' ');
} else if (eks.equals(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, InputEvent.SHIFT_MASK))) {
eks = KeyStroke.getKeyStroke(new Character(' '), InputEvent.SHIFT_MASK);
} else if (eks.equals(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0))) {
} else if (eks.equals(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0))) {
}
return eks;
}
public void resultChanged(LookupEvent ev) {
rebuildCodeTemplates();
}
private static MimePath getFullMimePath(Document document, int offset) {
String langPath = null;
if (document instanceof AbstractDocument) {
AbstractDocument adoc = (AbstractDocument)document;
adoc.readLock();
try {
List<TokenSequence<?>> list = TokenHierarchy.get(document).embeddedTokenSequences(offset, true);
if (list.size() > 1) {
langPath = list.get(list.size() - 1).languagePath().mimePath();
}
} finally {
adoc.readUnlock();
}
}
if (langPath == null) {
langPath = NbEditorUtilities.getMimeType(document);
}
if (langPath != null) {
return MimePath.parse(langPath);
} else {
return null;
}
}
}