/*
* SnippetHelper.java
*
* Copyright (C) 2009-12 by RStudio, Inc.
*
* Unless you have received this program directly from RStudio pursuant
* to the terms of a commercial license agreement with RStudio, then
* this program is licensed to you under the terms of version 3 of the
* GNU Affero General Public License. This program is distributed WITHOUT
* ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
* MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
* AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
*
*/
package org.rstudio.studio.client.workbench.snippets;
import com.google.gwt.core.client.JavaScriptException;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.core.client.JsArrayString;
import com.google.inject.Inject;
import org.rstudio.core.client.Debug;
import org.rstudio.core.client.HandlerRegistrations;
import org.rstudio.core.client.JsArrayUtil;
import org.rstudio.core.client.StringUtil;
import org.rstudio.core.client.regex.Pattern;
import org.rstudio.studio.client.RStudioGinjector;
import org.rstudio.studio.client.common.FilePathUtils;
import org.rstudio.studio.client.server.ServerError;
import org.rstudio.studio.client.server.ServerRequestCallback;
import org.rstudio.studio.client.workbench.snippets.model.Snippet;
import org.rstudio.studio.client.workbench.snippets.model.SnippetData;
import org.rstudio.studio.client.workbench.snippets.model.SnippetsChangedEvent;
import org.rstudio.studio.client.workbench.views.source.editors.text.AceEditor;
import org.rstudio.studio.client.workbench.views.source.editors.text.ace.AceEditorNative;
import org.rstudio.studio.client.workbench.views.source.editors.text.ace.Position;
import org.rstudio.studio.client.workbench.views.source.editors.text.events.EditorLoadedEvent;
import org.rstudio.studio.client.workbench.views.source.editors.text.events.EditorLoadedHandler;
import java.util.ArrayList;
public class SnippetHelper
{
static class SnippetManager extends JavaScriptObject
{
protected SnippetManager() {}
}
public SnippetHelper(AceEditor editor)
{
this(editor, null);
}
public SnippetHelper(AceEditor editor, String path)
{
editor_ = editor;
native_ = editor.getWidget().getEditor();
manager_ = getSnippetManager();
path_ = path;
handlers_ = new HandlerRegistrations();
RStudioGinjector.INSTANCE.injectMembers(this);
handlers_.add(editor_.getWidget().addEditorLoadedHandler(new EditorLoadedHandler()
{
@Override
public void onEditorLoaded(EditorLoadedEvent event)
{
if (editor_.getFileType() != null)
ensureSnippetsLoaded();
}
}));
}
public void detach()
{
handlers_.removeHandler();
}
@Inject
public void initialize(SnippetServerOperations server)
{
server_ = server;
}
private static final native SnippetManager getSnippetManager() /*-{
return $wnd.require("ace/snippets").snippetManager;
}-*/;
public ArrayList<String> getAvailableSnippets()
{
return JsArrayUtil.fromJsArrayString(
getAvailableSnippetsImpl(manager_, getActiveMode()));
}
public final void ensureSnippetsLoaded()
{
ensureSnippetsLoadedImpl(
getActiveMode(), manager_);
}
// Parse a snippet file and apply the parsed snippets for
// mode 'mode'. Returns an associated exception on failure,
// or 'null' on success.
public static final native JavaScriptException loadSnippetsForMode(
String mode,
String snippetText,
SnippetManager manager)
/*-{
// Parse snippets passed through
var snippets = null;
try {
snippets = manager.parseSnippetFile(snippetText)
} catch (e) {
return e;
}
// Clear old snippets associated with this mode
delete manager.snippetMap[mode];
delete manager.snippetNameMap[mode];
// Overwrite the old snippets stored. This amounts to
// either overwriting the old RStudio snippets or the
// Ace snippets themselves (if no such RStudio snippets
// exist)
var old = $wnd.require("rstudio/snippets/" + mode);
if (old == null)
old = $wnd.require("ace/snippets/" + mode);
if (old != null) {
old.$snippetText = old.snippetText;
old.snippetText = snippetText;
}
// Apply new snippets
manager.register(snippets, mode);
return null;
}-*/;
public static final JavaScriptException loadSnippetsForMode(
String mode,
String snippetText)
{
return loadSnippetsForMode(
mode,
snippetText,
getSnippetManager());
}
private static final native void ensureSnippetsLoadedImpl(
String mode,
SnippetManager manager) /*-{
var snippetsForMode = manager.snippetNameMap[mode];
if (!snippetsForMode) {
// Try loading our own, local snippets. Loading those snippets will
// automatically register the snippets as necessary.
var m = null;
m = $wnd.require("rstudio/snippets/" + mode);
if (m != null)
return;
// Try loading internal Ace snippets. We need to pull the snippet
// content out of the appropriate require, then parse and load those
// snippets.
var id = "ace/snippets/" + mode;
var m = $wnd.require(id);
if (!m) {
console.log("Failed load Ace snippets for mode '" + mode + "'");
return;
}
if (!manager.files)
manager.files = {};
manager.files[id] = m;
if (!m.snippets && m.snippetText)
m.snippets = manager.parseSnippetFile(m.snippetText);
manager.register(m.snippets || [], m.scope);
}
}-*/;
private void selectToken(String token)
{
int offset = token.length();
if (StringUtil.isComplementOf(
token.substring(offset - 1),
String.valueOf(editor_.getCharacterAtCursor())))
{
editor_.moveCursorRight();
offset++;
}
editor_.expandSelectionLeft(offset);
}
public void applySnippet(final String token,
final String snippetName)
{
// Set the selection based on what we want to replace. For auto-paired
// insertions, e.g. `[|]`, we want to replace both characters; typically
// we only want to replace the token.
String snippetContent = transformMacros(
getSnippetContents(snippetName),
token,
snippetName);
// For snippets that contain code we want to execute in R, we pass the
// snippet down to the server and then apply the response.
if (containsExecutableRCode(snippetContent))
{
server_.transformSnippet(snippetContent, new ServerRequestCallback<String>()
{
@Override
public void onError(ServerError error)
{
Debug.logError(error);
}
@Override
public void onResponseReceived(String transformed)
{
selectToken(token);
applySnippetImpl(transformed, manager_, editor_.getWidget().getEditor());
}
});
}
else
{
selectToken(token);
applySnippetImpl(snippetContent, manager_, editor_.getWidget().getEditor());
}
}
private boolean containsExecutableRCode(String snippetContent)
{
return RE_R_CODE.test(snippetContent);
}
private String replaceFilename(String snippet)
{
String fileName = FilePathUtils.fileNameSansExtension(path_);
return snippet.replaceAll("`Filename.*`", fileName);
}
private String replaceHeaderGuard(String snippet)
{
// Munge the path a bit
String path = path_;
if (path.startsWith("~/"))
path = path.substring(2);
int instIncludeIdx = path.indexOf("/inst/include/");
if (instIncludeIdx != -1)
path = path.substring(instIncludeIdx + 15);
int srcIdx = path.indexOf("/src/");
if (srcIdx != -1)
path = path.substring(srcIdx + 6);
path = path.replaceAll("[./]", "_");
path = path.toUpperCase();
return snippet.replaceAll("`HeaderGuardFileName`", path);
}
private String transformMacros(
String snippet,
String token,
String snippetName)
{
if (path_ != null)
{
snippet = replaceFilename(snippet);
snippet = replaceHeaderGuard(snippet);
}
return snippet.replaceAll("\\$\\$", token.substring(snippetName.length()));
}
public final native void applySnippetImpl(
String snippetContent,
SnippetManager manager,
AceEditorNative editor) /*-{
manager.insertSnippet(editor, snippetContent);
}-*/;
private static final native JsArrayString getAvailableSnippetsImpl(
SnippetManager manager,
String mode) /*-{
var snippetsForMode = manager.snippetNameMap[mode];
if (snippetsForMode)
return Object.keys(snippetsForMode);
return [];
}-*/;
public Snippet getSnippet(String name)
{
return getSnippet(name, getActiveMode());
}
public Snippet getSnippet(String name, String mode)
{
return getSnippetImpl(manager_, mode, name);
}
private static final native Snippet getSnippetImpl(
SnippetManager manager,
String mode,
String name) /*-{
var snippetsForMode = manager.snippetNameMap[mode];
if (snippetsForMode)
return snippetsForMode[name];
else
return null;
}-*/;
// NOTE: this function assumes you've already called ensureSnippetsLoaded
// (this is a safe assumption because in order to enumerate snippet names
// you need to call the ensure* functions)
public String getSnippetContents(String snippetName)
{
return getSnippetImpl(manager_, getActiveMode(), snippetName).getContent();
}
private static final native String getActiveModeImpl(AceEditorNative editor,
Position position,
String major)
/*-{
var Utils = $wnd.require("mode/utils");
var state = Utils.primaryState(editor.getSession().getState(position.row));
return Utils.activeMode(state, major);
}-*/;
private String getMajorMode()
{
String modeName = editor_.getFileType().getEditorLanguage().getModeName();
if (modeName == "rmarkdown")
return "markdown";
else if (modeName == "sweave")
return "tex";
else if (modeName == "rhtml")
return "html";
else
return modeName;
}
private String getActiveMode()
{
String mode = getActiveModeImpl(
editor_.getWidget().getEditor(),
editor_.getCursorPosition(),
getMajorMode());
// TODO: Find a way to unify 'mode names' and 'state names' we use as
// prefixes for multi-mode documents
if (mode == "r-cpp" || mode == "c" || mode == "cpp")
mode = "c_cpp";
return mode.toLowerCase();
}
public static void onSnippetsChanged(SnippetsChangedEvent event)
{
SnippetManager manager = getSnippetManager();
JsArray<SnippetData> data = event.getData();
for (int i = 0; i < data.length(); i++)
{
SnippetData snippetData = data.get(i);
loadSnippetsForMode(
snippetData.getMode(),
snippetData.getContents(),
manager);
}
}
public boolean onInsertSnippet()
{
return attemptSnippetInsertion(true);
}
public boolean attemptSnippetInsertion(boolean allowPrefixMatch)
{
if (!editor_.getSelection().isEmpty())
return false;
String token = StringUtil.getToken(
editor_.getCurrentLine(),
editor_.getCursorPosition().getColumn(),
"[^ \\s\\n\\t\\r\\v]",
false,
false);
ArrayList<String> snippets = getAvailableSnippets();
if (snippets.contains(token))
{
applySnippet(token, token);
return true;
}
if (allowPrefixMatch)
{
for (int i = 0; i < snippets.size(); i++)
{
String snippetName = snippets.get(i);
if (token.startsWith(snippetName))
{
applySnippet(token, snippetName);
return true;
}
}
// Try 'special' snippets (those that start with punctuation characters)
String line = editor_.getCurrentLine().trim();
if (!Character.isLetterOrDigit(line.charAt(0)))
{
for (int i = 0; i < snippets.size(); i++)
{
String snippetName = snippets.get(i);
if (line.startsWith(snippetName))
{
applySnippet(line, snippetName);
return true;
}
}
}
}
return false;
}
private final AceEditor editor_;
private final AceEditorNative native_;
private final SnippetManager manager_;
private final String path_;
private final HandlerRegistrations handlers_;
private static boolean customCppSnippetsLoaded_;
private static boolean customRSnippetsLoaded_;
private static final Pattern RE_R_CODE =
Pattern.create("`[Rr]\\s+[^`]+`", "");
// Injected ----
private SnippetServerOperations server_;
}