diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser index 8b8733f2..5994ddd6 100644 Binary files a/.idea/caches/build_file_checksums.ser and b/.idea/caches/build_file_checksums.ser differ diff --git a/app/build.gradle b/app/build.gradle index ea809434..1b8f1c78 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,8 +8,8 @@ android { applicationId "org.autojs.autojs" minSdkVersion 17 targetSdkVersion 23 - versionCode 406 - versionName "4.0.2 Alpha2" + versionCode 408 + versionName "4.0.2 Alpha3" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" multiDexEnabled true ndk { diff --git a/app/src/main/assets/editor/theme/dark_plus.json b/app/src/main/assets/editor/theme/dark_plus.json index 8de4718c..ce0aac21 100644 --- a/app/src/main/assets/editor/theme/dark_plus.json +++ b/app/src/main/assets/editor/theme/dark_plus.json @@ -5,9 +5,11 @@ "editor.background": "#1E1E1E", "editor.foreground": "#D4D4D4", "editorLineNumber.foreground": "#404040", + "editorBreakpoint.foreground": "#1976D2", "imeBar.background": "#dd1e1e1e", "imeBar.foreground": "#f1f1f1", - "editor.lineHighlightBackground": "#2e2e35" + "editor.lineHighlightBackground": "#2e2e35", + "editor.debuggingLineBackground": "#40c4ff" }, "tokenColors": [ { diff --git a/app/src/main/assets/editor/theme/light_plus.json b/app/src/main/assets/editor/theme/light_plus.json index e2f62835..e37395a2 100644 --- a/app/src/main/assets/editor/theme/light_plus.json +++ b/app/src/main/assets/editor/theme/light_plus.json @@ -499,6 +499,8 @@ "editor.background": "#F5F5F5", "editor.foreground": "#000000", "editor.lineHighlightBackground": "#E4F6D4", + "editor.debuggingLineBackground": "#40c4ff", + "editorBreakpoint.foreground": "#03A9F4", "focusBorder": "#A6B39B", "pickerGroup.foreground": "#A6B39B", "pickerGroup.border": "#749351", diff --git a/app/src/main/java/org/autojs/autojs/model/editor/EditorColors.java b/app/src/main/java/org/autojs/autojs/model/editor/EditorColors.java index 3579b4d5..57c5b04d 100644 --- a/app/src/main/java/org/autojs/autojs/model/editor/EditorColors.java +++ b/app/src/main/java/org/autojs/autojs/model/editor/EditorColors.java @@ -22,6 +22,10 @@ public class EditorColors { private String mImeForegroundColor; @SerializedName("editor.lineHighlightBackground") private String mLineHighlightBackground; + @SerializedName("editorBreakpoint.foreground") + private String mBreakpointForeground; + @SerializedName("editor.debuggingLineBackground") + private String mDebuggingLineBackground; public String getLineHighlightBackground() { return mLineHighlightBackground; @@ -86,4 +90,20 @@ public class EditorColors { public void setImeForegroundColor(String imeForegroundColor) { mImeForegroundColor = imeForegroundColor; } + + public String getBreakpointForeground() { + return mBreakpointForeground; + } + + public void setBreakpointForeground(String breakpointForeground) { + mBreakpointForeground = breakpointForeground; + } + + public String getDebuggingLineBackground() { + return mDebuggingLineBackground; + } + + public void setDebuggingLineBackground(String debuggingLineBackground) { + mDebuggingLineBackground = debuggingLineBackground; + } } diff --git a/app/src/main/java/org/autojs/autojs/ui/edit/EditorMenu.java b/app/src/main/java/org/autojs/autojs/ui/edit/EditorMenu.java index 26d6710b..9c1e39e9 100644 --- a/app/src/main/java/org/autojs/autojs/ui/edit/EditorMenu.java +++ b/app/src/main/java/org/autojs/autojs/ui/edit/EditorMenu.java @@ -6,12 +6,14 @@ import android.text.InputType; import android.view.MenuItem; import com.stardust.pio.PFiles; + import org.autojs.autojs.R; import org.autojs.autojs.ui.build.BuildActivity; import org.autojs.autojs.ui.build.BuildActivity_; import org.autojs.autojs.ui.edit.editor.CodeEditor; import org.autojs.autojs.ui.log.LogActivity_; import org.autojs.autojs.theme.dialog.ThemeColorMaterialDialogBuilder; + import com.stardust.util.ClipboardUtil; import java.util.Locale; @@ -53,6 +55,21 @@ public class EditorMenu { if (onMoreOptionsSelected(item)) { return true; } + if(onDebugOptionsSelected(item)){ + return true; + } + } + return false; + } + + private boolean onDebugOptionsSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_breakpoint: + mEditor.addOrRemoveBreakpointAtCurrentLine(); + return true; + case R.id.action_launch_debugger: + mEditorView.launchDebugger(); + return true; } return false; } diff --git a/app/src/main/java/org/autojs/autojs/ui/edit/EditorView.java b/app/src/main/java/org/autojs/autojs/ui/edit/EditorView.java index 46b0489a..daf308d3 100644 --- a/app/src/main/java/org/autojs/autojs/ui/edit/EditorView.java +++ b/app/src/main/java/org/autojs/autojs/ui/edit/EditorView.java @@ -11,10 +11,13 @@ import android.os.Bundle; import android.os.Parcelable; import android.support.annotation.Nullable; import android.support.design.widget.Snackbar; +import android.support.v4.app.Fragment; import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentManager; import android.support.v4.widget.DrawerLayout; import android.text.TextUtils; import android.util.AttributeSet; +import android.util.SparseBooleanArray; import android.view.Gravity; import android.view.View; import android.widget.FrameLayout; @@ -50,6 +53,8 @@ import org.autojs.autojs.ui.edit.keyboard.FunctionsKeyboardHelper; import org.autojs.autojs.ui.edit.keyboard.FunctionsKeyboardView; import org.autojs.autojs.ui.edit.theme.Theme; import org.autojs.autojs.ui.edit.theme.Themes; +import org.autojs.autojs.ui.edit.toolbar.DebugToolbarFragment; +import org.autojs.autojs.ui.edit.toolbar.DebugToolbarFragment_; import org.autojs.autojs.ui.edit.toolbar.NormalToolbarFragment; import org.autojs.autojs.ui.edit.toolbar.NormalToolbarFragment_; import org.autojs.autojs.ui.edit.toolbar.SearchToolbarFragment; @@ -117,6 +122,9 @@ public class EditorView extends FrameLayout implements CodeCompletionBar.OnHintC public void onReceive(Context context, Intent intent) { if (ACTION_ON_EXECUTION_FINISHED.equals(intent.getAction())) { mScriptExecutionId = ScriptExecution.NO_ID; + if (mDebugging) { + exitDebugging(); + } setMenuItemStatus(R.id.run, true); String msg = intent.getStringExtra(Scripts.EXTRA_EXCEPTION_MESSAGE); int line = intent.getIntExtra(Scripts.EXTRA_EXCEPTION_LINE_NUMBER, -1); @@ -130,8 +138,11 @@ public class EditorView extends FrameLayout implements CodeCompletionBar.OnHintC } } }; + + private SparseBooleanArray mMenuItemStatus = new SparseBooleanArray(); private String mRestoredText; private NormalToolbarFragment mNormalToolbar = new NormalToolbarFragment_(); + private boolean mDebugging = false; public EditorView(Context context) { super(context); @@ -231,6 +242,7 @@ public class EditorView extends FrameLayout implements CodeCompletionBar.OnHintC private void setMenuItemStatus(int id, boolean enabled) { + mMenuItemStatus.put(id, enabled); ToolbarFragment fragment = (ToolbarFragment) getActivity().getSupportFragmentManager() .findFragmentById(R.id.toolbar_menu); if (fragment == null) { @@ -240,6 +252,9 @@ public class EditorView extends FrameLayout implements CodeCompletionBar.OnHintC } } + public boolean getMenuItemStatus(int id, boolean defValue) { + return mMenuItemStatus.get(id, defValue); + } @AfterViews void init() { @@ -347,6 +362,7 @@ public class EditorView extends FrameLayout implements CodeCompletionBar.OnHintC } } + @SuppressLint("CheckResult") public void runAndSaveFileIfNeeded() { save().observeOn(AndroidSchedulers.mainThread()) .subscribe(s -> run()); @@ -515,6 +531,26 @@ public class EditorView extends FrameLayout implements CodeCompletionBar.OnHintC } + public void launchDebugger() { + DebugToolbarFragment debugToolbarFragment = DebugToolbarFragment_.builder() + .build(); + getActivity().getSupportFragmentManager().beginTransaction() + .replace(R.id.toolbar_menu, debugToolbarFragment) + .commit(); + mDebugging = true; + } + + public void exitDebugging() { + FragmentManager fragmentManager = getActivity().getSupportFragmentManager(); + Fragment fragment = fragmentManager.findFragmentById(R.id.toolbar_menu); + if (fragment instanceof DebugToolbarFragment) { + ((DebugToolbarFragment) fragment).detachDebugger(); + } + showNormalToolbar(); + mEditor.setDebuggingLine(-1); + mDebugging = false; + } + private void showErrorMessage(String msg) { Snackbar.make(EditorView.this, getResources().getString(R.string.text_error) + ": " + msg, Snackbar.LENGTH_LONG) .setAction(R.string.text_detail, v -> LogActivity_.intent(getContext()).start()) diff --git a/app/src/main/java/org/autojs/autojs/ui/edit/editor/CodeEditText.java b/app/src/main/java/org/autojs/autojs/ui/edit/editor/CodeEditText.java index 79602b90..db8dca48 100644 --- a/app/src/main/java/org/autojs/autojs/ui/edit/editor/CodeEditText.java +++ b/app/src/main/java/org/autojs/autojs/ui/edit/editor/CodeEditText.java @@ -22,7 +22,6 @@ import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Typeface; -import android.os.Parcelable; import android.support.v7.widget.AppCompatEditText; import android.text.Editable; import android.text.Layout; @@ -31,14 +30,16 @@ import android.util.Log; import android.util.TimingLogger; import android.view.Gravity; -import org.autojs.autojs.BuildConfig; import org.autojs.autojs.ui.edit.theme.Theme; import org.autojs.autojs.ui.edit.theme.TokenMapping; + import com.stardust.util.ClipboardUtil; import com.stardust.util.TextUtils; import org.mozilla.javascript.Token; +import java.util.LinkedHashMap; + import static org.autojs.autojs.ui.edit.editor.BracketMatching.UNMATCHED_BRACKET; /** @@ -62,6 +63,8 @@ public class CodeEditText extends AppCompatEditText { private int mFirstLineForDraw = -1, mLastLineForDraw; private int[] mMatchingBrackets = {-1, -1}; private int mUnmatchedBracket = -1; + private LinkedHashMap mBreakpoints = new LinkedHashMap<>(); + private int mDebuggingLine = -1; public CodeEditText(Context context) { @@ -87,6 +90,10 @@ public class CodeEditText extends AppCompatEditText { mLineHighlightPaint.setStyle(Paint.Style.FILL); } + public LinkedHashMap getBreakpoints() { + return mBreakpoints; + } + public void setTheme(Theme theme) { mTheme = theme; invalidate(); @@ -101,8 +108,8 @@ public class CodeEditText extends AppCompatEditText { updatePaddingForGutter(); updateLineRangeForDraw(canvas); - //绘制当前行高亮需要在绘制光标之前 - drawLineHighlight(canvas, mLineHighlightPaint, getCurrentLine()); + //绘制行高亮需要在绘制光标之前 + drawLineHighlights(canvas); //调用super.onDraw绘制光标和选择高亮。因为字体颜色被设置为透明因此super.onDraw()绘制的字体不显示 // TODO: 2018/2/24 优化效率。不绘制透明字体。 @@ -118,6 +125,30 @@ public class CodeEditText extends AppCompatEditText { mLogger.dumpToLog(); } + public int getDebuggingLine() { + return mDebuggingLine; + } + + public void setDebuggingLine(int debuggingLine) { + mDebuggingLine = debuggingLine; + invalidate(); + } + + private void drawLineHighlights(Canvas canvas) { + int currentLine = getCurrentLine(); + int debugHighlightLine = mDebuggingLine; + if(debugHighlightLine != currentLine){ + //绘制当前行高亮 + mLineHighlightPaint.setColor(mTheme.getLineHighlightBackgroundColor()); + drawLineHighlight(canvas, mLineHighlightPaint, getCurrentLine()); + } + if(debugHighlightLine != -1){ + mLineHighlightPaint.setColor(mTheme.getDebuggingLineBackgroundColor()); + drawLineHighlight(canvas, mLineHighlightPaint, debugHighlightLine); + } + + } + private void updateLineRangeForDraw(Canvas canvas) { Layout layout = getLayout(); if (layout == null) @@ -159,15 +190,22 @@ public class CodeEditText extends AppCompatEditText { int scrollX = Math.max(getRealScrollX() - paddingLeft, 0); Paint paint = getPaint(); int lineNumberColor = mTheme.getLineNumberColor(); + int breakPointColor = mTheme.getBreakpointColor(); if (DEBUG) Log.d(LOG_TAG, "draw line: " + (mLastLineForDraw - mFirstLineForDraw + 1)); mLogger.addSplit("before draw line"); for (int line = mFirstLineForDraw; line <= mLastLineForDraw && line < lineCount; line++) { int lineBottom = layout.getLineTop(line + 1); + int lineTop = layout.getLineTop(line); int lineBaseline = lineBottom - layout.getLineDescent(line); //drawLineNumber String lineNumberText = Integer.toString(line + 1); + // if there is a breakpoint at this line, draw highlight background for line number + if (mBreakpoints.containsKey(line)) { + paint.setColor(breakPointColor); + canvas.drawRect(0, lineTop, paddingLeft - 10, lineBottom, paint); + } paint.setColor(lineNumberColor); canvas.drawText(lineNumberText, 0, lineNumberText.length(), 10, lineBaseline, paint); @@ -225,7 +263,6 @@ public class CodeEditText extends AppCompatEditText { } int lineTop = getLayout().getLineTop(line); int lineBottom = getLayout().getLineTop(line + 1); - paint.setColor(mTheme.getLineHighlightBackgroundColor()); canvas.drawRect(0, lineTop, canvas.getWidth(), lineBottom, paint); } diff --git a/app/src/main/java/org/autojs/autojs/ui/edit/editor/CodeEditor.java b/app/src/main/java/org/autojs/autojs/ui/edit/editor/CodeEditor.java index 5f894d70..3834cda8 100644 --- a/app/src/main/java/org/autojs/autojs/ui/edit/editor/CodeEditor.java +++ b/app/src/main/java/org/autojs/autojs/ui/edit/editor/CodeEditor.java @@ -7,13 +7,15 @@ import android.util.AttributeSet; import android.widget.Toast; import com.afollestad.materialdialogs.MaterialDialog; -import com.android.dx.util.IntList; import com.stardust.autojs.script.JsBeautifier; + import org.autojs.autojs.R; import org.autojs.autojs.ui.edit.theme.Theme; + import com.stardust.util.ClipboardUtil; import com.stardust.util.TextUtils; +import java.util.LinkedHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -53,7 +55,6 @@ public class CodeEditor extends HVScrollView { private JsBeautifier mJsBeautifier; private MaterialDialog mProcessDialog; - private CharSequence mReplacement = ""; private String mKeywords; private Matcher mMatcher; @@ -73,7 +74,7 @@ public class CodeEditor extends HVScrollView { private void init() { //setFillViewport(true); inflate(getContext(), R.layout.code_editor, this); - mCodeEditText = (CodeEditText) findViewById(R.id.code_edit_text); + mCodeEditText = findViewById(R.id.code_edit_text); mCodeEditText.addTextChangedListener(new AutoIndent(mCodeEditText)); mTextViewRedoUndo = new TextViewRedoUndo(mCodeEditText); mJavaScriptHighlighter = new JavaScriptHighlighter(mTheme, mCodeEditText); @@ -260,7 +261,7 @@ public class CodeEditor extends HVScrollView { } public void findPrev() { - if (mMatcher != null){ + if (mMatcher != null) { Toast.makeText(getContext(), R.string.error_regex_find_prev, Toast.LENGTH_SHORT).show(); return; } @@ -329,6 +330,30 @@ public class CodeEditor extends HVScrollView { mTextViewRedoUndo.markTextAsUnchanged(); } + public LinkedHashMap getBreakpoints() { + return mCodeEditText.getBreakpoints(); + } + + public void setDebuggingLine(int line){ + mCodeEditText.setDebuggingLine(line); + } + + public void addOrRemoveBreakpoint(int line) { + LinkedHashMap breakpoints = mCodeEditText.getBreakpoints(); + if(breakpoints.remove(line) == null){ + breakpoints.put(line, new Breakpoint(line)); + } + mCodeEditText.invalidate(); + } + + public void addOrRemoveBreakpointAtCurrentLine() { + int line = LayoutHelper.getLineOfChar(mCodeEditText.getLayout(), mCodeEditText.getSelectionStart()); + if (line < 0 || line >= mCodeEditText.getLayout().getLineCount()) + return; + addOrRemoveBreakpoint(line); + } + + @Override protected void onDraw(Canvas canvas) { int codeWidth = getWidth() - getPaddingLeft() - getPaddingRight(); @@ -340,4 +365,14 @@ public class CodeEditor extends HVScrollView { } super.onDraw(canvas); } + + public static class Breakpoint { + + public int line; + public boolean enabled = true; + + public Breakpoint(int line) { + this.line = line; + } + } } diff --git a/app/src/main/java/org/autojs/autojs/ui/edit/theme/Theme.java b/app/src/main/java/org/autojs/autojs/ui/edit/theme/Theme.java index 2c05413e..27e490b4 100644 --- a/app/src/main/java/org/autojs/autojs/ui/edit/theme/Theme.java +++ b/app/src/main/java/org/autojs/autojs/ui/edit/theme/Theme.java @@ -25,6 +25,8 @@ public class Theme { private int mImeBarForegroundColor = Color.WHITE; private EditorTheme mEditorTheme; private int mLineHighlightBackground; + private int mBreakpointColor; + private int mDebuggingLineBackground; public Theme(EditorTheme theme) { mEditorTheme = theme; @@ -34,6 +36,8 @@ public class Theme { mImeBarBackgroundColor = parseColor(theme.getEditorColors().getImeBackgroundColor(), mImeBarBackgroundColor); mImeBarForegroundColor = parseColor(theme.getEditorColors().getImeForegroundColor(), mImeBarForegroundColor); mLineHighlightBackground = parseColor(theme.getEditorColors().getLineHighlightBackground(), mLineHighlightBackground); + mDebuggingLineBackground = parseColor(theme.getEditorColors().getDebuggingLineBackground(), mDebuggingLineBackground); + mBreakpointColor = parseColor(theme.getEditorColors().getBreakpointForeground(), mBackgroundColor); for (TokenColor tokenColor : theme.getTokenColors()) { String foregroundStr = tokenColor.getSettings().getForeground(); @@ -141,4 +145,11 @@ public class Theme { } + public int getBreakpointColor() { + return mBreakpointColor; + } + + public int getDebuggingLineBackgroundColor() { + return mDebuggingLineBackground; + } } diff --git a/app/src/main/java/org/autojs/autojs/ui/edit/toolbar/DebugToolbarFragment.java b/app/src/main/java/org/autojs/autojs/ui/edit/toolbar/DebugToolbarFragment.java index 535fc072..d2c7d640 100644 --- a/app/src/main/java/org/autojs/autojs/ui/edit/toolbar/DebugToolbarFragment.java +++ b/app/src/main/java/org/autojs/autojs/ui/edit/toolbar/DebugToolbarFragment.java @@ -1,35 +1,116 @@ package org.autojs.autojs.ui.edit.toolbar; +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; import android.os.Looper; +import android.support.annotation.Nullable; import android.util.Log; +import android.view.View; + +import com.stardust.autojs.engine.RhinoJavaScriptEngine; +import com.stardust.autojs.execution.ScriptExecution; +import com.stardust.autojs.rhino.debug.Dim; +import com.stardust.autojs.rhino.debug.DebugCallback; + +import org.androidannotations.annotations.Click; +import org.androidannotations.annotations.EFragment; +import org.autojs.autojs.R; +import org.autojs.autojs.autojs.AutoJs; +import org.autojs.autojs.ui.edit.EditorView; +import org.autojs.autojs.ui.edit.editor.CodeEditor; import org.mozilla.javascript.ContextFactory; -import org.mozilla.javascript.tools.debugger.Dim; -import org.mozilla.javascript.tools.debugger.GuiCallback; -public class DebugToolbarFragment implements GuiCallback { +import java.util.Arrays; +import java.util.List; - private Dim mDim; +@EFragment(R.layout.fragment_debug_toolbar) +public class DebugToolbarFragment extends ToolbarFragment implements DebugCallback { - public void attachDebugger(){ - mDim.attachTo(ContextFactory.getGlobal()); + private static final String LOG_TAG = "DebugToolbarFragment"; + private Dim mDim = new Dim(); + private EditorView mEditorView; + private Handler mHandler; + + public DebugToolbarFragment() { + mDim.setGuiCallback(this); + mDim.setBreak(); + mDim.attachTo(AutoJs.getInstance().getScriptEngineService(), ContextFactory.getGlobal()); + Log.d(LOG_TAG, "DebugToolbarFragment()"); } - public void deattchDebugger(){ + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mHandler = new Handler(); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + mEditorView = findEditorView(view); + mEditorView.run(); + Log.d(LOG_TAG, "onViewCreated"); + } + + public void detachDebugger() { mDim.detach(); } - public void breakpoint(){ - mDim.setGuiCallback(this); + @Click(R.id.step_over) + void stepOver() { + mEditorView.getEditor().setDebuggingLine(-1); + mDim.setReturnValue(Dim.STEP_OVER); + } + + @Click(R.id.step_into) + void stepInto() { + mEditorView.getEditor().setDebuggingLine(-1); + mDim.setReturnValue(Dim.STEP_INTO); + } + + @Click(R.id.stop_out) + void stepOut() { + mEditorView.getEditor().setDebuggingLine(-1); + mDim.setReturnValue(Dim.STEP_OUT); + } + + @Click(R.id.stop_script) + void stopScript() { + mEditorView.forceStop(); + } + + @Click(R.id.resume_script) + void resumeScript() { + mEditorView.getEditor().setDebuggingLine(-1); + mDim.setReturnValue(Dim.GO); } @Override public void updateSourceText(Dim.SourceInfo sourceInfo) { + Log.d(LOG_TAG, "updateSourceText: url = " + sourceInfo.url() + ", source = " + sourceInfo.source()); + if (!sourceInfo.url().equals(mEditorView.getFile().toString())) { + return; + } + sourceInfo.removeAllBreakpoints(); + for (CodeEditor.Breakpoint breakpoint : mEditorView.getEditor().getBreakpoints().values()) { + int line = breakpoint.line + 1; + if (sourceInfo.breakableLine(line)) { + sourceInfo.breakpoint(line, breakpoint.enabled); + Log.d(LOG_TAG, "not breakable: " + line); + } + } } @Override - public void enterInterrupt(Dim.StackFrame stackFrame, String s, String s1) { - + public void enterInterrupt(Dim.StackFrame stackFrame, String threadName, String s1) { + Log.d(LOG_TAG, "enterInterrupt: threadName = " + threadName + ", url = " + stackFrame.getUrl() + ", line = " + stackFrame.getLineNumber()); + if (stackFrame.getUrl().equals(mEditorView.getFile().toString())) { + mEditorView.getEditor().setDebuggingLine(stackFrame.getLineNumber() - 1); + } else { + mHandler.post(this::resumeScript); + } } @Override @@ -38,7 +119,26 @@ public class DebugToolbarFragment implements GuiCallback { } @Override - public void dispatchNextGuiEvent() throws InterruptedException { + public void dispatchNextGuiEvent() { + Log.d(LOG_TAG, "dispatchNextGuiEvent"); } + + @Override + public boolean shouldAttachDebugger(RhinoJavaScriptEngine engine) { + ScriptExecution execution = AutoJs.getInstance().getScriptEngineService().getScriptExecution(mEditorView.getScriptExecutionId()); + return execution != null && execution.getId() == engine.getId(); + + } + + @Override + public List getMenuItemIds() { + return Arrays.asList(R.id.step_over, R.id.step_into, R.id.stop_out, R.id.resume_script, R.id.stop_script); + } + + @Override + public void onDestroy() { + super.onDestroy(); + mDim.detach(); + } } diff --git a/app/src/main/java/org/autojs/autojs/ui/edit/toolbar/ToolbarFragment.java b/app/src/main/java/org/autojs/autojs/ui/edit/toolbar/ToolbarFragment.java index 1dcd814b..aed9a309 100644 --- a/app/src/main/java/org/autojs/autojs/ui/edit/toolbar/ToolbarFragment.java +++ b/app/src/main/java/org/autojs/autojs/ui/edit/toolbar/ToolbarFragment.java @@ -1,11 +1,14 @@ package org.autojs.autojs.ui.edit.toolbar; import android.os.Bundle; +import android.os.Parcelable; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.util.SparseBooleanArray; import android.view.View; +import org.autojs.autojs.ui.edit.EditorView; + import java.util.List; public abstract class ToolbarFragment extends Fragment implements View.OnClickListener { @@ -16,7 +19,6 @@ public abstract class ToolbarFragment extends Fragment implements View.OnClickLi private OnMenuItemClickListener mOnMenuItemClickListener; private List mMenuItemIds; - private SparseBooleanArray mMenuItemStatus = new SparseBooleanArray(); public void setOnMenuItemClickListener(OnMenuItemClickListener listener) { mOnMenuItemClickListener = listener; @@ -27,21 +29,32 @@ public abstract class ToolbarFragment extends Fragment implements View.OnClickLi @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - updateMenuItemStatus(); + updateMenuItemStatus(view); } - private void updateMenuItemStatus() { - View rootView = getView(); + protected EditorView findEditorView(View view) { + while (!(view instanceof EditorView) && view.getParent() != null) { + view = (View) view.getParent(); + } + if (!(view instanceof EditorView)) { + throw new IllegalStateException("cannot find EditorView from child: " + view); + } + return (EditorView) view; + } + + + private void updateMenuItemStatus(View rootView) { if (rootView == null) { return; } + EditorView editorView = findEditorView(rootView); if (mMenuItemIds == null) { mMenuItemIds = getMenuItemIds(); } for (int id : mMenuItemIds) { View view = rootView.findViewById(id); view.setOnClickListener(this); - view.setEnabled(mMenuItemStatus.get(id, view.isEnabled())); + view.setEnabled(editorView.getMenuItemStatus(id, view.isEnabled())); } } @@ -60,7 +73,6 @@ public abstract class ToolbarFragment extends Fragment implements View.OnClickLi if (!mMenuItemIds.contains(id)) { return; } - mMenuItemStatus.put(id, enabled); View rootView = getView(); if (rootView == null) { return; diff --git a/app/src/main/java/org/autojs/autojs/ui/main/task/TaskListRecyclerView.java b/app/src/main/java/org/autojs/autojs/ui/main/task/TaskListRecyclerView.java index 3c9c7fe7..aba82132 100644 --- a/app/src/main/java/org/autojs/autojs/ui/main/task/TaskListRecyclerView.java +++ b/app/src/main/java/org/autojs/autojs/ui/main/task/TaskListRecyclerView.java @@ -64,7 +64,7 @@ public class TaskListRecyclerView extends ThemeColorRecyclerView { private ScriptExecutionListener mScriptExecutionListener = new SimpleScriptExecutionListener() { @Override public void onStart(final ScriptExecution execution) { - mAdapter.notifyChildInserted(0, mRunningTaskGroup.addTask(execution)); + post(()-> mAdapter.notifyChildInserted(0, mRunningTaskGroup.addTask(execution))); } @Override @@ -78,12 +78,14 @@ public class TaskListRecyclerView extends ThemeColorRecyclerView { } private void onFinish(ScriptExecution execution){ - final int i = mRunningTaskGroup.removeTask(execution); - if (i >= 0) { - mAdapter.notifyChildRemoved(0, i); - } else { - refresh(); - } + post(()->{ + final int i = mRunningTaskGroup.removeTask(execution); + if (i >= 0) { + mAdapter.notifyChildRemoved(0, i); + } else { + refresh(); + } + }); } }; diff --git a/app/src/main/res/drawable-xhdpi/ic_debug.png b/app/src/main/res/drawable-xhdpi/ic_debug.png new file mode 100644 index 00000000..c766543b Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_debug.png differ diff --git a/app/src/main/res/drawable/ic_debug_step_into.png b/app/src/main/res/drawable/ic_debug_step_into.png new file mode 100644 index 00000000..bf7f1a2a Binary files /dev/null and b/app/src/main/res/drawable/ic_debug_step_into.png differ diff --git a/app/src/main/res/drawable/ic_debug_step_out.png b/app/src/main/res/drawable/ic_debug_step_out.png new file mode 100644 index 00000000..468e1975 Binary files /dev/null and b/app/src/main/res/drawable/ic_debug_step_out.png differ diff --git a/app/src/main/res/drawable/ic_debug_step_over.png b/app/src/main/res/drawable/ic_debug_step_over.png new file mode 100644 index 00000000..3860385c Binary files /dev/null and b/app/src/main/res/drawable/ic_debug_step_over.png differ diff --git a/app/src/main/res/layout/fragment_debug_toolbar.xml b/app/src/main/res/layout/fragment_debug_toolbar.xml new file mode 100644 index 00000000..6752d24f --- /dev/null +++ b/app/src/main/res/layout/fragment_debug_toolbar.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_editor.xml b/app/src/main/res/menu/menu_editor.xml index e2456bdd..bc7ae83f 100644 --- a/app/src/main/res/menu/menu_editor.xml +++ b/app/src/main/res/menu/menu_editor.xml @@ -79,6 +79,24 @@ + + + + + + + + + + key_script_dir_path /脚本/ 发生错误: %s + 调试 + 断点 + 启动调试 + 跳出 + 进入 + 单步 + 继续 + 停止 diff --git a/autojs/src/main/java/com/stardust/autojs/AutoJs.java b/autojs/src/main/java/com/stardust/autojs/AutoJs.java index c6fc77c1..2f95ba50 100644 --- a/autojs/src/main/java/com/stardust/autojs/AutoJs.java +++ b/autojs/src/main/java/com/stardust/autojs/AutoJs.java @@ -101,6 +101,7 @@ public abstract class AutoJs { engine.setRuntime(createRuntime()); return engine; }); + LoopBasedJavaScriptEngine.initEngine(); mScriptEngineManager.registerEngine(AutoFileSource.ENGINE, () -> new RootAutomatorEngine(mContext)); } diff --git a/autojs/src/main/java/com/stardust/autojs/engine/LoopBasedJavaScriptEngine.java b/autojs/src/main/java/com/stardust/autojs/engine/LoopBasedJavaScriptEngine.java index 4cb03c4b..c2b6243a 100644 --- a/autojs/src/main/java/com/stardust/autojs/engine/LoopBasedJavaScriptEngine.java +++ b/autojs/src/main/java/com/stardust/autojs/engine/LoopBasedJavaScriptEngine.java @@ -86,5 +86,9 @@ public class LoopBasedJavaScriptEngine extends RhinoJavaScriptEngine { super.init(); } + public static void initEngine(){ + RhinoJavaScriptEngine.initEngine(); + } + } diff --git a/autojs/src/main/java/com/stardust/autojs/engine/RhinoJavaScriptEngine.java b/autojs/src/main/java/com/stardust/autojs/engine/RhinoJavaScriptEngine.java index d50a8e4a..dfd1b86b 100644 --- a/autojs/src/main/java/com/stardust/autojs/engine/RhinoJavaScriptEngine.java +++ b/autojs/src/main/java/com/stardust/autojs/engine/RhinoJavaScriptEngine.java @@ -3,6 +3,7 @@ package com.stardust.autojs.engine; import android.os.Looper; import android.util.Log; +import com.stardust.app.GlobalAppContext; import com.stardust.autojs.BuildConfig; import com.stardust.autojs.rhino.AndroidContextFactory; import com.stardust.autojs.rhino.NativeJavaClassWithPrototype; @@ -33,6 +34,7 @@ import java.io.Reader; import java.net.URI; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.concurrent.ConcurrentHashMap; @@ -43,10 +45,13 @@ import java.util.concurrent.ConcurrentHashMap; public class RhinoJavaScriptEngine extends JavaScriptEngine { + public static final String SOURCE_NAME_INIT = ""; + private static final String LOG_TAG = "RhinoJavaScriptEngine"; private static int contextCount = 0; private static StringScriptSource sInitScript; + private static final ConcurrentHashMap sContextEngineMap = new ConcurrentHashMap<>(); private Context mContext; private Scriptable mScriptable; @@ -69,7 +74,7 @@ public class RhinoJavaScriptEngine extends JavaScriptEngine { Reader reader = source.getNonNullScriptReader(); try { reader = preprocess(reader); - return mContext.evaluateReader(mScriptable, reader, "<" + source.getName() + ">", 1, null); + return mContext.evaluateReader(mScriptable, reader, source.toString(), 1, null); } catch (IOException e) { throw new UncheckedIOException(e); } @@ -90,6 +95,7 @@ public class RhinoJavaScriptEngine extends JavaScriptEngine { public synchronized void destroy() { super.destroy(); Log.d(LOG_TAG, "on destroy"); + sContextEngineMap.remove(getContext()); Context.exit(); contextCount--; Log.d(LOG_TAG, "contextCount = " + contextCount); @@ -105,7 +111,7 @@ public class RhinoJavaScriptEngine extends JavaScriptEngine { mThread = Thread.currentThread(); ScriptableObject.putProperty(mScriptable, "__engine__", this); initRequireBuilder(mContext, mScriptable); - mContext.evaluateString(mScriptable, getInitScript().getScript(), "", 1, null); + mContext.evaluateString(mScriptable, getInitScript().getScript(), SOURCE_NAME_INIT, 1, null); } private JavaScriptSource getInitScript() { @@ -148,12 +154,10 @@ public class RhinoJavaScriptEngine extends JavaScriptEngine { } public Context createContext() { - if (!ContextFactory.hasExplicitGlobal()) { - ContextFactory.initGlobal(new InterruptibleAndroidContextFactory(new File(mAndroidContext.getCacheDir(), "classes"))); - } Context context = new RhinoAndroidHelper(mAndroidContext).enterContext(); contextCount++; setupContext(context); + sContextEngineMap.put(context, this); return context; } @@ -164,6 +168,17 @@ public class RhinoJavaScriptEngine extends JavaScriptEngine { context.setWrapFactory(new WrapFactory()); } + public static void initEngine() { + if (!ContextFactory.hasExplicitGlobal()) { + android.content.Context context = GlobalAppContext.get(); + ContextFactory.initGlobal(new InterruptibleAndroidContextFactory(new File(context.getCacheDir(), "classes"))); + } + } + + public static RhinoJavaScriptEngine getEngineOfContext(Context context) { + return sContextEngineMap.get(context); + } + private class WrapFactory extends org.mozilla.javascript.WrapFactory { diff --git a/autojs/src/main/java/com/stardust/autojs/engine/ScriptEngine.java b/autojs/src/main/java/com/stardust/autojs/engine/ScriptEngine.java index ec08c4ac..f0a2c09a 100644 --- a/autojs/src/main/java/com/stardust/autojs/engine/ScriptEngine.java +++ b/autojs/src/main/java/com/stardust/autojs/engine/ScriptEngine.java @@ -3,7 +3,6 @@ package com.stardust.autojs.engine; import android.support.annotation.CallSuper; import com.stardust.autojs.execution.ScriptExecution; -import com.stardust.autojs.runtime.exception.ScriptException; import com.stardust.autojs.script.ScriptSource; import java.util.Map; @@ -14,7 +13,7 @@ import java.util.concurrent.atomic.AtomicInteger; * Created by Stardust on 2017/4/2. *

*

- * A ScriptEngine is created by {@link ScriptEngineManager#createEngine(String)} ()}, and then can be + * A ScriptEngine is created by {@link ScriptEngineManager#createEngine(String, int)} ()}, and then can be * used to execute script with {@link ScriptEngine#execute(ScriptSource)} in the **same** thread. * When the execution finish successfully, the engine should be destroy in the thread that created it. *

diff --git a/autojs/src/main/java/com/stardust/autojs/engine/ScriptEngineManager.java b/autojs/src/main/java/com/stardust/autojs/engine/ScriptEngineManager.java index c73a8cda..91507838 100644 --- a/autojs/src/main/java/com/stardust/autojs/engine/ScriptEngineManager.java +++ b/autojs/src/main/java/com/stardust/autojs/engine/ScriptEngineManager.java @@ -4,6 +4,7 @@ import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import com.stardust.autojs.execution.ScriptExecution; import com.stardust.autojs.script.ScriptSource; import com.stardust.util.Supplier; @@ -96,37 +97,34 @@ public class ScriptEngineManager { @Nullable - public ScriptEngine createEngine(String name) { + public ScriptEngine createEngine(String name, int id) { Supplier s = mEngineSuppliers.get(name); if (s == null) { return null; } ScriptEngine engine = s.get(); + engine.setId(id); putProperties(engine); addEngine(engine); return engine; } @Nullable - public ScriptEngine createEngineOfSource(ScriptSource source) { - return createEngine(source.getEngineName()); + public ScriptEngine createEngineOfSource(ScriptSource source, int id) { + return createEngine(source.getEngineName(), id); } - @NonNull - public ScriptEngine createEngineByNameOrThrow(String name) { - ScriptEngine engine = createEngine(name); + public ScriptEngine createEngineOfSourceOrThrow(ScriptSource source, int id) { + ScriptEngine engine = createEngineOfSource(source, id); if (engine == null) - throw new ScriptEngineFactory.EngineNotFoundException("name: " + name); + throw new ScriptEngineFactory.EngineNotFoundException("source: " + source.toString()); return engine; } @NonNull public ScriptEngine createEngineOfSourceOrThrow(ScriptSource source) { - ScriptEngine engine = createEngineOfSource(source); - if (engine == null) - throw new ScriptEngineFactory.EngineNotFoundException("source: " + source.toString()); - return engine; + return createEngineOfSourceOrThrow(source, ScriptExecution.NO_ID); } public void registerEngine(String name, Supplier supplier) { diff --git a/autojs/src/main/java/com/stardust/autojs/execution/RunnableScriptExecution.java b/autojs/src/main/java/com/stardust/autojs/execution/RunnableScriptExecution.java index f1ac849c..10e7b4b7 100644 --- a/autojs/src/main/java/com/stardust/autojs/execution/RunnableScriptExecution.java +++ b/autojs/src/main/java/com/stardust/autojs/execution/RunnableScriptExecution.java @@ -28,8 +28,7 @@ public class RunnableScriptExecution extends ScriptExecution.AbstractScriptExecu } public Object execute() { - mScriptEngine = mScriptEngineManager.createEngineOfSourceOrThrow(getSource()); - mScriptEngine.setId(getId()); + mScriptEngine = mScriptEngineManager.createEngineOfSourceOrThrow(getSource(), getId()); return execute(mScriptEngine); } diff --git a/autojs/src/main/java/com/stardust/autojs/rhino/debug/DebugCallback.java b/autojs/src/main/java/com/stardust/autojs/rhino/debug/DebugCallback.java new file mode 100644 index 00000000..3dd25922 --- /dev/null +++ b/autojs/src/main/java/com/stardust/autojs/rhino/debug/DebugCallback.java @@ -0,0 +1,50 @@ +package com.stardust.autojs.rhino.debug; +/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + +import com.stardust.autojs.engine.RhinoJavaScriptEngine; +import com.stardust.autojs.engine.ScriptEngine; + +import org.mozilla.javascript.Context; + +/** + * Interface for communication between the debugger and its GUI. This + * should be implemented by the GUI. + */ +public interface DebugCallback { + + /** + * Called when the source text of some script has been changed. + */ + void updateSourceText(Dim.SourceInfo sourceInfo); + + /** + * Called when the interrupt loop has been entered. + */ + void enterInterrupt(Dim.StackFrame lastFrame, + String threadTitle, + String alertMessage); + + /** + * Returns whether the current thread is the GUI's event thread. + * This information is required to avoid blocking the event thread + * from the debugger. + */ + boolean isGuiEventThread(); + + /** + * Processes the next GUI event. This manual pumping of GUI events + * is necessary when the GUI event thread itself has been stopped. + */ + void dispatchNextGuiEvent() throws InterruptedException; + + /** + * + * Returns whether the debugger should attach to this engine or not. + */ + boolean shouldAttachDebugger(RhinoJavaScriptEngine engine); +} \ No newline at end of file diff --git a/autojs/src/main/java/com/stardust/autojs/rhino/debug/Dim.java b/autojs/src/main/java/com/stardust/autojs/rhino/debug/Dim.java new file mode 100644 index 00000000..58b757b6 --- /dev/null +++ b/autojs/src/main/java/com/stardust/autojs/rhino/debug/Dim.java @@ -0,0 +1,1558 @@ +package com.stardust.autojs.rhino.debug; + +/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import com.stardust.autojs.ScriptEngineService; +import com.stardust.autojs.engine.RhinoJavaScriptEngine; +import com.stardust.autojs.engine.ScriptEngine; +import com.stardust.autojs.engine.ScriptEngineManager; + +import org.mozilla.javascript.*; +import org.mozilla.javascript.debug.*; +import org.mozilla.javascript.tools.debugger.*; + +import java.util.*; +import java.io.*; +import java.net.URL; + +/** + * Dim or Debugger Implementation for Rhino. + */ +public class Dim { + + // Constants for instructing the debugger what action to perform + // to end interruption. Used by 'returnValue'. + public static final int STEP_OVER = 0; + public static final int STEP_INTO = 1; + public static final int STEP_OUT = 2; + public static final int GO = 3; + public static final int BREAK = 4; + public static final int EXIT = 5; + + // Constants for the DimIProxy interface implementation class. + private static final int IPROXY_DEBUG = 0; + private static final int IPROXY_LISTEN = 1; + private static final int IPROXY_COMPILE_SCRIPT = 2; + private static final int IPROXY_EVAL_SCRIPT = 3; + private static final int IPROXY_STRING_IS_COMPILABLE = 4; + private static final int IPROXY_OBJECT_TO_STRING = 5; + private static final int IPROXY_OBJECT_PROPERTY = 6; + private static final int IPROXY_OBJECT_IDS = 7; + + /** + * Interface to the debugger GUI. + */ + private DebugCallback callback; + + /** + * Whether the debugger should break. + */ + private boolean breakFlag; + + /** + * The ScopeProvider object that provides the scope in which to + * evaluate script. + */ + private ScopeProvider scopeProvider; + + /** + * The SourceProvider object that provides the source of evaluated scripts. + */ + private SourceProvider sourceProvider; + + /** + * The index of the current stack frame. + */ + private int frameIndex = -1; + + /** + * Information about the current stack at the point of interruption. + */ + private volatile ContextData interruptedContextData; + + /** + * The ContextFactory to listen to for debugging information. + */ + private ContextFactory contextFactory; + + private ScriptEngineService scriptEngineService; + + /** + * Synchronization object used to allow script evaluations to + * happen when a thread is resumed. + */ + private Object monitor = new Object(); + + /** + * Synchronization object used to wait for valid + * {@link #interruptedContextData}. + */ + private Object eventThreadMonitor = new Object(); + + /** + * The action to perform to end the interruption loop. + */ + private volatile int returnValue = -1; + + /** + * Whether the debugger is inside the interruption loop. + */ + private boolean insideInterruptLoop; + + /** + * The requested script string to be evaluated when the thread + * has been resumed. + */ + private String evalRequest; + + /** + * The stack frame in which to evaluate {@link #evalRequest}. + */ + private StackFrame evalFrame; + + /** + * The result of evaluating {@link #evalRequest}. + */ + private String evalResult; + + /** + * Whether the debugger should break when a script exception is thrown. + */ + private boolean breakOnExceptions; + + /** + * Whether the debugger should break when a script function is entered. + */ + private boolean breakOnEnter; + + /** + * Whether the debugger should break when a script function is returned + * from. + */ + private boolean breakOnReturn; + + /** + * Table mapping URLs to information about the script source. + */ + private final Map urlToSourceInfo = + Collections.synchronizedMap(new HashMap()); + + /** + * Table mapping function names to information about the function. + */ + private final Map functionNames = + Collections.synchronizedMap(new HashMap()); + + /** + * Table mapping functions to information about the function. + */ + private final Map functionToSource = + Collections.synchronizedMap(new HashMap()); + + /** + * ContextFactory.Listener instance attached to {@link #contextFactory}. + */ + private DimIProxy listener; + + /** + * Sets the GuiCallback object to use. + */ + public void setGuiCallback(DebugCallback callback) { + this.callback = callback; + } + + /** + * Tells the debugger to break at the next opportunity. + */ + public void setBreak() { + this.breakFlag = true; + } + + /** + * Sets the ScopeProvider to be used. + */ + public void setScopeProvider(ScopeProvider scopeProvider) { + this.scopeProvider = scopeProvider; + } + + /** + * Sets the ScopeProvider to be used. + */ + public void setSourceProvider(final SourceProvider sourceProvider) { + this.sourceProvider = sourceProvider; + } + + /** + * Switches context to the stack frame with the given index. + */ + public void contextSwitch(int frameIndex) { + this.frameIndex = frameIndex; + } + + /** + * Sets whether the debugger should break on exceptions. + */ + public void setBreakOnExceptions(boolean breakOnExceptions) { + this.breakOnExceptions = breakOnExceptions; + } + + /** + * Sets whether the debugger should break on function entering. + */ + public void setBreakOnEnter(boolean breakOnEnter) { + this.breakOnEnter = breakOnEnter; + } + + /** + * Sets whether the debugger should break on function return. + */ + public void setBreakOnReturn(boolean breakOnReturn) { + this.breakOnReturn = breakOnReturn; + } + + /** + * Attaches the debugger to the given ContextFactory. + */ + public void attachTo(ScriptEngineService scriptEngineService, ContextFactory factory) { + detach(); + this.contextFactory = factory; + this.scriptEngineService = scriptEngineService; + this.listener = new DimIProxy(this, IPROXY_LISTEN); + scriptEngineService.registerEngineLifecycleCallback(this.listener); + } + + /** + * Detaches the debugger from the current ContextFactory. + */ + public void detach() { + if (listener != null) { + scriptEngineService.unregisterEngineLifecycleCallback(listener); + contextFactory = null; + scriptEngineService = null; + listener = null; + } + } + + /** + * Releases resources associated with this debugger. + */ + public void dispose() { + detach(); + } + + /** + * Returns the FunctionSource object for the given script or function. + */ + private FunctionSource getFunctionSource(DebuggableScript fnOrScript) { + FunctionSource fsource = functionSource(fnOrScript); + if (fsource == null) { + String url = getNormalizedUrl(fnOrScript); + SourceInfo si = sourceInfo(url); + if (si == null) { + if (!fnOrScript.isGeneratedScript()) { + // Not eval or Function, try to load it from URL + String source = loadSource(url); + if (source != null) { + DebuggableScript top = fnOrScript; + for (; ; ) { + DebuggableScript parent = top.getParent(); + if (parent == null) { + break; + } + top = parent; + } + registerTopScript(top, source); + fsource = functionSource(fnOrScript); + } + } + } + } + return fsource; + } + + /** + * Loads the script at the given URL. + */ + private String loadSource(String sourceUrl) { + String source = null; + int hash = sourceUrl.indexOf('#'); + if (hash >= 0) { + sourceUrl = sourceUrl.substring(0, hash); + } + try { + InputStream is; + openStream: + { + if (sourceUrl.indexOf(':') < 0) { + // Can be a file name + try { + if (sourceUrl.startsWith("~/")) { + String home = SecurityUtilities.getSystemProperty("user.home"); + if (home != null) { + String pathFromHome = sourceUrl.substring(2); + File f = new File(new File(home), pathFromHome); + if (f.exists()) { + is = new FileInputStream(f); + break openStream; + } + } + } + File f = new File(sourceUrl); + if (f.exists()) { + is = new FileInputStream(f); + break openStream; + } + } catch (SecurityException ex) { + } + // No existing file, assume missed http:// + if (sourceUrl.startsWith("//")) { + sourceUrl = "http:" + sourceUrl; + } else if (sourceUrl.startsWith("/")) { + sourceUrl = "http://127.0.0.1" + sourceUrl; + } else { + sourceUrl = "http://" + sourceUrl; + } + } + + is = (new URL(sourceUrl)).openStream(); + } + + try { + source = Kit.readReader(new InputStreamReader(is)); + } finally { + is.close(); + } + } catch (IOException ex) { + System.err.println + ("Failed to load source from " + sourceUrl + ": " + ex); + } + return source; + } + + /** + * Registers the given script as a top-level script in the debugger. + */ + private void registerTopScript(DebuggableScript topScript, String source) { + if (!topScript.isTopLevel()) { + throw new IllegalArgumentException(); + } + String url = getNormalizedUrl(topScript); + DebuggableScript[] functions = getAllFunctions(topScript); + if (sourceProvider != null) { + final String providedSource = sourceProvider.getSource(topScript); + if (providedSource != null) { + source = providedSource; + } + } + + final SourceInfo sourceInfo = new SourceInfo(source, functions, url); + + synchronized (urlToSourceInfo) { + SourceInfo old = urlToSourceInfo.get(url); + if (old != null) { + sourceInfo.copyBreakpointsFrom(old); + } + urlToSourceInfo.put(url, sourceInfo); + for (int i = 0; i != sourceInfo.functionSourcesTop(); ++i) { + FunctionSource fsource = sourceInfo.functionSource(i); + String name = fsource.name(); + if (name.length() != 0) { + functionNames.put(name, fsource); + } + } + } + + synchronized (functionToSource) { + for (int i = 0; i != functions.length; ++i) { + FunctionSource fsource = sourceInfo.functionSource(i); + functionToSource.put(functions[i], fsource); + } + } + + callback.updateSourceText(sourceInfo); + } + + /** + * Returns the FunctionSource object for the given function or script. + */ + private FunctionSource functionSource(DebuggableScript fnOrScript) { + return functionToSource.get(fnOrScript); + } + + /** + * Returns an array of all function names. + */ + public String[] functionNames() { + synchronized (urlToSourceInfo) { + return functionNames.keySet().toArray(new String[functionNames.size()]); + } + } + + /** + * Returns the FunctionSource object for the function with the given name. + */ + public FunctionSource functionSourceByName(String functionName) { + return functionNames.get(functionName); + } + + /** + * Returns the SourceInfo object for the given URL. + */ + public SourceInfo sourceInfo(String url) { + return urlToSourceInfo.get(url); + } + + /** + * Returns the source URL for the given script or function. + */ + private String getNormalizedUrl(DebuggableScript fnOrScript) { + String url = fnOrScript.getSourceName(); + if (url == null) { + url = ""; + } else { + // Not to produce window for eval from different lines, + // strip line numbers, i.e. replace all #[0-9]+\(eval\) by + // (eval) + // Option: similar teatment for Function? + char evalSeparator = '#'; + StringBuilder sb = null; + int urlLength = url.length(); + int cursor = 0; + for (; ; ) { + int searchStart = url.indexOf(evalSeparator, cursor); + if (searchStart < 0) { + break; + } + String replace = null; + int i = searchStart + 1; + while (i != urlLength) { + int c = url.charAt(i); + if (!('0' <= c && c <= '9')) { + break; + } + ++i; + } + if (i != searchStart + 1) { + // i points after #[0-9]+ + if ("(eval)".regionMatches(0, url, i, 6)) { + cursor = i + 6; + replace = "(eval)"; + } + } + if (replace == null) { + break; + } + if (sb == null) { + sb = new StringBuilder(); + sb.append(url.substring(0, searchStart)); + } + sb.append(replace); + } + if (sb != null) { + if (cursor != urlLength) { + sb.append(url.substring(cursor)); + } + url = sb.toString(); + } + } + return url; + } + + /** + * Returns an array of all functions in the given script. + */ + private static DebuggableScript[] getAllFunctions + (DebuggableScript function) { + ObjArray functions = new ObjArray(); + collectFunctions_r(function, functions); + DebuggableScript[] result = new DebuggableScript[functions.size()]; + functions.toArray(result); + return result; + } + + /** + * Helper function for {@link #getAllFunctions(DebuggableScript)}. + */ + private static void collectFunctions_r(DebuggableScript function, + ObjArray array) { + array.add(function); + for (int i = 0; i != function.getFunctionCount(); ++i) { + collectFunctions_r(function.getFunction(i), array); + } + } + + /** + * Clears all breakpoints. + */ + public void clearAllBreakpoints() { + for (SourceInfo si : urlToSourceInfo.values()) { + si.removeAllBreakpoints(); + } + } + + /** + * Called when a breakpoint has been hit. + */ + private void handleBreakpointHit(StackFrame frame, Context cx) { + breakFlag = false; + interrupted(cx, frame, null); + } + + /** + * Called when a script exception has been thrown. + */ + private void handleExceptionThrown(Context cx, Throwable ex, + StackFrame frame) { + if (breakOnExceptions) { + ContextData cd = frame.contextData(); + if (cd.lastProcessedException != ex) { + interrupted(cx, frame, ex); + cd.lastProcessedException = ex; + } + } + } + + /** + * Returns the current ContextData object. + */ + public ContextData currentContextData() { + return interruptedContextData; + } + + /** + * Sets the action to perform to end interruption. + */ + public void setReturnValue(int returnValue) { + synchronized (monitor) { + this.returnValue = returnValue; + monitor.notify(); + } + } + + /** + * Resumes execution of script. + */ + public void go() { + synchronized (monitor) { + this.returnValue = GO; + monitor.notifyAll(); + } + } + + /** + * Evaluates the given script. + */ + public String eval(String expr) { + String result = "undefined"; + if (expr == null) { + return result; + } + ContextData contextData = currentContextData(); + if (contextData == null || frameIndex >= contextData.frameCount()) { + return result; + } + StackFrame frame = contextData.getFrame(frameIndex); + if (contextData.eventThreadFlag) { + Context cx = Context.getCurrentContext(); + result = do_eval(cx, frame, expr); + } else { + synchronized (monitor) { + if (insideInterruptLoop) { + evalRequest = expr; + evalFrame = frame; + monitor.notify(); + do { + try { + monitor.wait(); + } catch (InterruptedException exc) { + Thread.currentThread().interrupt(); + break; + } + } while (evalRequest != null); + result = evalResult; + } + } + } + return result; + } + + /** + * Compiles the given script. + */ + public void compileScript(String url, String text) { + DimIProxy action = new DimIProxy(this, IPROXY_COMPILE_SCRIPT); + action.url = url; + action.text = text; + action.withContext(); + } + + /** + * Evaluates the given script. + */ + public void evalScript(final String url, final String text) { + DimIProxy action = new DimIProxy(this, IPROXY_EVAL_SCRIPT); + action.url = url; + action.text = text; + action.withContext(); + } + + /** + * Converts the given script object to a string. + */ + public String objectToString(Object object) { + DimIProxy action = new DimIProxy(this, IPROXY_OBJECT_TO_STRING); + action.object = object; + action.withContext(); + return action.stringResult; + } + + /** + * Returns whether the given string is syntactically valid script. + */ + public boolean stringIsCompilableUnit(String str) { + DimIProxy action = new DimIProxy(this, IPROXY_STRING_IS_COMPILABLE); + action.text = str; + action.withContext(); + return action.booleanResult; + } + + /** + * Returns the value of a property on the given script object. + */ + public Object getObjectProperty(Object object, Object id) { + DimIProxy action = new DimIProxy(this, IPROXY_OBJECT_PROPERTY); + action.object = object; + action.id = id; + action.withContext(); + return action.objectResult; + } + + /** + * Returns an array of the property names on the given script object. + */ + public Object[] getObjectIds(Object object) { + DimIProxy action = new DimIProxy(this, IPROXY_OBJECT_IDS); + action.object = object; + action.withContext(); + return action.objectArrayResult; + } + + /** + * Returns the value of a property on the given script object. + */ + private Object getObjectPropertyImpl(Context cx, Object object, + Object id) { + Scriptable scriptable = (Scriptable) object; + Object result; + if (id instanceof String) { + String name = (String) id; + if (name.equals("this")) { + result = scriptable; + } else if (name.equals("__proto__")) { + result = scriptable.getPrototype(); + } else if (name.equals("__parent__")) { + result = scriptable.getParentScope(); + } else { + result = ScriptableObject.getProperty(scriptable, name); + if (result == ScriptableObject.NOT_FOUND) { + result = Undefined.instance; + } + } + } else { + int index = ((Integer) id).intValue(); + result = ScriptableObject.getProperty(scriptable, index); + if (result == ScriptableObject.NOT_FOUND) { + result = Undefined.instance; + } + } + return result; + } + + /** + * Returns an array of the property names on the given script object. + */ + private Object[] getObjectIdsImpl(Context cx, Object object) { + if (!(object instanceof Scriptable) || object == Undefined.instance) { + return Context.emptyArgs; + } + + Object[] ids; + Scriptable scriptable = (Scriptable) object; + if (scriptable instanceof DebuggableObject) { + ids = ((DebuggableObject) scriptable).getAllIds(); + } else { + ids = scriptable.getIds(); + } + + Scriptable proto = scriptable.getPrototype(); + Scriptable parent = scriptable.getParentScope(); + int extra = 0; + if (proto != null) { + ++extra; + } + if (parent != null) { + ++extra; + } + if (extra != 0) { + Object[] tmp = new Object[extra + ids.length]; + System.arraycopy(ids, 0, tmp, extra, ids.length); + ids = tmp; + extra = 0; + if (proto != null) { + ids[extra++] = "__proto__"; + } + if (parent != null) { + ids[extra++] = "__parent__"; + } + } + + return ids; + } + + /** + * Interrupts script execution. + */ + private void interrupted(Context cx, final StackFrame frame, + Throwable scriptException) { + ContextData contextData = frame.contextData(); + boolean eventThreadFlag = callback.isGuiEventThread(); + contextData.eventThreadFlag = eventThreadFlag; + + boolean recursiveEventThreadCall = false; + + interruptedCheck: + synchronized (eventThreadMonitor) { + if (eventThreadFlag) { + if (interruptedContextData != null) { + recursiveEventThreadCall = true; + break interruptedCheck; + } + } else { + while (interruptedContextData != null) { + try { + eventThreadMonitor.wait(); + } catch (InterruptedException exc) { + return; + } + } + } + interruptedContextData = contextData; + } + + if (recursiveEventThreadCall) { + // XXX: For now the following is commented out as on Linux + // too deep recursion of dispatchNextGuiEvent causes GUI lockout. + // Note: it can make GUI unresponsive if long-running script + // will be called on GUI thread while processing another interrupt + if (false) { + // Run event dispatch until gui sets a flag to exit the initial + // call to interrupted. + while (this.returnValue == -1) { + try { + callback.dispatchNextGuiEvent(); + } catch (InterruptedException exc) { + } + } + } + return; + } + + if (interruptedContextData == null) Kit.codeBug(); + + try { + do { + int frameCount = contextData.frameCount(); + this.frameIndex = frameCount - 1; + + final String threadTitle = Thread.currentThread().toString(); + final String alertMessage; + if (scriptException == null) { + alertMessage = null; + } else { + alertMessage = scriptException.toString(); + } + + int returnValue = -1; + if (!eventThreadFlag) { + synchronized (monitor) { + if (insideInterruptLoop) Kit.codeBug(); + this.insideInterruptLoop = true; + this.evalRequest = null; + this.returnValue = -1; + callback.enterInterrupt(frame, threadTitle, + alertMessage); + try { + for (; ; ) { + try { + monitor.wait(); + } catch (InterruptedException exc) { + Thread.currentThread().interrupt(); + break; + } + if (evalRequest != null) { + this.evalResult = null; + try { + evalResult = do_eval(cx, evalFrame, + evalRequest); + } finally { + evalRequest = null; + evalFrame = null; + monitor.notify(); + } + continue; + } + if (this.returnValue != -1) { + returnValue = this.returnValue; + break; + } + } + } finally { + insideInterruptLoop = false; + } + } + } else { + this.returnValue = -1; + callback.enterInterrupt(frame, threadTitle, alertMessage); + while (this.returnValue == -1) { + try { + callback.dispatchNextGuiEvent(); + } catch (InterruptedException exc) { + } + } + returnValue = this.returnValue; + } + switch (returnValue) { + case STEP_OVER: + contextData.breakNextLine = true; + contextData.stopAtFrameDepth = contextData.frameCount(); + break; + case STEP_INTO: + contextData.breakNextLine = true; + contextData.stopAtFrameDepth = -1; + break; + case STEP_OUT: + if (contextData.frameCount() > 1) { + contextData.breakNextLine = true; + contextData.stopAtFrameDepth + = contextData.frameCount() - 1; + } + break; + } + } while (false); + } finally { + synchronized (eventThreadMonitor) { + interruptedContextData = null; + eventThreadMonitor.notifyAll(); + } + } + + } + + /** + * Evaluates script in the given stack frame. + */ + private static String do_eval(Context cx, StackFrame frame, String expr) { + String resultString; + Debugger saved_debugger = cx.getDebugger(); + Object saved_data = cx.getDebuggerContextData(); + int saved_level = cx.getOptimizationLevel(); + + cx.setDebugger(null, null); + cx.setOptimizationLevel(-1); + cx.setGeneratingDebug(false); + try { + Callable script = (Callable) cx.compileString(expr, "", 0, null); + Object result = script.call(cx, frame.scope, frame.thisObj, + ScriptRuntime.emptyArgs); + if (result == Undefined.instance) { + resultString = ""; + } else { + resultString = ScriptRuntime.toString(result); + } + } catch (Exception exc) { + resultString = exc.getMessage(); + } finally { + cx.setGeneratingDebug(true); + cx.setOptimizationLevel(saved_level); + cx.setDebugger(saved_debugger, saved_data); + } + if (resultString == null) { + resultString = "null"; + } + return resultString; + } + + /** + * Proxy class to implement debug interfaces without bloat of class + * files. + */ + private class DimIProxy + implements ContextAction, ScriptEngineManager.EngineLifecycleCallback, Debugger { + + /** + * The debugger. + */ + private Dim dim; + + /** + * The interface implementation type. One of the IPROXY_* constants + * defined in {@link Dim}. + */ + private int type; + + /** + * The URL origin of the script to compile or evaluate. + */ + private String url; + + /** + * The text of the script to compile, evaluate or test for compilation. + */ + private String text; + + /** + * The object to convert, get a property from or enumerate. + */ + private Object object; + + /** + * The property to look up in {@link #object}. + */ + private Object id; + + /** + * The boolean result of the action. + */ + private boolean booleanResult; + + /** + * The String result of the action. + */ + private String stringResult; + + /** + * The Object result of the action. + */ + private Object objectResult; + + /** + * The Object[] result of the action. + */ + private Object[] objectArrayResult; + + /** + * Creates a new DimIProxy. + */ + private DimIProxy(Dim dim, int type) { + this.dim = dim; + this.type = type; + } + + // ContextAction + + /** + * Performs the action given by {@link #type}. + */ + public Object run(Context cx) { + switch (type) { + case IPROXY_COMPILE_SCRIPT: + cx.compileString(text, url, 1, null); + break; + + case IPROXY_EVAL_SCRIPT: { + Scriptable scope = null; + if (dim.scopeProvider != null) { + scope = dim.scopeProvider.getScope(); + } + if (scope == null) { + scope = new ImporterTopLevel(cx); + } + cx.evaluateString(scope, text, url, 1, null); + } + break; + + case IPROXY_STRING_IS_COMPILABLE: + booleanResult = cx.stringIsCompilableUnit(text); + break; + + case IPROXY_OBJECT_TO_STRING: + if (object == Undefined.instance) { + stringResult = "undefined"; + } else if (object == null) { + stringResult = "null"; + } else if (object instanceof NativeCall) { + stringResult = "[object Call]"; + } else { + stringResult = Context.toString(object); + } + break; + + case IPROXY_OBJECT_PROPERTY: + objectResult = dim.getObjectPropertyImpl(cx, object, id); + break; + + case IPROXY_OBJECT_IDS: + objectArrayResult = dim.getObjectIdsImpl(cx, object); + break; + + default: + throw Kit.codeBug(); + } + return null; + } + + /** + * Performs the action given by {@link #type} with the attached + * {@link ContextFactory}. + */ + private void withContext() { + dim.contextFactory.call(this); + } + + @Override + public void onEngineCreate(ScriptEngine engine) { + if (type != IPROXY_LISTEN) Kit.codeBug(); + if (!(engine instanceof RhinoJavaScriptEngine) || + !callback.shouldAttachDebugger((RhinoJavaScriptEngine) engine)) { + return; + } + + Context cx = ((RhinoJavaScriptEngine) engine).getContext(); + ContextData contextData = new ContextData(); + Debugger debugger = new DimIProxy(dim, IPROXY_DEBUG); + cx.setDebugger(debugger, contextData); + cx.setGeneratingDebug(true); + cx.setOptimizationLevel(-1); + } + + @Override + public void onEngineRemove(ScriptEngine engine) { + if (type != IPROXY_LISTEN) Kit.codeBug(); + } + + // Debugger + + /** + * Returns a StackFrame for the given function or script. + */ + public DebugFrame getFrame(Context cx, DebuggableScript fnOrScript) { + if (type != IPROXY_DEBUG) Kit.codeBug(); + + FunctionSource item = dim.getFunctionSource(fnOrScript); + if (item == null) { + // Can not debug if source is not available + return null; + } + return new StackFrame(cx, dim, item); + } + + /** + * Called when compilation is finished. + */ + public void handleCompilationDone(Context cx, + DebuggableScript fnOrScript, + String source) { + if (type != IPROXY_DEBUG) Kit.codeBug(); + + if (!fnOrScript.isTopLevel()) { + return; + } + dim.registerTopScript(fnOrScript, source); + } + } + + /** + * Class to store information about a stack. + */ + public static class ContextData { + + /** + * The stack frames. + */ + private ObjArray frameStack = new ObjArray(); + + /** + * Whether the debugger should break at the next line in this context. + */ + private boolean breakNextLine; + + /** + * The frame depth the debugger should stop at. Used to implement + * "step over" and "step out". + */ + private int stopAtFrameDepth = -1; + + /** + * Whether this context is in the event thread. + */ + private boolean eventThreadFlag; + + /** + * The last exception that was processed. + */ + private Throwable lastProcessedException; + + /** + * Returns the ContextData for the given Context. + */ + public static ContextData get(Context cx) { + return (ContextData) cx.getDebuggerContextData(); + } + + /** + * Returns the number of stack frames. + */ + public int frameCount() { + return frameStack.size(); + } + + /** + * Returns the stack frame with the given index. + */ + public StackFrame getFrame(int frameNumber) { + int num = frameStack.size() - frameNumber - 1; + return (StackFrame) frameStack.get(num); + } + + /** + * Pushes a stack frame on to the stack. + */ + private void pushFrame(StackFrame frame) { + frameStack.push(frame); + } + + /** + * Pops a stack frame from the stack. + */ + private void popFrame() { + frameStack.pop(); + } + } + + /** + * Object to represent one stack frame. + */ + public static class StackFrame implements DebugFrame { + + /** + * The debugger. + */ + private Dim dim; + + /** + * The ContextData for the Context being debugged. + */ + private ContextData contextData; + + /** + * The scope. + */ + private Scriptable scope; + + /** + * The 'this' object. + */ + private Scriptable thisObj; + + /** + * Information about the function. + */ + private FunctionSource fsource; + + /** + * Array of breakpoint state for each source line. + */ + private boolean[] breakpoints; + + /** + * Current line number. + */ + private int lineNumber; + + /** + * Creates a new StackFrame. + */ + private StackFrame(Context cx, Dim dim, FunctionSource fsource) { + this.dim = dim; + this.contextData = ContextData.get(cx); + this.fsource = fsource; + this.breakpoints = fsource.sourceInfo().breakpoints; + this.lineNumber = fsource.firstLine(); + } + + /** + * Called when the stack frame is entered. + */ + public void onEnter(Context cx, Scriptable scope, + Scriptable thisObj, Object[] args) { + contextData.pushFrame(this); + this.scope = scope; + this.thisObj = thisObj; + if (dim.breakOnEnter) { + dim.handleBreakpointHit(this, cx); + } + } + + /** + * Called when the current position has changed. + */ + public void onLineChange(Context cx, int lineno) { + this.lineNumber = lineno; + + if (!breakpoints[lineno] && !dim.breakFlag) { + boolean lineBreak = contextData.breakNextLine; + if (lineBreak && contextData.stopAtFrameDepth >= 0) { + lineBreak = (contextData.frameCount() + <= contextData.stopAtFrameDepth); + } + if (!lineBreak) { + return; + } + contextData.stopAtFrameDepth = -1; + contextData.breakNextLine = false; + } + + dim.handleBreakpointHit(this, cx); + } + + /** + * Called when an exception has been thrown. + */ + public void onExceptionThrown(Context cx, Throwable exception) { + dim.handleExceptionThrown(cx, exception, this); + } + + /** + * Called when the stack frame has been left. + */ + public void onExit(Context cx, boolean byThrow, + Object resultOrException) { + if (dim.breakOnReturn && !byThrow) { + dim.handleBreakpointHit(this, cx); + } + contextData.popFrame(); + } + + /** + * Called when a 'debugger' statement is executed. + */ + public void onDebuggerStatement(Context cx) { + dim.handleBreakpointHit(this, cx); + } + + /** + * Returns the SourceInfo object for the function. + */ + public SourceInfo sourceInfo() { + return fsource.sourceInfo(); + } + + /** + * Returns the ContextData object for the Context. + */ + public ContextData contextData() { + return contextData; + } + + /** + * Returns the scope object for this frame. + */ + public Object scope() { + return scope; + } + + /** + * Returns the 'this' object for this frame. + */ + public Object thisObj() { + return thisObj; + } + + /** + * Returns the source URL. + */ + public String getUrl() { + return fsource.sourceInfo().url(); + } + + /** + * Returns the current line number. + */ + public int getLineNumber() { + return lineNumber; + } + + /** + * Returns the current function name. + */ + public String getFunctionName() { + return fsource.name(); + } + } + + /** + * Class to store information about a function. + */ + public static class FunctionSource { + + /** + * Information about the source of the function. + */ + private SourceInfo sourceInfo; + + /** + * Line number of the first line of the function. + */ + private int firstLine; + + /** + * The function name. + */ + private String name; + + /** + * Creates a new FunctionSource. + */ + private FunctionSource(SourceInfo sourceInfo, int firstLine, + String name) { + if (name == null) throw new IllegalArgumentException(); + this.sourceInfo = sourceInfo; + this.firstLine = firstLine; + this.name = name; + } + + /** + * Returns the SourceInfo object that describes the source of the + * function. + */ + public SourceInfo sourceInfo() { + return sourceInfo; + } + + /** + * Returns the line number of the first line of the function. + */ + public int firstLine() { + return firstLine; + } + + /** + * Returns the name of the function. + */ + public String name() { + return name; + } + } + + /** + * Class to store information about a script source. + */ + public static class SourceInfo { + + /** + * An empty array of booleans. + */ + private static final boolean[] EMPTY_BOOLEAN_ARRAY = new boolean[0]; + + /** + * The script. + */ + private String source; + + /** + * The URL of the script. + */ + private String url; + + /** + * Array indicating which lines can have breakpoints set. + */ + private boolean[] breakableLines; + + /** + * Array indicating whether a breakpoint is set on the line. + */ + private boolean[] breakpoints; + + /** + * Array of FunctionSource objects for the functions in the script. + */ + private FunctionSource[] functionSources; + + /** + * Creates a new SourceInfo object. + */ + private SourceInfo(String source, DebuggableScript[] functions, + String normilizedUrl) { + this.source = source; + this.url = normilizedUrl; + + int N = functions.length; + int[][] lineArrays = new int[N][]; + for (int i = 0; i != N; ++i) { + lineArrays[i] = functions[i].getLineNumbers(); + } + + int minAll = 0, maxAll = -1; + int[] firstLines = new int[N]; + for (int i = 0; i != N; ++i) { + int[] lines = lineArrays[i]; + if (lines == null || lines.length == 0) { + firstLines[i] = -1; + } else { + int min, max; + min = max = lines[0]; + for (int j = 1; j != lines.length; ++j) { + int line = lines[j]; + if (line < min) { + min = line; + } else if (line > max) { + max = line; + } + } + firstLines[i] = min; + if (minAll > maxAll) { + minAll = min; + maxAll = max; + } else { + if (min < minAll) { + minAll = min; + } + if (max > maxAll) { + maxAll = max; + } + } + } + } + + if (minAll > maxAll) { + // No line information + this.breakableLines = EMPTY_BOOLEAN_ARRAY; + this.breakpoints = EMPTY_BOOLEAN_ARRAY; + } else { + if (minAll < 0) { + // Line numbers can not be negative + throw new IllegalStateException(String.valueOf(minAll)); + } + int linesTop = maxAll + 1; + this.breakableLines = new boolean[linesTop]; + this.breakpoints = new boolean[linesTop]; + for (int i = 0; i != N; ++i) { + int[] lines = lineArrays[i]; + if (lines != null && lines.length != 0) { + for (int j = 0; j != lines.length; ++j) { + int line = lines[j]; + this.breakableLines[line] = true; + } + } + } + } + this.functionSources = new FunctionSource[N]; + for (int i = 0; i != N; ++i) { + String name = functions[i].getFunctionName(); + if (name == null) { + name = ""; + } + this.functionSources[i] + = new FunctionSource(this, firstLines[i], name); + } + } + + /** + * Returns the source text. + */ + public String source() { + return this.source; + } + + /** + * Returns the script's origin URL. + */ + public String url() { + return this.url; + } + + /** + * Returns the number of FunctionSource objects stored in this object. + */ + public int functionSourcesTop() { + return functionSources.length; + } + + /** + * Returns the FunctionSource object with the given index. + */ + public FunctionSource functionSource(int i) { + return functionSources[i]; + } + + /** + * Copies the breakpoints from the given SourceInfo object into this + * one. + */ + private void copyBreakpointsFrom(SourceInfo old) { + int end = old.breakpoints.length; + if (end > this.breakpoints.length) { + end = this.breakpoints.length; + } + for (int line = 0; line != end; ++line) { + if (old.breakpoints[line]) { + this.breakpoints[line] = true; + } + } + } + + /** + * Returns whether the given line number can have a breakpoint set on + * it. + */ + public boolean breakableLine(int line) { + return (line < this.breakableLines.length) + && this.breakableLines[line]; + } + + /** + * Returns whether there is a breakpoint set on the given line. + */ + public boolean breakpoint(int line) { + if (!breakableLine(line)) { + throw new IllegalArgumentException(String.valueOf(line)); + } + return line < this.breakpoints.length && this.breakpoints[line]; + } + + /** + * Sets or clears the breakpoint flag for the given line. + */ + public boolean breakpoint(int line, boolean value) { + if (!breakableLine(line)) { + throw new IllegalArgumentException(String.valueOf(line)); + } + boolean changed; + synchronized (breakpoints) { + if (breakpoints[line] != value) { + breakpoints[line] = value; + changed = true; + } else { + changed = false; + } + } + return changed; + } + + /** + * Removes all breakpoints from the script. + */ + public void removeAllBreakpoints() { + synchronized (breakpoints) { + for (int line = 0; line != breakpoints.length; ++line) { + breakpoints[line] = false; + } + } + } + } +} \ No newline at end of file