feat: new editor: jsdroid editor

This commit is contained in:
hyb1996
2018-02-21 21:32:07 +08:00
parent 4b4c823db5
commit ea5f80b437
186 changed files with 6283 additions and 31683 deletions

View File

@@ -0,0 +1,79 @@
package com.stardust.scriptdroid.model.editor;
import com.google.gson.annotations.SerializedName;
public class EditorColors {
@SerializedName("editor.background")
private String mEditorBackground;
@SerializedName("editor.foreground")
private String mEditorForeground;
@SerializedName("editor.inactiveSelectionBackground")
private String mEditorInactiveSelectionBackground;
@SerializedName("editorIndentGuide.background")
private String mEditorIndentGuideBackground;
@SerializedName("editor.selectionHighlightBackground")
private String mEditorSelectionHighlightBackground;
@SerializedName("imeBar.background")
private String mImeBackgroundColor;
@SerializedName("imeBar.foreground")
private String mImeForegroundColor;
public String getEditorBackground() {
return mEditorBackground;
}
public void setEditorBackground(String editorBackground) {
mEditorBackground = editorBackground;
}
public String getEditorForeground() {
return mEditorForeground;
}
public void setEditorForeground(String editorForeground) {
mEditorForeground = editorForeground;
}
public String getEditorInactiveSelectionBackground() {
return mEditorInactiveSelectionBackground;
}
public void setEditorInactiveSelectionBackground(String editorInactiveSelectionBackground) {
mEditorInactiveSelectionBackground = editorInactiveSelectionBackground;
}
public String getEditorIndentGuideBackground() {
return mEditorIndentGuideBackground;
}
public void setEditorIndentGuideBackground(String editorIndentGuideBackground) {
mEditorIndentGuideBackground = editorIndentGuideBackground;
}
public String getEditorSelectionHighlightBackground() {
return mEditorSelectionHighlightBackground;
}
public void setEditorSelectionHighlightBackground(String editorSelectionHighlightBackground) {
mEditorSelectionHighlightBackground = editorSelectionHighlightBackground;
}
public String getImeBackgroundColor() {
return mImeBackgroundColor;
}
public void setImeBackgroundColor(String imeBackgroundColor) {
mImeBackgroundColor = imeBackgroundColor;
}
public String getImeForegroundColor() {
return mImeForegroundColor;
}
public void setImeForegroundColor(String imeForegroundColor) {
mImeForegroundColor = imeForegroundColor;
}
}

View File

@@ -0,0 +1,78 @@
package com.stardust.scriptdroid.model.editor;
import android.graphics.Color;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.annotations.SerializedName;
import java.io.Reader;
import java.util.Collections;
import java.util.List;
import static org.mozilla.javascript.Token.*;
public class EditorTheme {
private static Gson sGson;
static {
sGson = new GsonBuilder()
.registerTypeAdapter(TokenColor.class, new TokenColorDeserializer())
.create();
}
@SerializedName("colors")
private EditorColors mEditorColors;
@SerializedName("name")
private String mName;
@SerializedName("$schema")
private String mSchema;
@SerializedName("tokenColors")
private List<TokenColor> mTokenColors = Collections.emptyList();
public static EditorTheme fromJson(String json) {
return sGson.fromJson(json, EditorTheme.class);
}
public static EditorTheme fromJson(Reader json) {
return sGson.fromJson(json, EditorTheme.class);
}
public EditorColors getEditorColors() {
return mEditorColors;
}
public void setEditorColors(EditorColors editorColors) {
mEditorColors = editorColors;
}
public String getName() {
return mName;
}
public void setName(String name) {
mName = name;
}
public String getSchema() {
return mSchema;
}
public void setSchema(String schema) {
mSchema = schema;
}
public List<TokenColor> getTokenColors() {
return mTokenColors;
}
public void setTokenColors(List<TokenColor> tokenColors) {
mTokenColors = tokenColors;
}
}

View File

@@ -0,0 +1,49 @@
package com.stardust.scriptdroid.model.editor;
import com.google.gson.annotations.SerializedName;
import java.util.Collections;
import java.util.List;
/**
* Created by Stardust on 2018/2/21.
*/
public class TokenColor {
@SerializedName("name")
private String mName;
@SerializedName("scope")
private List<String> mScope = Collections.emptyList();
@SerializedName("settings")
private TokenColorSettings mSettings;
public TokenColor() {
}
public String getName() {
return mName;
}
public void setName(String name) {
mName = name;
}
public List<String> getScope() {
return mScope;
}
public void setScope(List<String> scope) {
mScope = scope;
}
public TokenColorSettings getSettings() {
return mSettings;
}
public void setSettings(TokenColorSettings settings) {
mSettings = settings;
}
}

View File

@@ -0,0 +1,51 @@
package com.stardust.scriptdroid.model.editor;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import org.mozilla.javascript.Token;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Created by Stardust on 2018/2/21.
*/
public class TokenColorDeserializer implements JsonDeserializer<TokenColor> {
@Override
public TokenColor deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
JsonObject object = json.getAsJsonObject();
TokenColor tokenColor = new TokenColor();
if (object.has("name")) {
tokenColor.setName(object.get("name").getAsString());
}
if (object.has("settings")) {
tokenColor.setSettings(context.deserialize(object.get("settings"), TokenColorSettings.class));
}
if (object.has("scope")) {
tokenColor.setScope(deserializeAsList(object, "scope"));
}
return tokenColor;
}
private List<String> deserializeAsList(JsonObject object, String key) {
JsonElement scope = object.get(key);
if (scope.isJsonArray()) {
ArrayList<String> list = new ArrayList<>();
for (JsonElement e : scope.getAsJsonArray()) {
list.add(e.getAsString());
}
return list;
} else {
return Collections.singletonList(scope.getAsString());
}
}
}

View File

@@ -0,0 +1,35 @@
package com.stardust.scriptdroid.model.editor;
import com.google.gson.annotations.SerializedName;
/**
* Created by Stardust on 2018/2/21.
*/
public class TokenColorSettings {
@SerializedName("foreground")
private String mForeground;
@SerializedName("fontStyle")
private String mFontStyle;
public TokenColorSettings() {
}
public String getForeground() {
return mForeground;
}
public void setForeground(String foreground) {
mForeground = foreground;
}
public String getFontStyle() {
return mFontStyle;
}
public void setFontStyle(String fontStyle) {
mFontStyle = fontStyle;
}
}

View File

@@ -1,499 +0,0 @@
package com.stardust.scriptdroid.ui.edit;
import android.content.Context;
import android.graphics.Color;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.support.design.widget.Snackbar;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.ActionMode;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.webkit.ConsoleMessage;
import android.webkit.JavascriptInterface;
import android.webkit.JsPromptResult;
import android.webkit.JsResult;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.FrameLayout;
import android.widget.Toast;
import com.stardust.pio.PFiles;
import com.stardust.scriptdroid.R;
import com.stardust.theme.dialog.ThemeColorMaterialDialogBuilder;
import com.stardust.util.ClipboardUtil;
import org.jdeferred.Deferred;
import org.jdeferred.impl.DeferredObject;
import java.io.File;
import java.io.IOException;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import io.reactivex.subjects.PublishSubject;
import me.zhanghai.android.materialprogressbar.MaterialProgressBar;
/**
* Created by Stardust on 2017/9/27.
*/
public class CodeMirrorEditor extends FrameLayout {
private static final String LOG_TAG = "CodeMirrorEditor";
public interface Callback {
void onKeyUp(String line, int ch);
}
private static String[] sAvailableThemes;
private String mTheme = "neo";
private FrameLayout mProgressBarContainer;
private WebView mWebView;
private JavaScriptBridge mJavaScriptBridge = new JavaScriptBridge();
private Callback mCallback;
private Deferred<Void, Void, Void> mPageFinished = new DeferredObject<>();
public CodeMirrorEditor(Context context) {
super(context);
init();
}
public CodeMirrorEditor(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public CodeMirrorEditor(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public CodeMirrorEditor(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
public WebView getWebView() {
return mWebView;
}
public void setCallback(Callback callback) {
mCallback = callback;
}
public void beautifyCode() {
evalJavaScript("editor.setValue(js_beautify(editor.getValue(), {indent_size: 2}));");
}
public String[] getAvailableThemes() {
if (sAvailableThemes == null) {
try {
sAvailableThemes = getResources().getAssets().list("editor/codemirror/theme");
for (int i = 0; i < sAvailableThemes.length; i++) {
sAvailableThemes[i] = PFiles.getNameWithoutExtension(sAvailableThemes[i]);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return sAvailableThemes;
}
public String getTheme() {
return mTheme;
}
public void setTheme(final String theme) {
mPageFinished.promise().done(result -> {
evalJavaScript(String.format(Locale.getDefault(),
"var e = document.createElement('link');e.setAttribute('rel', 'stylesheet');" +
"e.setAttribute('type', 'text/css');e.setAttribute('href', '%s');" +
"document.getElementsByTagName('head')[0].appendChild(e);",
"codemirror/theme/" + theme + ".css"));
evalJavaScript(String.format(Locale.getDefault(), "editor.setOption('theme', '%s');", theme));
mTheme = theme;
});
}
private void init() {
setupWebView();
setupProgress();
mWebView.loadUrl("file:///android_asset/editor/index.html");
}
private void setupProgress() {
mProgressBarContainer = new FrameLayout(getContext());
mProgressBarContainer.setBackgroundColor(Color.WHITE);
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
addView(mProgressBarContainer, params);
MaterialProgressBar progressBar = new MaterialProgressBar(getContext());
int dp50 = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 50, getResources().getDisplayMetrics());
FrameLayout.LayoutParams p = new FrameLayout.LayoutParams(dp50, dp50);
p.gravity = Gravity.CENTER;
mProgressBarContainer.addView(progressBar, p);
}
private void setupWebView() {
mWebView = new WebView(getContext()) {
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
InputConnection connection = super.onCreateInputConnection(outAttrs);
return connection;// == null ? null : new MyInputConnection(connection);
}
};
setupWebSettings();
mWebView.addJavascriptInterface(mJavaScriptBridge, "__bridge__");
addView(mWebView);
}
private void setupWebSettings() {
WebSettings settings = mWebView.getSettings();
settings.setBuiltInZoomControls(true);
//settings.setLoadWithOverviewMode(true);
settings.setJavaScriptEnabled(true);
settings.setJavaScriptCanOpenWindowsAutomatically(true);
settings.setDomStorageEnabled(true);
settings.setNeedInitialFocus(true);
settings.setDisplayZoomControls(false);
mWebView.setWebChromeClient(new MyWebChromeClient());
mWebView.setWebViewClient(new MyWebViewClient());
}
@Override
public ActionMode startActionModeForChild(View originalView, ActionMode.Callback callback) {
return super.startActionModeForChild(originalView, new EditorActionModeCallback(callback, this));
}
@Override
public ActionMode startActionModeForChild(View originalView, ActionMode.Callback callback, int type) {
return super.startActionModeForChild(originalView, new EditorActionModeCallback(callback, this), type);
}
public void setReadOnly(boolean readOnly) {
evalJavaScript(String.format("editor.setOption('readOnly', %b);", readOnly));
}
public void setProgress(boolean onProgress) {
mProgressBarContainer.setVisibility(onProgress ? VISIBLE : GONE);
}
public void setText(final String text) {
int id = SharedVariables.put(text);
mPageFinished.promise().done(result -> evalJavaScript(String.format(Locale.getDefault(),
"editor.setValue(__bridge__.getStringFromAndroid(%d));", id)));
}
public void insert(String text) {
int id = SharedVariables.put(text);
mPageFinished.promise().done(result -> evalJavaScript(String.format(Locale.getDefault(),
"editor.replaceSelection(__bridge__.getStringFromAndroid(%d));", id)));
}
public Observable<String> getText() {
return evalString("editor.getValue()");
}
public Observable<String> getLine() {
return evalString("editor.getLine(editor.getCursor().line)");
}
public void deleteLine() {
evalJavaScript("editor.replaceRange('', {line: editor.getCursor().line, ch: 0}," +
" {line: editor.getCursor().line + 1, ch: 0});");
}
public void moveCursor(int dLine, int dCh) {
evalJavaScript(String.format(Locale.getDefault(),
"editor.setCursor({line: editor.getCursor().line + (%d), ch: editor.getCursor().ch + (%d)});",
dLine, dCh));
}
public void copyLine() {
getLine()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(s -> {
ClipboardUtil.setClip(getContext(), s);
Snackbar.make(mWebView, R.string.text_already_copy_to_clip, Snackbar.LENGTH_SHORT).show();
});
}
public void cut() {
getSelection().observeOn(AndroidSchedulers.mainThread())
.subscribe(s -> {
ClipboardUtil.setClip(getContext(), s);
evalJavaScript("editor.replaceSelection('');");
});
}
public void copy() {
getSelection().observeOn(AndroidSchedulers.mainThread())
.subscribe(s -> ClipboardUtil.setClip(getContext(), s));
}
public void paste() {
CharSequence clip = ClipboardUtil.getClip(getContext());
if (clip == null) {
return;
}
evalJavaScript(String.format("editor.replaceSelection('%s');", clip.toString().replace("'", "\\'")));
}
public void selectAll() {
evalJavaScript("editor.execCommand('selectAll');");
}
public Observable<Integer> getLineCount() {
PublishSubject<Integer> publishSubject = PublishSubject.create();
int id = SharedVariables.put(publishSubject);
evalJavaScript(String.format(Locale.getDefault(), "__bridge__.setIntFromJs(%d, editor.lineCount())", id));
return publishSubject;
}
public void undo() {
evalJavaScript("editor.undo();");
}
public void redo() {
evalJavaScript("editor.redo();");
}
public void jumpTo(int line, int col) {
evalJavaScript("editor.setCursor({line: " + line + ", ch: " + col + "});");
}
public void find(String keywords, boolean usingRegex) {
setQuery(keywords, usingRegex);
findNext();
}
private void setQuery(String keywords, boolean usingRegex) {
keywords = keywords.replace("'", "\'");
if (usingRegex) {
keywords = "/" + keywords + "/";
}
evalJavaScript("editor.execCommand('clearSearch'); editor.state.search.query = '" +
keywords + "';");
}
public void findNext() {
evalJavaScript("editor.execCommand('findNext');");
}
public void findPrev() {
evalJavaScript("editor.execCommand('findPrev');");
}
public void replaceAll(String keywords, String replacement, boolean usingRegex) {
setQuery(keywords, usingRegex);
replacement = replacement.replace("'", "\'");
evalJavaScript("editor.state.search.text = '" + replacement + "';");
evalJavaScript("editor.execCommand('replaceAll');");
}
public void jumpToDef() {
evalJavaScript("editor.tern.jumpToDef();");
}
public void jumpToLineStart() {
evalJavaScript("editor.execCommand('goLineStart');");
}
public void jumpToLineEnd() {
evalJavaScript("editor.execCommand('goLineEnd')");
}
public void jumpToStart() {
evalJavaScript("editor.execCommand('goDocStart');");
}
public void jumpToEnd() {
evalJavaScript("editor.execCommand('goDocEnd');");
}
public void showType() {
evalJavaScript("editor.tern.showType();");
}
public void rename() {
evalJavaScript("editor.tern.rename();");
}
public void selectName() {
evalJavaScript("editor.tern.selectName();");
}
public void replace(String keywords, String replacement, boolean usingRegex) {
setQuery(keywords, usingRegex);
replacement = replacement.replace("'", "\'");
evalJavaScript("editor.state.search.text = '" + replacement + "';");
findNext();
}
public Observable<String> getSelection() {
return evalString("editor.getSelection()");
}
private Observable<String> evalString(String expr) {
PublishSubject<String> publishSubject = PublishSubject.create();
int id = SharedVariables.put(publishSubject);
evalJavaScript(String.format(Locale.getDefault(), "__bridge__.setStringFromJs(%d, %s);", id, expr));
return publishSubject;
}
public void replaceSelection() {
evalJavaScript("editor.execCommand('replace');");
}
private void evalJavaScript(String script) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
mWebView.evaluateJavascript(script, null);
} else {
mWebView.loadUrl("javascript:" + script);
}
}
public void replace(String text, int fromLine, int fromCh, int toLine, int toCh) {
String js = "editor.replaceRange('%s', {line: %d, ch: %d}, {line: %d, ch: %d})";
evalJavaScript(String.format(Locale.getDefault(), js, text, fromLine, fromCh, toLine, toCh));
}
public class JavaScriptBridge {
@JavascriptInterface
public void setStringFromJs(int id, String text) {
PublishSubject<String> publishSubject = SharedVariables.remove(id);
if (publishSubject == null)
return;
publishSubject.onNext(text);
publishSubject.onComplete();
}
@JavascriptInterface
public void setIntFromJs(int id, int i) {
PublishSubject<Integer> publishSubject = SharedVariables.remove(id);
if (publishSubject == null)
return;
publishSubject.onNext(i);
publishSubject.onComplete();
}
@JavascriptInterface
public String getStringFromAndroid(int id) {
return SharedVariables.remove(id);
}
@JavascriptInterface
public void onKeyUp(String line, int cursor) {
if (mCallback == null) {
return;
}
mWebView.post(() -> mCallback.onKeyUp(line, cursor));
}
}
private class MyWebViewClient extends WebViewClient {
private final String INIT_SCRIPT = "editor.on('change', function(){__bridge__.onTextChange();});";
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
mPageFinished.resolve(null);
evalJavaScript(INIT_SCRIPT);
setProgress(false);
}
}
private class MyWebChromeClient extends WebChromeClient {
private final Pattern RENAME = Pattern.compile("New name for ([a-zA-Z]+)");
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
Matcher m = RENAME.matcher(message);
if (m.matches() && m.groupCount() >= 1) {
showRenamePrompt(m.group(1), defaultValue, result);
return true;
}
return super.onJsPrompt(view, url, message, defaultValue, result);
}
private void showRenamePrompt(String name, String defaultValue, final JsPromptResult result) {
new ThemeColorMaterialDialogBuilder(getContext())
.title(getResources().getString(R.string.text_rename) + name)
.input("", defaultValue, (dialog, input) -> result.confirm(input.toString()))
.show();
}
@Override
public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
new ThemeColorMaterialDialogBuilder(getContext())
.title(R.string.text_alert)
.content(message)
.onPositive((dialog, which) -> result.confirm())
.cancelListener(dialog -> result.cancel())
.positiveText(R.string.ok)
.show();
return true;
}
@Override
public void onProgressChanged(WebView view, int newProgress) {
setProgress(newProgress != 100);
}
@Override
public boolean onConsoleMessage(ConsoleMessage m) {
switch (m.messageLevel()) {
case LOG:
Log.i(LOG_TAG, String.format("%s (%s:%d)", m.message(), m.sourceId(), m.lineNumber()));
return true;
case TIP:
Log.v(LOG_TAG, String.format("%s (%s:%d)", m.message(), m.sourceId(), m.lineNumber()));
return true;
case DEBUG:
Log.d(LOG_TAG, String.format("%s (%s:%d)", m.message(), m.sourceId(), m.lineNumber()));
return true;
case WARNING:
Log.w(LOG_TAG, String.format("%s (%s:%d)", m.message(), m.sourceId(), m.lineNumber()));
return true;
case ERROR:
Log.e(LOG_TAG, String.format("%s (%s:%d)", m.message(), m.sourceId(), m.lineNumber()));
return true;
}
return false;
}
}
}

View File

@@ -1,61 +0,0 @@
package com.stardust.scriptdroid.ui.edit;
import android.app.Activity;
import android.content.Intent;
import android.os.Build;
import android.support.v7.view.menu.MenuBuilder;
import android.support.v7.view.menu.MenuItemImpl;
import android.util.Log;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuItem;
import com.stardust.scriptdroid.R;
/**
* Created by Stardust on 2017/11/2.
*/
public class EditorActionModeCallback implements ActionMode.Callback {
private static final int ID_SELECT_ALL = 34275489;
private ActionMode.Callback mCallback;
private CodeMirrorEditor mEditor;
public EditorActionModeCallback(ActionMode.Callback callback, CodeMirrorEditor editor) {
mCallback = callback;
mEditor = editor;
}
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
return mCallback.onCreateActionMode(mode, menu);
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return mCallback.onPrepareActionMode(mode, menu);
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch (item.getItemId()) {
case R.id.action_delete_line:
mEditor.deleteLine();
return true;
case R.id.action_copy_line:
mEditor.copyLine();
mode.finish();
return true;
}
return mCallback.onActionItemClicked(mode, item);
}
@Override
public void onDestroyActionMode(ActionMode mode) {
mCallback.onDestroyActionMode(mode);
}
}

View File

@@ -5,12 +5,11 @@ import android.support.design.widget.Snackbar;
import android.text.InputType;
import android.view.MenuItem;
import com.afollestad.materialdialogs.MaterialDialog;
import com.stardust.pio.PFiles;
import com.stardust.scriptdroid.R;
import com.stardust.scriptdroid.autojs.AutoJs;
import com.stardust.scriptdroid.ui.build.BuildActivity;
import com.stardust.scriptdroid.ui.build.BuildActivity_;
import com.stardust.scriptdroid.ui.edit.editor.CodeEditor;
import com.stardust.scriptdroid.ui.log.LogActivity_;
import com.stardust.theme.dialog.ThemeColorMaterialDialogBuilder;
import com.stardust.util.ClipboardUtil;
@@ -20,7 +19,6 @@ import java.util.Locale;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.annotations.NonNull;
import io.reactivex.functions.BiFunction;
import io.reactivex.functions.Consumer;
/**
@@ -31,12 +29,12 @@ public class EditorMenu {
private EditorView mEditorView;
private Context mContext;
private CodeMirrorEditor mEditor;
private CodeEditor mEditor;
public EditorMenu(EditorView editorView) {
mEditorView = editorView;
mContext = editorView.getContext();
mEditor = editorView.mEditor;
mEditor = editorView.getEditor();
}
public boolean onOptionsItemSelected(MenuItem item) {
@@ -69,9 +67,6 @@ public class EditorMenu {
case R.id.action_jump_to_line:
jumpToLine();
return true;
case R.id.action_jump_to_def:
mEditor.jumpToDef();
return true;
case R.id.action_jump_to_start:
mEditor.jumpToStart();
return true;
@@ -94,10 +89,10 @@ public class EditorMenu {
beautifyCode();
return true;
case R.id.action_rename:
mEditor.rename();
//mEditor.rename();
return true;
case R.id.action_select_variable:
mEditor.selectName();
//mEditor.selectName();
return true;
}
return false;
@@ -110,7 +105,7 @@ public class EditorMenu {
showConsole();
return true;
case R.id.action_show_type:
mEditor.showType();
// mEditor.showType();
return true;
case R.id.action_editor_theme:
mEditorView.selectEditorTheme();
@@ -212,14 +207,11 @@ public class EditorMenu {
private void findOrReplace() {
mEditor.getSelection()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<String>() {
@Override
public void accept(@NonNull String s) throws Exception {
.subscribe(s ->
new FindOrReplaceDialogBuilder(mContext, mEditorView)
.setQueryIfNotEmpty(s)
.show();
}
});
.show()
);
}

View File

@@ -5,7 +5,6 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.preference.PreferenceManager;
import android.support.annotation.Nullable;
import android.support.design.widget.Snackbar;
import android.support.v4.widget.DrawerLayout;
@@ -17,7 +16,6 @@ import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.Toast;
import com.afollestad.materialdialogs.MaterialDialog;
import com.stardust.autojs.engine.JavaScriptEngine;
import com.stardust.autojs.execution.ScriptExecution;
import com.stardust.pio.PFiles;
@@ -31,10 +29,11 @@ import com.stardust.scriptdroid.model.script.Scripts;
import com.stardust.scriptdroid.ui.doc.ManualDialog;
import com.stardust.scriptdroid.model.autocomplete.CodeCompletions;
import com.stardust.scriptdroid.ui.edit.completion.CodeCompletionBar;
import com.stardust.scriptdroid.ui.edit.completion.InputMethodEnhancedBarColors;
import com.stardust.scriptdroid.model.autocomplete.Symbols;
import com.stardust.scriptdroid.ui.edit.editor.CodeEditor;
import com.stardust.scriptdroid.ui.edit.keyboard.FunctionsKeyboardHelper;
import com.stardust.scriptdroid.ui.edit.keyboard.FunctionsKeyboardView;
import com.stardust.scriptdroid.ui.edit.theme.Theme;
import com.stardust.scriptdroid.ui.log.LogActivity_;
import com.stardust.scriptdroid.ui.widget.EWebView;
import com.stardust.scriptdroid.ui.widget.ToolbarMenuItem;
@@ -47,7 +46,6 @@ import org.androidannotations.annotations.EViewGroup;
import org.androidannotations.annotations.ViewById;
import java.io.File;
import java.util.Arrays;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
@@ -69,7 +67,7 @@ public class EditorView extends FrameLayout implements CodeCompletionBar.OnHintC
public static final String EXTRA_RUN_ENABLED = "Love you with my life...really...17.9.28";
@ViewById(R.id.editor)
CodeMirrorEditor mEditor;
CodeEditor mEditor;
@ViewById(R.id.code_completion_bar)
CodeCompletionBar mCodeCompletionBar;
@@ -104,9 +102,8 @@ public class EditorView extends FrameLayout implements CodeCompletionBar.OnHintC
private File mFile;
private boolean mReadOnly = false;
private ScriptExecution mScriptExecution;
private boolean mTextChanged = false;
private boolean mInitialText = false;
private AutoCompletion mAutoCompletion;
private Theme mEditorTheme;
private FunctionsKeyboardHelper mFunctionsKeyboardHelper;
private BroadcastReceiver mOnRunFinishedReceiver = new BroadcastReceiver() {
@Override
@@ -209,9 +206,7 @@ public class EditorView extends FrameLayout implements CodeCompletionBar.OnHintC
}
private void setInitialText(String text) {
mInitialText = true;
mTextChanged = false;
mEditor.setText(text);
mEditor.setInitialText(text);
}
@@ -222,6 +217,7 @@ public class EditorView extends FrameLayout implements CodeCompletionBar.OnHintC
@AfterViews
void init() {
setTheme(Theme.getDefault(getContext()));
setUpEditor();
setUpInputMethodEnhancedBar();
setUpFunctionsKeyboard();
@@ -236,7 +232,7 @@ public class EditorView extends FrameLayout implements CodeCompletionBar.OnHintC
.setContent(mEditor)
.setFunctionsTrigger(mShowFunctionsButton)
.setFunctionsView(mFunctionsKeyboard)
.setEditView(mEditor.getWebView())
.setEditView(mEditor.getCodeEditText())
.build();
mFunctionsKeyboard.setClickCallback(this);
}
@@ -251,16 +247,9 @@ public class EditorView extends FrameLayout implements CodeCompletionBar.OnHintC
private void setUpEditor() {
setTheme(PreferenceManager.getDefaultSharedPreferences(getContext())
.getString(KEY_EDITOR_THEME, mEditor.getTheme()));
mEditor.setCallback((line, cursor) -> {
if (mInitialText) {
mInitialText = false;
} else {
mTextChanged = true;
setMenuItemStatus(R.id.save, true);
autoComplete(line, cursor);
}
mEditor.setCursorChangeCallback((line, cursor) -> {
setMenuItemStatus(R.id.save, mEditor.isTextChanged());
autoComplete(line, cursor);
});
@@ -270,10 +259,11 @@ public class EditorView extends FrameLayout implements CodeCompletionBar.OnHintC
mAutoCompletion.onCursorChange(line, cursor);
}
public void setTheme(String theme) {
public void setTheme(Theme theme) {
mEditorTheme = theme;
mEditor.setTheme(theme);
mInputMethodEnhanceBar.setBackgroundColor(InputMethodEnhancedBarColors.getBackgroundColor(theme));
int textColor = InputMethodEnhancedBarColors.getTextColor(theme);
mInputMethodEnhanceBar.setBackgroundColor(theme.getImeBarBackgroundColor());
int textColor = theme.getImeBarForegroundColor();
mCodeCompletionBar.setTextColor(textColor);
mSymbolBar.setTextColor(textColor);
mShowFunctionsButton.setColorFilter(textColor);
@@ -321,7 +311,7 @@ public class EditorView extends FrameLayout implements CodeCompletionBar.OnHintC
.doOnNext(s -> PFiles.write(mFile, s))
.observeOn(AndroidSchedulers.mainThread())
.doOnNext(s -> {
mTextChanged = false;
mEditor.markTextAsSaved();
setMenuItemStatus(R.id.save, false);
});
}
@@ -362,7 +352,7 @@ public class EditorView extends FrameLayout implements CodeCompletionBar.OnHintC
}
public boolean isTextChanged() {
return mTextChanged;
return mEditor.isTextChanged();
}
public void showConsole() {
@@ -381,22 +371,10 @@ public class EditorView extends FrameLayout implements CodeCompletionBar.OnHintC
}
public void selectEditorTheme() {
String[] themes = mEditor.getAvailableThemes();
int i = Arrays.asList(themes).indexOf(mEditor.getTheme());
new MaterialDialog.Builder(getContext())
.title(R.string.text_editor_theme)
.items((CharSequence[]) themes)
.itemsCallbackSingleChoice(i, (dialog, itemView, which, text) -> {
PreferenceManager.getDefaultSharedPreferences(getContext()).edit()
.putString(KEY_EDITOR_THEME, text.toString())
.apply();
setTheme(text.toString());
return true;
})
.show();
}
public CodeMirrorEditor getEditor() {
public CodeEditor getEditor() {
return mEditor;
}
@@ -466,7 +444,7 @@ public class EditorView extends FrameLayout implements CodeCompletionBar.OnHintC
mEditor.insert(m.getName() + "." + p);
}
if (!property.isVariable()) {
mEditor.moveCursor(0, -1);
mEditor.moveCursor(-1);
}
mFunctionsKeyboardHelper.hideFunctionsLayout(true);
}

View File

@@ -1,163 +0,0 @@
package com.stardust.scriptdroid.ui.edit;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.view.KeyEvent;
import android.view.inputmethod.CompletionInfo;
import android.view.inputmethod.CorrectionInfo;
import android.view.inputmethod.ExtractedText;
import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputContentInfo;
/**
* Created by Stardust on 2017/11/4.
*/
public class InputConnectProxy implements InputConnection {
private InputConnection mInputConnection;
public InputConnectProxy(InputConnection inputConnection) {
mInputConnection = inputConnection;
}
@Override
public CharSequence getTextBeforeCursor(int n, int flags) {
return mInputConnection.getTextBeforeCursor(n, flags);
}
@Override
public CharSequence getTextAfterCursor(int n, int flags) {
return mInputConnection.getTextAfterCursor(n, flags);
}
@Override
public CharSequence getSelectedText(int flags) {
return mInputConnection.getSelectedText(flags);
}
@Override
public int getCursorCapsMode(int reqModes) {
return mInputConnection.getCursorCapsMode(reqModes);
}
@Override
public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
return mInputConnection.getExtractedText(request, flags);
}
@Override
public boolean deleteSurroundingText(int beforeLength, int afterLength) {
return mInputConnection.deleteSurroundingText(beforeLength, afterLength);
}
@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) {
return mInputConnection.deleteSurroundingTextInCodePoints(beforeLength, afterLength);
}
@Override
public boolean setComposingText(CharSequence text, int newCursorPosition) {
return mInputConnection.setComposingText(text, newCursorPosition);
}
@Override
public boolean setComposingRegion(int start, int end) {
return mInputConnection.setComposingRegion(start, end);
}
@Override
public boolean finishComposingText() {
return mInputConnection.finishComposingText();
}
@Override
public boolean commitText(CharSequence text, int newCursorPosition) {
return mInputConnection.commitText(text, newCursorPosition);
}
@Override
public boolean commitCompletion(CompletionInfo text) {
return mInputConnection.commitCompletion(text);
}
@Override
public boolean commitCorrection(CorrectionInfo correctionInfo) {
return mInputConnection.commitCorrection(correctionInfo);
}
@Override
public boolean setSelection(int start, int end) {
return mInputConnection.setSelection(start, end);
}
@Override
public boolean performEditorAction(int editorAction) {
return mInputConnection.performEditorAction(editorAction);
}
@Override
public boolean performContextMenuAction(int id) {
return mInputConnection.performContextMenuAction(id);
}
@Override
public boolean beginBatchEdit() {
return mInputConnection.beginBatchEdit();
}
@Override
public boolean endBatchEdit() {
return mInputConnection.endBatchEdit();
}
@Override
public boolean sendKeyEvent(KeyEvent event) {
return mInputConnection.sendKeyEvent(event);
}
@Override
public boolean clearMetaKeyStates(int states) {
return mInputConnection.clearMetaKeyStates(states);
}
@Override
public boolean reportFullscreenMode(boolean enabled) {
return mInputConnection.reportFullscreenMode(enabled);
}
@Override
public boolean performPrivateCommand(String action, Bundle data) {
return mInputConnection.performPrivateCommand(action, data);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public boolean requestCursorUpdates(int cursorUpdateMode) {
return mInputConnection.requestCursorUpdates(cursorUpdateMode);
}
@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public Handler getHandler() {
return mInputConnection.getHandler();
}
@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public void closeConnection() {
mInputConnection.closeConnection();
}
@RequiresApi(api = Build.VERSION_CODES.N_MR1)
@Override
public boolean commitContent(@NonNull InputContentInfo inputContentInfo, int flags, @Nullable Bundle opts) {
return mInputConnection.commitContent(inputContentInfo, flags, opts);
}
}

View File

@@ -1,31 +0,0 @@
package com.stardust.scriptdroid.ui.edit;
import android.util.SparseArray;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Created by Stardust on 2018/2/4.
*/
public class SharedVariables {
private static SparseArray<Object> sSharedVariables = new SparseArray<>();
private static AtomicInteger sMaxId = new AtomicInteger();
public static int put(Object value) {
int id = sMaxId.getAndIncrement();
sSharedVariables.put(id, value);
return id;
}
@SuppressWarnings("unchecked")
public static <T> T remove(int id) {
Object o = sSharedVariables.get(id);
if (o != null)
sSharedVariables.remove(id);
return (T) o;
}
}

View File

@@ -1,77 +0,0 @@
package com.stardust.scriptdroid.ui.edit.completion;
import android.graphics.Color;
import com.stardust.util.MapEntries;
import java.util.Map;
/**
* Created by Stardust on 2017/9/28.
*/
public class InputMethodEnhancedBarColors {
private static final Map<String, Integer> sEditorThemeColors = new MapEntries<String, Integer>()
.entry("3024-day", 0xf7f7f7)
.entry("3024-night", 0x090300)
.entry("abcdef", 0x0f0f0f)
.entry("ambiance-mobile", 0xffffff)
.entry("ambiance", 0xffffff)
.entry("base16-dark", 0x151515)
.entry("base16-light", 0xf5f5f5)
.entry("bespin", 0x28211c)
.entry("dracula", 0x282a36)
.entry("duotone-dark", 0x2a2734)
.entry("duotone-light", 0xfaf8f5)
.entry("eclipse", 0xffffff)
.entry("icecoder", 0x1d1d1b)
.entry("material", 0x263238)
.entry("mbo", 0x2c2c2c)
.entry("mdn-like", 0xfefefe)
.entry("monokai", 0x272822)
.entry("neat", 0xffffff)
.entry("neo", 0xffffff)
.entry("night", 0x0a001f)
.entry("panda-syntax", 0x292a2b)
.entry("paraiso-dark", 0x2f1e2e)
.entry("paraiso-light", 0xe7e9db)
.entry("pastel-on-dark", 0x2c2827)
.entry("railscasts", 0x2b2b2b)
.entry("rubyblue", 0x112435)
.entry("seti", 0x151718)
.entry("solarized", 0xffffff)
.entry("the-matrix", 0x000000)
.entry("tomorrow-night-bright", 0x000000)
.entry("tomorrow-night-eighties", 0x000000)
.entry("ttcn", 0xffffff)
.entry("twilight", 0x141414)
.entry("xq-light", 0xffffff)
.entry("zenburn", 0x3f3f3f)
.map();
public static int getBackgroundColor(String theme) {
int themeColor = sEditorThemeColors.get(theme);
if (themeColor == 0xffffff) {
return 0x828389;
}
return themeColor | 0xdd000000;
}
public static int getTextColor(String theme) {
int c = sEditorThemeColors.get(theme);
if (isBrightColor(c)) {
return Color.BLACK;
} else {
return Color.WHITE;
}
}
public static boolean isBrightColor(int color) {
int[] rgb = {Color.red(color), Color.green(color), Color.blue(color)};
int brightness = (int) Math.sqrt(rgb[0] * rgb[0] * .241 + rgb[1]
* rgb[1] * .691 + rgb[2] * rgb[2] * .068);
return brightness >= 200;
}
}

View File

@@ -0,0 +1,212 @@
/**
* Copyright 2018 WHO<980008027@qq.com>
* <p>
* Licensed 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.
* <p>
* Modified by project: https://github.com/980008027/JsDroidEditor
*/
package com.stardust.scriptdroid.ui.edit.editor;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.support.v7.widget.AppCompatEditText;
import android.text.Editable;
import android.text.Layout;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import com.stardust.scriptdroid.ui.edit.theme.Theme;
/**
* Created by Administrator on 2018/2/11.
*/
public class CodeEditText extends AppCompatEditText {
private static final String LOG_TAG = "CodeEditText";
// 文字范围
protected HVScrollView mScrollView;
private CodeEditor.CursorChangeCallback mCallback;
private volatile JavaScriptHighlighter.HighlightTokens mHighlightTokens;
private Theme mTheme;
public CodeEditText(Context context) {
super(context);
init();
}
public CodeEditText(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
setGravity(Gravity.START);
// 设值背景透明
setBackgroundColor(Color.TRANSPARENT);
// 设置字体颜色
setTextColor(Color.TRANSPARENT);
// 设置字体
setTypeface(Typeface.MONOSPACE);
mTheme = Theme.getDefault(getContext());
}
public void setTheme(Theme theme) {
mTheme = theme;
invalidate();
}
@Override
protected void onDraw(Canvas canvas) {
if (mScrollView == null) {
mScrollView = (HVScrollView) getParent();
}
// 根据行号计算左边距padding
String max = Integer.toString(getLineCount());
float lineNumberSize = getPaint().measureText(max) + 20;
if (getPaddingLeft() != lineNumberSize) {
setPadding((int) lineNumberSize, 0, 0, 0);
invalidate();
}
super.onDraw(canvas);
// 画文字
canvas.save();
canvas.translate(0, getExtendedPaddingTop());
drawText(canvas);
canvas.restore();
}
// 绘制文本着色
private void drawText(Canvas canvas) {
JavaScriptHighlighter.HighlightTokens highlightTokens = mHighlightTokens;
Layout layout = getLayout();
long lineRange = getLineRangeForDraw(layout, canvas);
int firstLineForDraw = LayoutHelper.unpackRangeStartFromLong(lineRange);
int lastLineForDraw = LayoutHelper.unpackRangeEndFromLong(lineRange);
if (firstLineForDraw < 0) {
return;
}
int lineCount = getLineCount();
int paddingLeft = getPaddingLeft();
Paint paint = getPaint();
for (int line = firstLineForDraw; line <= lastLineForDraw && line < lineCount; line++) {
int lineBottom = layout.getLineTop(line + 1);
int lineBaseline = lineBottom - layout.getLineDescent(line);
drawLineNumber(canvas, paint, line, lineBaseline);
if (highlightTokens == null)
continue;
drawCode(canvas, paint, paddingLeft, line, layout, lineBaseline, highlightTokens);
}
}
private void drawCode(Canvas canvas, Paint paint, int paddingLeft, int line, Layout layout, int lineBaseline, JavaScriptHighlighter.HighlightTokens highlightTokens) {
int lineStart = layout.getLineStart(line);
if (lineStart >= mHighlightTokens.getText().length()) {
return;
}
int lineEnd = layout.getLineVisibleEnd(line);
int fontCount = 0;
int previousColor = mHighlightTokens.getCharColor(lineStart);
int previousColorPos = lineStart;
for (int i = lineStart; i < lineEnd; i++) {
fontCount++;
int color = mHighlightTokens.getCharColor(i);
if (previousColor != color) {
drawText(canvas, paint, paddingLeft, lineBaseline, lineStart, previousColorPos, previousColorPos + fontCount, previousColor);
previousColor = color;
previousColorPos = i;
fontCount = 1;
}
if (i == lineEnd - 1) {
drawText(canvas, paint, paddingLeft, lineBaseline, lineStart, previousColorPos, previousColorPos + fontCount, previousColor);
}
}
}
private void drawText(Canvas canvas, Paint paint, int paddingLeft, int lineBaseline, int lineStart, int start, int end, int color) {
if (start >= end) {
return;
}
paint.setColor(color);
float offsetX = paint.measureText(getText(), lineStart, start);
canvas.drawText(getText(), start, end, paddingLeft + offsetX, lineBaseline, paint);
}
private void drawLineNumber(Canvas canvas, Paint paint, int line, int lineBaseline) {
String lineNumberText = Integer.toString(line + 1);
paint.setColor(mTheme.getLineNumberColor());
canvas.drawText(lineNumberText, 0, lineNumberText.length(), 10,
lineBaseline, paint);
}
private long getLineRangeForDraw(Layout layout, Canvas canvas) {
canvas.save();
float clipTop = (mScrollView.getScrollY() == 0) ? 0
: getExtendedPaddingTop() + mScrollView.getScrollY()
- mScrollView.getPaddingTop();
canvas.clipRect(0, clipTop, getWidth(), mScrollView.getScrollY()
+ mScrollView.getHeight());
long lineRangeForDraw = LayoutHelper.getLineRangeForDraw(layout, canvas);
canvas.restore();
return lineRangeForDraw;
}
@Override
protected void onSelectionChanged(int selStart, int selEnd) {
super.onSelectionChanged(selStart, selEnd);
if (mCallback == null || selStart != selEnd) {
return;
}
String text = getText().toString();
if (text.isEmpty()) {
return;
}
int lineStart = text.lastIndexOf("\n", selStart) + 1;
if (lineStart < 0) {
lineStart = 0;
}
if (lineStart > text.length() - 1) {
lineStart = text.length() - 1;
}
int lineEnd = text.indexOf("\n", selStart);
if (lineEnd < 0) {
lineEnd = text.length();
}
if (lineEnd < lineStart || lineStart < 0 || lineEnd > text.length())
return;
String line = text.substring(lineStart, lineEnd);
int cursor = selStart - lineStart;
mCallback.onCursorChange(line, cursor);
}
public void setCursorChangeCallback(CodeEditor.CursorChangeCallback callback) {
mCallback = callback;
}
public void updateHighlightTokens(JavaScriptHighlighter.HighlightTokens highlightTokens) {
mHighlightTokens = highlightTokens;
Log.d(LOG_TAG, "tokens = " + highlightTokens);
invalidate();
}
}

View File

@@ -0,0 +1,277 @@
package com.stardust.scriptdroid.ui.edit.editor;
import android.content.Context;
import android.support.design.widget.Snackbar;
import android.util.AttributeSet;
import com.stardust.scriptdroid.R;
import com.stardust.scriptdroid.ui.edit.theme.Theme;
import com.stardust.util.ClipboardUtil;
import org.apache.commons.lang3.StringUtils;
import java.util.Collections;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import io.reactivex.Observable;
/**
* Copyright 2018 WHO<980008027@qq.com>
* <p>
* Licensed 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.
* <p>
* Modified by project: https://github.com/980008027/JsDroidEditor
*/
public class CodeEditor extends HVScrollView {
private CharSequence mReplacement = "";
private String mKeywords;
private int mFoundIndex = -1;
public Observable<Integer> getLineCount() {
return Observable.just(mCodeEditText.getLayout().getLineCount());
}
public void copyLine() {
int line = getLineOfChar(mCodeEditText.getSelectionStart());
if (line < 0 || line >= mCodeEditText.getLayout().getLineCount())
return;
CharSequence lineText = mCodeEditText.getText().subSequence(mCodeEditText.getLayout().getLineStart(line),
mCodeEditText.getLayout().getLineEnd(line));
ClipboardUtil.setClip(getContext(), lineText);
Snackbar.make(this, R.string.text_already_copy_to_clip, Snackbar.LENGTH_SHORT).show();
}
private int getLineOfChar(int i) {
int low = 0;
int high = mCodeEditText.getLineCount() - 1;
while (low < high) {
int mid = (low + high) >>> 1;
int midVal = mCodeEditText.getLayout().getLineEnd(mid);
if (midVal <= i)
low = mid + 1;
else if (midVal > i)
high = mid - 1;
}
return low;
}
public void deleteLine() {
int line = getLineOfChar(mCodeEditText.getSelectionStart());
if (line < 0 || line >= mCodeEditText.getLayout().getLineCount())
return;
mCodeEditText.getText().replace(mCodeEditText.getLayout().getLineStart(line),
mCodeEditText.getLayout().getLineEnd(line), "");
}
public void jumpToStart() {
mCodeEditText.setSelection(0);
}
public void jumpToEnd() {
mCodeEditText.setSelection(mCodeEditText.getText().length());
}
public void jumpToLineStart() {
int line = getLineOfChar(mCodeEditText.getSelectionStart());
if (line < 0 || line >= mCodeEditText.getLayout().getLineCount())
return;
mCodeEditText.setSelection(mCodeEditText.getLayout().getLineStart(line));
}
public void jumpToLineEnd() {
int line = getLineOfChar(mCodeEditText.getSelectionStart());
if (line < 0 || line >= mCodeEditText.getLayout().getLineCount())
return;
mCodeEditText.setSelection(mCodeEditText.getLayout().getLineEnd(line) - 1);
}
public interface CursorChangeCallback {
void onCursorChange(String line, int ch);
}
private CodeEditText mCodeEditText;
private PreformEdit mPreformEdit;
private JavaScriptHighlighter mJavaScriptHighlighter;
private Theme mTheme;
public CodeEditor(Context context) {
super(context);
init();
}
public CodeEditor(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
setFillViewport(true);
inflate(getContext(), R.layout.code_editor, this);
mCodeEditText = (CodeEditText) findViewById(R.id.code_edit_text);
mPreformEdit = new PreformEdit(mCodeEditText);
mJavaScriptHighlighter = new JavaScriptHighlighter(mTheme, mCodeEditText);
setTheme(Theme.getDefault(getContext()));
}
public void setTheme(Theme theme) {
mTheme = theme;
mJavaScriptHighlighter.setTheme(theme);
setBackgroundColor(mTheme.getBackgroundColor());
}
public boolean isTextChanged() {
return mPreformEdit.isTextChanged();
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
mCodeEditText.postInvalidate();
}
public CodeEditText getCodeEditText() {
return mCodeEditText;
}
public void setInitialText(String text) {
mCodeEditText.setText(text);
mPreformEdit.setDefaultText(text);
}
public void jumpTo(int line, int col) {
mCodeEditText.setSelection(mCodeEditText.getLayout().getLineStart(line) + col);
//int top = mCodeEditText.getLayout().getLineTop(line - 1);
//post(() -> scrollTo(0, top));
}
public void setReadOnly(boolean readOnly) {
setEnabled(!readOnly);
}
public void setProgress(boolean progress) {
}
public void setText(String text) {
mCodeEditText.setText(text);
}
public void setCursorChangeCallback(CursorChangeCallback callback) {
mCodeEditText.setCursorChangeCallback(callback);
}
public void undo() {
mPreformEdit.undo();
}
public void redo() {
mPreformEdit.redo();
}
public void findNext() {
Matcher matcher = Pattern.compile(mKeywords).matcher(mCodeEditText.getText());
if (!matcher.find(mFoundIndex + 1)) {
return;
}
mFoundIndex = matcher.start();
mCodeEditText.setSelection(mFoundIndex, mFoundIndex + mKeywords.length());
}
public void findPrev() {
// FIXME: 2018/2/21 lastIndexOf不支持正则
if (mFoundIndex <= 0) {
mFoundIndex = mCodeEditText.getText().length();
}
int index = mCodeEditText.getText().toString().lastIndexOf(mKeywords, mFoundIndex - 1);
if (index < 0) {
return;
}
mFoundIndex = index;
mCodeEditText.setSelection(index, index + mKeywords.length());
}
public void replaceSelection() {
mCodeEditText.getText().replace(mCodeEditText.getSelectionStart(), mCodeEditText.getSelectionEnd(), mReplacement);
}
public void beautifyCode() {
}
public void find(String keywords, boolean usingRegex) {
if (usingRegex) {
mKeywords = keywords;
} else {
mKeywords = Pattern.quote(keywords);
}
mFoundIndex = -1;
findNext();
}
public void replace(String keywords, String replacement, boolean usingRegex) {
mReplacement = replacement == null ? "" : replacement;
find(keywords, usingRegex);
}
public void replaceAll(String keywords, String replacement, boolean usingRegex) {
if (!usingRegex) {
keywords = Pattern.quote(keywords);
}
String text = mCodeEditText.getText().toString();
text = text.replaceAll(keywords, replacement);
setText(text);
}
public void insert(String insertText) {
int selection = Math.max(mCodeEditText.getSelectionStart(), 0);
mCodeEditText.getText().insert(selection, insertText);
}
public void moveCursor(int dCh) {
mCodeEditText.setSelection(mCodeEditText.getSelectionStart() + dCh);
}
public Observable<String> getText() {
return Observable.just(mCodeEditText.getText().toString());
}
public Observable<String> getSelection() {
int s = mCodeEditText.getSelectionStart();
int e = mCodeEditText.getSelectionEnd();
if (s == e) {
return Observable.just("");
}
return Observable.just(mCodeEditText.getText().toString().substring(s, e));
}
public void markTextAsSaved() {
mPreformEdit.clearHistory();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,110 @@
package com.stardust.scriptdroid.ui.edit.editor;
import android.text.Editable;
import android.util.Log;
import com.stardust.autojs.rhino.TokenStream;
import com.stardust.pio.UncheckedIOException;
import com.stardust.scriptdroid.ui.edit.theme.Theme;
import com.stardust.scriptdroid.ui.widget.SimpleTextWatcher;
import org.mozilla.javascript.Token;
import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
public class JavaScriptHighlighter implements SimpleTextWatcher.AfterTextChangedListener {
public static class HighlightTokens {
private int[] mColors;
private String mText;
public HighlightTokens(String text) {
mColors = new int[text.length()];
mText = text;
}
public int getCharColor(int i) {
return mColors[i];
}
public void addToken(int tokenStart, int tokenEnd, int color) {
for (int i = tokenStart; i < tokenEnd; i++) {
mColors[i] = color;
}
}
@Override
public String toString() {
return "HighlightTokens{" +
"mColors=" + Arrays.toString(mColors) +
'}';
}
public int getCharCount() {
return mColors.length;
}
public String getText() {
return mText;
}
}
private Theme mTheme;
private CodeEditText mCodeEditText;
private ExecutorService mExecutorService = Executors.newSingleThreadExecutor();
private AtomicInteger mRunningHighlighterId = new AtomicInteger();
public JavaScriptHighlighter(Theme theme, CodeEditText codeEditText) {
mTheme = theme;
mCodeEditText = codeEditText;
codeEditText.addTextChangedListener(new SimpleTextWatcher(this));
}
@Override
public void afterTextChanged(Editable s) {
updateTokens(s.toString());
}
public void setTheme(Theme theme) {
mTheme = theme;
}
private void updateTokens(String sourceString) {
final int id = mRunningHighlighterId.incrementAndGet();
mExecutorService.execute(() -> {
try {
updateTokens(sourceString, id);
} catch (IOException neverHappen) {
throw new UncheckedIOException(neverHappen);
}
});
}
private void updateTokens(String sourceString, int id) throws IOException {
TokenStream ts = new TokenStream(null, sourceString, 0);
HighlightTokens highlightTokens = new HighlightTokens(sourceString);
int token;
while ((token = ts.getToken()) != Token.EOF) {
if (mRunningHighlighterId.get() != id)
return;
int color = mTheme.getColorForToken(token);
highlightTokens.addToken(ts.getTokenBeg(), ts.getTokenEnd(), color);
}
mCodeEditText.updateHighlightTokens(highlightTokens);
Log.d("Highlighter", "code = " + sourceString);
Log.d("Highlighter", "tokens = " + highlightTokens);
}
}

View File

@@ -0,0 +1,54 @@
package com.stardust.scriptdroid.ui.edit.editor;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.text.Layout;
import android.text.TextUtils;
/**
* Created by Stardust on 2018/2/13.
*/
public class LayoutHelper {
private static final Rect sTempRect = new Rect();
public static long getLineRangeForDraw(Layout layout, Canvas canvas) {
int dtop, dbottom;
synchronized (sTempRect) {
if (!canvas.getClipBounds(sTempRect)) {
// Negative range end used as a special flag
return packRangeInLong(0, -1);
}
dtop = sTempRect.top;
dbottom = sTempRect.bottom;
}
final int top = Math.max(dtop, 0);
final int bottom = Math.min(layout.getLineTop(layout.getLineCount()), dbottom);
if (top >= bottom) return packRangeInLong(0, -1);
return packRangeInLong(layout.getLineForVertical(top), layout.getLineForVertical(bottom));
}
/**
* Pack 2 int values into a long, useful as a return value for a range
*/
public static long packRangeInLong(int start, int end) {
return (((long) start) << 32) | end;
}
/**
* Get the start value from a range packed in a long by {@link #packRangeInLong(int, int)}
*/
public static int unpackRangeStartFromLong(long range) {
return (int) (range >>> 32);
}
public static int unpackRangeEndFromLong(long range) {
return (int) (range & 0x00000000FFFFFFFFL);
}
}

View File

@@ -0,0 +1,267 @@
package com.stardust.scriptdroid.ui.edit.editor;
/*
* Copyright 2016. SHENQINCI(沈钦赐)<946736079@qq.com>
*
* Licensed 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.
*/
import android.support.annotation.NonNull;
import android.text.Editable;
import android.text.TextWatcher;
import android.widget.EditText;
import java.util.Stack;
/**
* 撤销和恢复撤销
* Created by 沈钦赐 on 16/6/23.
*/
public class PreformEdit {
//操作序号(一次编辑可能对应多个操作,如替换文字,就是删除+插入)
int index;
//撤销栈
Stack<Action> history = new Stack<>();
//恢复栈
Stack<Action> historyBack = new Stack<>();
private Editable editable;
private EditText editText;
//自动操作标志,防止重复回调,导致无限撤销
private boolean flag = false;
public PreformEdit(@NonNull EditText editText) {
CheckNull(editText, "EditText不能为空");
this.editable = editText.getText();
this.editText = editText;
editText.addTextChangedListener(new Watcher());
}
protected void onEditableChanged(Editable s) {
}
protected void onTextChanged(Editable s) {
}
/**
* 清理记录
* Clear history.
*/
public final void clearHistory() {
history.clear();
historyBack.clear();
}
public boolean canUndo() {
return !history.empty();
}
/**
* 撤销
* Undo.
*/
public final void undo() {
if (history.empty()) return;
//锁定操作
flag = true;
Action action = history.pop();
historyBack.push(action);
if (action.isAdd) {
//撤销添加
editable.delete(action.startCursor, action.startCursor + action.actionTarget.length());
editText.setSelection(action.startCursor, action.startCursor);
} else {
//插销删除
editable.insert(action.startCursor, action.actionTarget);
if (action.endCursor == action.startCursor) {
editText.setSelection(action.startCursor + action.actionTarget.length());
} else {
editText.setSelection(action.startCursor, action.endCursor);
}
}
//释放操作
flag = false;
//判断是否是下一个动作是否和本动作是同一个操作,直到不同为止
if (!history.empty() && history.peek().index == action.index) {
undo();
}
}
public boolean canRedo() {
return !historyBack.empty();
}
/**
* 恢复
* Redo.
*/
public final void redo() {
if (historyBack.empty()) return;
flag = true;
Action action = historyBack.pop();
history.push(action);
if (action.isAdd) {
//恢复添加
editable.insert(action.startCursor, action.actionTarget);
if (action.endCursor == action.startCursor) {
editText.setSelection(action.startCursor + action.actionTarget.length());
} else {
editText.setSelection(action.startCursor, action.endCursor);
}
} else {
//恢复删除
editable.delete(action.startCursor, action.startCursor + action.actionTarget.length());
editText.setSelection(action.startCursor, action.startCursor);
}
flag = false;
//判断是否是下一个动作是否和本动作是同一个操作
if (!historyBack.empty() && historyBack.peek().index == action.index)
redo();
}
/**
* 首次设置文本
* Set default text.
*/
public final void setDefaultText(CharSequence text) {
clearHistory();
flag = true;
editable.replace(0, editable.length(), text);
flag = false;
}
public boolean isTextChanged() {
return !history.empty();
}
private class Watcher implements TextWatcher {
/**
* Before text changed.
*
* @param s the s
* @param start the start 起始光标
* @param count the endCursor 选择数量
* @param after the after 替换增加的文字数
*/
@Override
public final void beforeTextChanged(CharSequence s, int start, int count, int after) {
if (flag) return;
int end = start + count;
if (end > start && end <= s.length()) {
CharSequence charSequence = s.subSequence(start, end);
//删除了文字
if (charSequence.length() > 0) {
Action action = new Action(charSequence, start, false);
if (count > 1) {
//如果一次超过一个字符,说名用户选择了,然后替换或者删除操作
action.setSelectCount(count);
} else if (count == 1 && count == after) {
//一个字符替换
action.setSelectCount(count);
}
//还有一种情况:选择一个字符,然后删除(暂时没有考虑这种情况)
history.push(action);
historyBack.clear();
action.setIndex(++index);
}
}
}
/**
* On text changed.
*
* @param s the s
* @param start the start 起始光标
* @param before the before 选择数量
* @param count the endCursor 添加的数量
*/
@Override
public final void onTextChanged(CharSequence s, int start, int before, int count) {
if (flag) return;
int end = start + count;
if (end > start) {
CharSequence charSequence = s.subSequence(start, end);
//添加文字
if (charSequence.length() > 0) {
Action action = new Action(charSequence, start, true);
history.push(action);
historyBack.clear();
if (before > 0) {
//文字替换(先删除再增加),删除和增加是同一个操作,所以不需要增加序号
action.setIndex(index);
} else {
action.setIndex(++index);
}
}
}
}
@Override
public final void afterTextChanged(Editable s) {
if (flag) return;
if (s != editable) {
editable = s;
onEditableChanged(s);
}
PreformEdit.this.onTextChanged(s);
}
}
private class Action {
/**
* 改变字符.
*/
CharSequence actionTarget;
/**
* 光标位置.
*/
int startCursor;
int endCursor;
/**
* 标志增加操作.
*/
boolean isAdd;
/**
* 操作序号.
*/
int index;
public Action(CharSequence actionTag, int startCursor, boolean add) {
this.actionTarget = actionTag;
this.startCursor = startCursor;
this.endCursor = startCursor;
this.isAdd = add;
}
public void setSelectCount(int count) {
this.endCursor = endCursor + count;
}
public void setIndex(int index) {
this.index = index;
}
}
private static void CheckNull(Object o, String message) {
if (o == null) throw new IllegalStateException(message);
}
}

View File

@@ -0,0 +1,114 @@
package com.stardust.scriptdroid.ui.edit.theme;
import android.graphics.Color;
import android.util.SparseArray;
import android.util.SparseIntArray;
import com.google.gson.Gson;
import com.stardust.scriptdroid.model.editor.EditorTheme;
import com.stardust.scriptdroid.model.editor.TokenColor;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Token;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.Map;
import static org.mozilla.javascript.Token.*;
/**
* Created by Stardust on 2018/2/16.
*/
public class Theme {
private int mBackgroundColor;
private int mForegroundColor;
private int mLineNumberColor;
private SparseIntArray mTokenColors = new SparseIntArray();
private int mImeBarBackgroundColor;
private int mImeBarForegroundColor;
public Theme(EditorTheme theme) {
mBackgroundColor = Color.parseColor(theme.getEditorColors().getEditorBackground());
mForegroundColor = Color.parseColor(theme.getEditorColors().getEditorForeground());
mLineNumberColor = Color.parseColor(theme.getEditorColors().getEditorIndentGuideBackground());
mImeBarBackgroundColor = Color.parseColor(theme.getEditorColors().getImeBackgroundColor());
mImeBarForegroundColor = Color.parseColor(theme.getEditorColors().getImeForegroundColor());
for (TokenColor tokenColor : theme.getTokenColors()) {
String foregroundStr = tokenColor.getSettings().getForeground();
if (foregroundStr == null)
continue;
int foreground = Color.parseColor(foregroundStr);
for (String scope : tokenColor.getScope()) {
setTokenColor(scope, foreground);
}
}
}
private void setTokenColor(String scope, int foreground) {
for (int token : TokenMapping.getTokensForScope(scope)) {
mTokenColors.put(token, foreground);
}
}
public int getColorForToken(int token) {
return mTokenColors.get(token, mForegroundColor);
}
public int getLineNumberColor() {
return mLineNumberColor;
}
public static Theme getDefault(android.content.Context context) {
return fromAssetsJson(context, "editor/theme/dark_plus.json");
}
public static Theme fromJson(String json) {
EditorTheme theme = EditorTheme.fromJson(json);
if (theme == null)
return null;
return new Theme(theme);
}
public static Theme fromJson(Reader reader) {
EditorTheme theme = EditorTheme.fromJson(reader);
if (theme == null)
return null;
return new Theme(theme);
}
public static Theme fromAssetsJson(android.content.Context context, String assetsPath) {
try {
return fromJson(new InputStreamReader(context.getAssets().open(assetsPath)));
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
public int getBackgroundColor() {
return mBackgroundColor;
}
public int getImeBarBackgroundColor() {
return mImeBarBackgroundColor;
}
public void setImeBarBackgroundColor(int imeBarBackgroundColor) {
mImeBarBackgroundColor = imeBarBackgroundColor;
}
public int getImeBarForegroundColor() {
return mImeBarForegroundColor;
}
public void setImeBarForegroundColor(int imeBarForegroundColor) {
mImeBarForegroundColor = imeBarForegroundColor;
}
}

View File

@@ -0,0 +1,68 @@
package com.stardust.scriptdroid.ui.edit.theme;
import org.mozilla.javascript.Token;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Created by Stardust on 2018/2/21.
*/
public class TokenMapping {
private static final List<Integer> KEYWORD = tokenNamesToTypes(Arrays.asList("return", "new", "delete", "typeof", "null", "this", "false", "true", "throw", "in", "instanceof", "yield", "try", "function", "if", "else", "switch", "case", "default", "while", "do", "for", "break", "continue", "var", "with", "catch", "finally", "void", "let", "const", "debugger"));
private static final List<Integer> KEYWORD_CONTROL = tokenNamesToTypes(Arrays.asList("if", "else", "switch", "case", "break", "continue", "goto", "return", "try", "catch", "throw", "finally"));
private static final List<Integer> KEYWORD_OPERATOR = Collections.emptyList();
public static List<Integer> getTokensForScope(String scope) {
switch (scope) {
case "keyword":
return KEYWORD;
case "keyword.operator":
return KEYWORD_OPERATOR;
case "keyword.control":
return KEYWORD_CONTROL;
default:
int token = tokenNameToType(scope);
if (Token.isValidToken(token)) {
return Collections.singletonList(token);
}
}
return Collections.emptyList();
}
public static int tokenNameToType(String name) {
switch (name) {
case "this.self":
return Token.THIS;
case "keyword.operator.quantifier.regexp":
return Token.REGEXP;
case "variable":
return Token.NAME;
case "constant.numeric":
return Token.NUMBER;
}
for (int token = Token.ERROR; token < Token.LAST_TOKEN; token++) {
if (token == Token.STRICT_SETNAME || token == Token.SETCONSTVAR)
continue;
if (Token.typeToName(token).equalsIgnoreCase(name)) {
return token;
}
}
return -2;
}
private static List<Integer> tokenNamesToTypes(List<String> tokenNames) {
List<Integer> types = new ArrayList<>(tokenNames.size());
for (String name : tokenNames) {
types.add(tokenNameToType(name));
}
return types;
}
}

View File

@@ -0,0 +1,66 @@
package com.stardust.scriptdroid.ui.widget;
import android.text.Editable;
import android.text.TextWatcher;
/**
* Created by Stardust on 2018/2/16.
*/
public class SimpleTextWatcher implements TextWatcher {
public interface AfterTextChangedListener {
void afterTextChanged(Editable s);
}
public interface BeforeTextChangedListener {
void beforeTextChanged(CharSequence s, int start, int count, int after);
}
public interface OnTextChangedListener {
void onTextChanged(CharSequence s, int start, int before, int count);
}
private BeforeTextChangedListener mBeforeTextChangedListener;
private OnTextChangedListener mOnTextChangedListener;
private AfterTextChangedListener mAfterTextChangedListener;
public SimpleTextWatcher(BeforeTextChangedListener beforeTextChangedListener, OnTextChangedListener onTextChangedListener, AfterTextChangedListener afterTextChangedListener) {
mBeforeTextChangedListener = beforeTextChangedListener;
mOnTextChangedListener = onTextChangedListener;
mAfterTextChangedListener = afterTextChangedListener;
}
public SimpleTextWatcher(AfterTextChangedListener afterTextChangedListener) {
mAfterTextChangedListener = afterTextChangedListener;
}
public SimpleTextWatcher(BeforeTextChangedListener beforeTextChangedListener) {
mBeforeTextChangedListener = beforeTextChangedListener;
}
public SimpleTextWatcher(OnTextChangedListener onTextChangedListener) {
mOnTextChangedListener = onTextChangedListener;
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
if (mBeforeTextChangedListener != null) {
mBeforeTextChangedListener.beforeTextChanged(s, start, count, after);
}
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (mOnTextChangedListener != null) {
mOnTextChangedListener.onTextChanged(s, start, before, count);
}
}
@Override
public void afterTextChanged(Editable s) {
if (mAfterTextChangedListener != null) {
mAfterTextChangedListener.afterTextChanged(s);
}
}
}