mirror of
https://gitee.com/zhangsken/AndroidIDE.git
synced 2026-10-16 15:56:48 +08:00
Updated editor
This commit is contained in:
Generated
+17
@@ -1,5 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ANTLRGenerationPreferences">
|
||||
<option name="perGrammarGenerationSettings">
|
||||
<list>
|
||||
<PerGrammarGenerationSettings>
|
||||
<option name="fileName" value="$PROJECT_DIR$/lexers/src/main/java/com/itsaky/androidide/lexers/xml/XMLParser.g4" />
|
||||
<option name="autoGen" value="true" />
|
||||
<option name="outputDir" value="D:\Projects\AndroidStudioProjects\AndroidIDE\lexers\src\main\java" />
|
||||
<option name="libDir" value="" />
|
||||
<option name="encoding" value="" />
|
||||
<option name="pkg" value="" />
|
||||
<option name="language" value="Java" />
|
||||
<option name="generateVisitor" value="true" />
|
||||
</PerGrammarGenerationSettings>
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="DesignSurface">
|
||||
<option name="filePathToZoomLevelMap">
|
||||
<map>
|
||||
@@ -24,6 +40,7 @@
|
||||
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/layout/fragment_editor.xml" value="0.20512820512820512" />
|
||||
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/layout/fragment_non_editable_editor.xml" value="0.20512820512820512" />
|
||||
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/layout/layout_about_items.xml" value="0.20512820512820512" />
|
||||
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/layout/layout_code_editor.xml" value="0.20185185185185187" />
|
||||
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/layout/layout_completion_item.xml" value="0.23854166666666668" />
|
||||
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/layout/layout_crash_report.xml" value="0.25" />
|
||||
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/layout/layout_diagnostic_info.xml" value="0.20512820512820512" />
|
||||
|
||||
@@ -113,6 +113,7 @@ dependencies {
|
||||
implementation 'com.google.firebase:firebase-messaging'
|
||||
|
||||
// Local projects here
|
||||
implementation project(':editor')
|
||||
implementation project(':treeview')
|
||||
implementation project(':emulatorview')
|
||||
implementation project(':xml-inflater')
|
||||
|
||||
Binary file not shown.
@@ -107,7 +107,8 @@ import com.itsaky.androidide.utils.Logger;
|
||||
import com.itsaky.androidide.utils.RecursiveFileSearcher;
|
||||
import com.itsaky.androidide.utils.Symbols;
|
||||
import com.itsaky.androidide.viewmodel.EditorViewModel;
|
||||
import com.itsaky.androidide.views.CodeEditorView;
|
||||
import com.itsaky.androidide.views.editor.CodeEditorView;
|
||||
import com.itsaky.androidide.views.editor.IDEEditor;
|
||||
import com.itsaky.androidide.views.MaterialBanner;
|
||||
import com.itsaky.androidide.views.SymbolInputView;
|
||||
import com.itsaky.inflater.ILayoutInflater;
|
||||
@@ -135,7 +136,6 @@ import java.util.concurrent.CompletionException;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import io.github.rosemoe.editor.widget.CodeEditor;
|
||||
import me.piruin.quickaction.ActionItem;
|
||||
import me.piruin.quickaction.QuickAction;
|
||||
|
||||
@@ -228,7 +228,6 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
|
||||
KeyboardUtils.registerSoftInputChangedListener (this, __ -> onSoftInputChanged ());
|
||||
registerLogReceiver ();
|
||||
setupContainers ();
|
||||
setupSignatureText ();
|
||||
setupDiagnosticInfo ();
|
||||
|
||||
mUIDesignerLauncher = registerForActivityResult (new ActivityResultContracts.StartActivityForResult (), this::onGetUIDesignerResult);
|
||||
@@ -671,7 +670,7 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
|
||||
openFile (file, range);
|
||||
final var opened = getEditorForFile (file);
|
||||
if (opened != null && opened.getEditor () != null) {
|
||||
CodeEditor editor = opened.getEditor ();
|
||||
IDEEditor editor = opened.getEditor ();
|
||||
editor.post (() -> {
|
||||
if (LSPUtils.isEqual (range.getStart (), range.getEnd ())) {
|
||||
editor.setSelection (range.getStart ().getLine (), range.getEnd ().getColumn ());
|
||||
@@ -1288,16 +1287,6 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
|
||||
return mViewModel.getIDEProject ();
|
||||
}
|
||||
|
||||
private void setupSignatureText () {
|
||||
GradientDrawable gd = new GradientDrawable ();
|
||||
gd.setShape (GradientDrawable.RECTANGLE);
|
||||
gd.setColor (0xff212121);
|
||||
gd.setStroke (1, 0xffffffff);
|
||||
gd.setCornerRadius (8);
|
||||
mBinding.symbolText.setBackground (gd);
|
||||
mBinding.symbolText.setVisibility (View.GONE);
|
||||
}
|
||||
|
||||
private void setupDiagnosticInfo () {
|
||||
GradientDrawable gd = new GradientDrawable ();
|
||||
gd.setShape (GradientDrawable.RECTANGLE);
|
||||
|
||||
@@ -190,7 +190,7 @@ public class MainActivity extends StudioActivity implements View.OnClickListener
|
||||
if (mProgressSheet == null) {
|
||||
createProgressSheet ();
|
||||
}
|
||||
|
||||
|
||||
if (mProgressSheet.isShowing ()) {
|
||||
mProgressSheet.dismiss ();
|
||||
}
|
||||
@@ -204,7 +204,7 @@ public class MainActivity extends StudioActivity implements View.OnClickListener
|
||||
if (mProgressSheet == null) {
|
||||
createProgressSheet ();
|
||||
}
|
||||
|
||||
|
||||
if (mProgressSheet.isShowing ()) {
|
||||
mProgressSheet.dismiss ();
|
||||
}
|
||||
@@ -267,10 +267,10 @@ public class MainActivity extends StudioActivity implements View.OnClickListener
|
||||
|
||||
ProjectTemplate
|
||||
empty = new ProjectTemplate ()
|
||||
.setId (0)
|
||||
.setName (this, R.string.template_empty)
|
||||
.setDescription (this, R.string.template_description_empty)
|
||||
.setImageId (R.drawable.template_empty),
|
||||
.setId (0)
|
||||
.setName (this, R.string.template_empty)
|
||||
.setDescription (this, R.string.template_description_empty)
|
||||
.setImageId (R.drawable.template_empty),
|
||||
basic = new ProjectTemplate ()
|
||||
.setId (1)
|
||||
.setName (this, R.string.template_basic)
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
/************************************************************************************
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
*
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
@@ -16,7 +14,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
**************************************************************************************/
|
||||
*/
|
||||
package com.itsaky.androidide.adapters;
|
||||
|
||||
import android.content.res.Resources;
|
||||
@@ -47,7 +45,7 @@ import com.itsaky.apiinfo.models.MethodInfo;
|
||||
import com.itsaky.lsp.models.CompletionItem;
|
||||
import com.itsaky.lsp.models.CompletionItemKind;
|
||||
|
||||
import io.github.rosemoe.editor.widget.EditorCompletionAdapter;
|
||||
import io.github.rosemoe.sora.widget.component.EditorCompletionAdapter;
|
||||
|
||||
public class CompletionListAdapter extends EditorCompletionAdapter {
|
||||
|
||||
@@ -61,10 +59,10 @@ public class CompletionListAdapter extends EditorCompletionAdapter {
|
||||
@Override
|
||||
protected View getView(int position, View convertView, ViewGroup parent, boolean isCurrentCursorPosition) {
|
||||
final LayoutCompletionItemBinding binding = LayoutCompletionItemBinding.inflate(LayoutInflater.from(getContext()), parent, false);
|
||||
|
||||
var item = getItem(position);
|
||||
|
||||
var item = (CompletionItem) getItem(position);
|
||||
String label = item.getLabel(), desc = item.getDetail(), type = item.getKind().toString();
|
||||
String header = type.length() <= 0 ? "O" : String.valueOf(type.toString().charAt(0));
|
||||
String header = type.length() <= 0 ? "O" : String.valueOf(type.charAt(0));
|
||||
|
||||
binding.completionIconText.setText(header);
|
||||
binding.completionLabel.setText(label);
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
/************************************************************************************
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
*
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
@@ -16,58 +14,58 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
**************************************************************************************/
|
||||
|
||||
|
||||
*/
|
||||
package com.itsaky.androidide.adapters;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.itsaky.androidide.databinding.LayoutSymbolItemBinding;
|
||||
import com.itsaky.androidide.views.editor.IDEEditor;
|
||||
import com.itsaky.androidide.views.SymbolInputView.Symbol;
|
||||
import io.github.rosemoe.editor.widget.CodeEditor;
|
||||
import io.github.rosemoe.editor.widget.SymbolChannel;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
public class SymbolInputAdapter extends RecyclerView.Adapter<SymbolInputAdapter.VH> {
|
||||
|
||||
private SymbolChannel channel;
|
||||
private final IDEEditor editor;
|
||||
private Symbol[] symbols;
|
||||
|
||||
public SymbolInputAdapter(CodeEditor editor) {
|
||||
public SymbolInputAdapter(IDEEditor editor) {
|
||||
this(editor, null);
|
||||
}
|
||||
|
||||
public SymbolInputAdapter(CodeEditor editor, Symbol[] symbols) {
|
||||
this.channel = editor.createNewSymbolChannel();
|
||||
public SymbolInputAdapter(IDEEditor editor, Symbol[] symbols) {
|
||||
this.editor = editor;
|
||||
this.symbols = symbols == null ? new Symbol[0] : symbols;
|
||||
}
|
||||
|
||||
public SymbolInputAdapter setSymbols(Symbol... symbols) {
|
||||
return setSymbols(false, symbols);
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
public SymbolInputAdapter setSymbols(boolean notify, Symbol... symbols) {
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
public void setSymbols(boolean notify, Symbol... symbols) {
|
||||
this.symbols = symbols;
|
||||
if(notify) {
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public VH onCreateViewHolder(@NonNull ViewGroup parent, int itemType) {
|
||||
return new VH (LayoutSymbolItemBinding.inflate (LayoutInflater.from (parent.getContext ()), parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public VH onCreateViewHolder(ViewGroup parent, int itemType) {
|
||||
return new VH(LayoutSymbolItemBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(VH holder, int position) {
|
||||
public void onBindViewHolder(@NonNull VH holder, int position) {
|
||||
if(symbols == null || symbols[position] == null) return;
|
||||
final Symbol symbol = symbols[position];
|
||||
holder.binding.symbol.setText(symbol.label);
|
||||
holder.binding.symbol.setOnClickListener(__ -> channel.insertSymbol(symbol.commit, symbol.offset));
|
||||
holder.binding.symbol.setOnClickListener(__ -> insertSymbol(symbol.commit, symbol.offset));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -75,7 +73,7 @@ public class SymbolInputAdapter extends RecyclerView.Adapter<SymbolInputAdapter.
|
||||
return symbols == null ? 0 : symbols.length;
|
||||
}
|
||||
|
||||
public class VH extends RecyclerView.ViewHolder {
|
||||
public static class VH extends RecyclerView.ViewHolder {
|
||||
LayoutSymbolItemBinding binding;
|
||||
|
||||
public VH(LayoutSymbolItemBinding binding) {
|
||||
@@ -83,4 +81,42 @@ public class SymbolInputAdapter extends RecyclerView.Adapter<SymbolInputAdapter.
|
||||
this.binding = binding;
|
||||
}
|
||||
}
|
||||
|
||||
void insertSymbol(String text, int selectionOffset) {
|
||||
if (selectionOffset < 0 || selectionOffset > text.length()) {
|
||||
return;
|
||||
}
|
||||
var cur = editor.getText().getCursor();
|
||||
if (cur.isSelected()) {
|
||||
editor.getText ().delete (cur.getLeftLine (),
|
||||
cur.getLeftColumn (),
|
||||
cur.getRightLine (),
|
||||
cur.getRightColumn ()
|
||||
);
|
||||
editor.notifyIMEExternalCursorChange ();
|
||||
}
|
||||
|
||||
if(cur.getLeftColumn() < editor.getText().getColumnCount(cur.getLeftLine())
|
||||
&& text.length() == 1
|
||||
&& text.charAt(0) == editor.getText().charAt(cur.getLeftLine(), cur.getLeftColumn())
|
||||
&& pairs.contains(text.charAt(0))) {
|
||||
editor.moveSelectionRight();
|
||||
} else {
|
||||
editor.commitText(text);
|
||||
if (selectionOffset != text.length()) {
|
||||
editor.setSelection(cur.getRightLine(), cur.getRightColumn() - (text.length() - selectionOffset));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final List<Character> pairs;
|
||||
static {
|
||||
pairs = new ArrayList<>();
|
||||
pairs.add('}');
|
||||
pairs.add(')');
|
||||
pairs.add(']');
|
||||
pairs.add('"');
|
||||
pairs.add('\'');
|
||||
pairs.add('>');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,13 +36,12 @@ import com.blankj.utilcode.util.FileIOUtils;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||
import com.itsaky.androidide.R;
|
||||
import com.itsaky.androidide.adapters.viewholders.FileTreeViewHolder;
|
||||
import com.itsaky.androidide.app.StudioApp;
|
||||
import com.itsaky.androidide.databinding.LayoutEditorFileTreeBinding;
|
||||
import com.itsaky.androidide.project.AndroidProject;
|
||||
import com.itsaky.androidide.tasks.TaskExecutor;
|
||||
import com.itsaky.androidide.tasks.callables.FileTreeCallable;
|
||||
import com.itsaky.androidide.utils.Environment;
|
||||
import com.itsaky.androidide.views.CodeEditorView;
|
||||
import com.itsaky.androidide.views.editor.CodeEditorView;
|
||||
import com.unnamed.b.atv.model.TreeNode;
|
||||
import com.unnamed.b.atv.view.AndroidTreeView;
|
||||
import java.io.File;
|
||||
|
||||
@@ -31,22 +31,22 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import io.github.rosemoe.editor.interfaces.EditorLanguage;
|
||||
import io.github.rosemoe.sora.lang.Language;
|
||||
|
||||
public class LogViewFragment extends NonEditableEditorFragment {
|
||||
|
||||
private final EditorLanguage language = getLanguage ();
|
||||
private final Language language = getLanguage ();
|
||||
private final List<LogLine> unsavedLines = new ArrayList<> ();
|
||||
|
||||
@Override
|
||||
public void onViewCreated (@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated (view, savedInstanceState);
|
||||
Objects.requireNonNull (getEditor ()).setEditorLanguage (language, null);
|
||||
Objects.requireNonNull (getEditor ()).setEditorLanguage (language);
|
||||
|
||||
if (!unsavedLines.isEmpty ()) {
|
||||
for (var line : unsavedLines) {
|
||||
applyToLanguage (line);
|
||||
getEditor ().getText ().append (line.toString ().trim () + "\n");
|
||||
getEditor ().append (line.toString ().trim () + "\n");
|
||||
}
|
||||
unsavedLines.clear ();
|
||||
}
|
||||
@@ -62,7 +62,7 @@ public class LogViewFragment extends NonEditableEditorFragment {
|
||||
|
||||
final var lineString = line.toString ();
|
||||
final var msg = lineString.endsWith ("\n") ? lineString : lineString + "\n";
|
||||
ThreadUtils.runOnUiThread (() -> getEditor ().getText ().append (msg));
|
||||
ThreadUtils.runOnUiThread (() -> getEditor ().append (msg));
|
||||
}
|
||||
|
||||
private void applyToLanguage (LogLine line) {
|
||||
@@ -71,7 +71,7 @@ public class LogViewFragment extends NonEditableEditorFragment {
|
||||
}
|
||||
}
|
||||
|
||||
protected EditorLanguage getLanguage () {
|
||||
protected Language getLanguage () {
|
||||
return new LogLanguageImpl ();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,9 +29,9 @@ import androidx.fragment.app.Fragment;
|
||||
import com.itsaky.androidide.databinding.FragmentNonEditableEditorBinding;
|
||||
import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE;
|
||||
import com.itsaky.androidide.utils.TypefaceUtils;
|
||||
import com.itsaky.androidide.views.editor.IDEEditor;
|
||||
|
||||
import io.github.rosemoe.editor.langs.EmptyLanguage;
|
||||
import io.github.rosemoe.editor.widget.CodeEditor;
|
||||
import io.github.rosemoe.sora.lang.EmptyLanguage;
|
||||
|
||||
public abstract class NonEditableEditorFragment extends Fragment {
|
||||
|
||||
@@ -51,9 +51,7 @@ public abstract class NonEditableEditorFragment extends Fragment {
|
||||
final var editor = binding.getRoot ();
|
||||
editor.setEditable (false);
|
||||
editor.setDividerWidth (0);
|
||||
editor.setEditorLanguage (new EmptyLanguage (), null);
|
||||
editor.setOverScrollEnabled (false);
|
||||
editor.setTextActionMode (CodeEditor.TextActionMode.ACTION_MODE);
|
||||
editor.setEditorLanguage (new EmptyLanguage ());
|
||||
editor.setWordwrap (false);
|
||||
editor.setUndoEnabled (false);
|
||||
editor.setTypefaceLineNumber (TypefaceUtils.jetbrainsMono ());
|
||||
@@ -63,7 +61,7 @@ public abstract class NonEditableEditorFragment extends Fragment {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public CodeEditor getEditor () {
|
||||
public IDEEditor getEditor () {
|
||||
if (binding == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ public class SimpleOutputFragment extends NonEditableEditorFragment {
|
||||
|
||||
if (!unsavedLines.isEmpty ()) {
|
||||
for (String line : unsavedLines) {
|
||||
Objects.requireNonNull (getEditor ()).getText ().append (line.trim () + "\n");
|
||||
Objects.requireNonNull (getEditor ()).append (line.trim () + "\n");
|
||||
}
|
||||
unsavedLines.clear ();
|
||||
}
|
||||
@@ -52,7 +52,7 @@ public class SimpleOutputFragment extends NonEditableEditorFragment {
|
||||
}
|
||||
ThreadUtils.runOnUiThread (() -> {
|
||||
final var message = output == null || output.endsWith ("\n") ? output : output + "\n";
|
||||
getEditor ().getText ().append (message);
|
||||
getEditor ().append (message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
/************************************************************************************
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
*
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
@@ -15,28 +13,28 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
**************************************************************************************/
|
||||
|
||||
|
||||
*/
|
||||
package com.itsaky.androidide.fragments.sheets;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.itsaky.androidide.R;
|
||||
import com.itsaky.androidide.utils.TypefaceUtils;
|
||||
import io.github.rosemoe.editor.langs.EmptyLanguage;
|
||||
import io.github.rosemoe.editor.widget.CodeEditor;
|
||||
import com.itsaky.androidide.views.editor.IDEEditor;
|
||||
|
||||
import io.github.rosemoe.sora.lang.EmptyLanguage;
|
||||
|
||||
public class TextSheetFragment extends BaseBottomSheetFragment {
|
||||
|
||||
|
||||
private SpannableStringBuilder outputBuilder;
|
||||
private CodeEditor editor;
|
||||
private IDEEditor editor;
|
||||
private boolean textSelectable;
|
||||
|
||||
public TextSheetFragment setTextSelectable(boolean textSelectable) {
|
||||
@@ -45,8 +43,8 @@ public class TextSheetFragment extends BaseBottomSheetFragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void bind(LinearLayout container) {
|
||||
final CodeEditor e = getEditor();
|
||||
protected void bind(@NonNull LinearLayout container) {
|
||||
final IDEEditor e = getEditor();
|
||||
container.setPadding(0, 0, 0, 0);
|
||||
container.setPaddingRelative(0, 0, 0, 0);
|
||||
LinearLayout.LayoutParams p = new LinearLayout.LayoutParams(-1, 0);
|
||||
@@ -67,13 +65,10 @@ public class TextSheetFragment extends BaseBottomSheetFragment {
|
||||
super.onShow();
|
||||
editor.setText(getOutputBuilder());
|
||||
}
|
||||
|
||||
private Void scrollToBottom() {
|
||||
|
||||
private void scrollToBottom() {
|
||||
if(getActivity() != null)
|
||||
getActivity().runOnUiThread(() -> {
|
||||
editor.goToEnd();
|
||||
});
|
||||
return null;
|
||||
getActivity().runOnUiThread(() -> editor.goToEnd());
|
||||
}
|
||||
|
||||
public TextSheetFragment setTitleText(int res) {
|
||||
@@ -108,22 +103,20 @@ public class TextSheetFragment extends BaseBottomSheetFragment {
|
||||
private void setToEditor(SpannableString line) {
|
||||
if(getActivity() != null)
|
||||
getActivity().runOnUiThread(() -> {
|
||||
editor.getText().append(line);
|
||||
editor.append(line);
|
||||
scrollToBottom();
|
||||
});
|
||||
}
|
||||
|
||||
private CodeEditor getEditor() {
|
||||
private IDEEditor getEditor() {
|
||||
return editor == null ? createEditor() : editor;
|
||||
}
|
||||
|
||||
private CodeEditor createEditor() {
|
||||
editor = new CodeEditor(getContext());
|
||||
private IDEEditor createEditor() {
|
||||
editor = new IDEEditor (getContext());
|
||||
editor.setEditable(false);
|
||||
editor.setDividerWidth(0);
|
||||
editor.setEditorLanguage(new EmptyLanguage(), null);
|
||||
editor.setOverScrollEnabled(false);
|
||||
editor.setTextActionMode(CodeEditor.TextActionMode.ACTION_MODE);
|
||||
editor.setEditorLanguage(new EmptyLanguage ());
|
||||
editor.setWordwrap(false);
|
||||
editor.setUndoEnabled(false);
|
||||
editor.setTypefaceLineNumber(TypefaceUtils.jetbrainsMono());
|
||||
|
||||
@@ -21,9 +21,9 @@ package com.itsaky.androidide.language;
|
||||
|
||||
import com.itsaky.androidide.app.StudioApp;
|
||||
|
||||
import io.github.rosemoe.editor.interfaces.EditorLanguage;
|
||||
import io.github.rosemoe.sora.lang.Language;
|
||||
|
||||
public abstract class BaseLanguage implements EditorLanguage {
|
||||
public abstract class BaseLanguage implements Language {
|
||||
|
||||
@Override
|
||||
public boolean useTab() {
|
||||
|
||||
@@ -29,17 +29,18 @@ import com.itsaky.lsp.models.Position;
|
||||
|
||||
import org.jetbrains.annotations.Contract;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import io.github.rosemoe.editor.interfaces.AutoCompleteProvider;
|
||||
import io.github.rosemoe.editor.text.Content;
|
||||
import io.github.rosemoe.editor.text.TextAnalyzeResult;
|
||||
import io.github.rosemoe.sora.lang.completion.CompletionHelper;
|
||||
import io.github.rosemoe.sora.text.CharPosition;
|
||||
import io.github.rosemoe.sora.text.ContentReference;
|
||||
import io.github.rosemoe.sora.util.MyCharacter;
|
||||
|
||||
/**
|
||||
* Common implementation of completion provider which requests completions
|
||||
@@ -47,7 +48,7 @@ import io.github.rosemoe.editor.text.TextAnalyzeResult;
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
public class CommonCompletionProvider implements AutoCompleteProvider {
|
||||
public class CommonCompletionProvider {
|
||||
|
||||
private CompletableFuture<CompletionResult> future;
|
||||
|
||||
@@ -57,8 +58,7 @@ public class CommonCompletionProvider implements AutoCompleteProvider {
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CompletionItem> getAutoCompleteItems(Content content, String fileUri, String prefix, boolean isInCodeBlock, TextAnalyzeResult colors, int index, int line, int column) throws Exception {
|
||||
public List<CompletionItem> complete(ContentReference content, Path file, CharPosition position) {
|
||||
if (this.future != null && !this.future.isDone ()) {
|
||||
try {
|
||||
this.future.cancel (true);
|
||||
@@ -68,20 +68,29 @@ public class CommonCompletionProvider implements AutoCompleteProvider {
|
||||
}
|
||||
|
||||
this.future = CompletableFuture.supplyAsync (() -> {
|
||||
final var prefix = CompletionHelper.computePrefix (content, position, this::checkCompletionChar);
|
||||
final var completer = server.getCompletionProvider ();
|
||||
final var path = Paths.get (URI.create (fileUri));
|
||||
|
||||
if (!completer.canComplete (path)) {
|
||||
if (!completer.canComplete (file)) {
|
||||
return ICompletionProvider.EMPTY;
|
||||
}
|
||||
|
||||
final var params = new CompletionParams (new Position (line, column, index), path);
|
||||
final var params = new CompletionParams (new Position (position.line, position.column, position.index), file);
|
||||
params.setContent (content);
|
||||
params.setPrefix (prefix);
|
||||
return completer.complete (params);
|
||||
});
|
||||
|
||||
return finalizeResults (future.get ().getItems ());
|
||||
|
||||
try {
|
||||
return finalizeResults (future.get ().getItems ());
|
||||
} catch (Throwable e) {
|
||||
LOG.error ("Unable to compute completions", e);
|
||||
return Collections.emptyList ();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkCompletionChar (char c) {
|
||||
return MyCharacter.isJavaIdentifierPart (c) || c == '.';
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
||||
@@ -18,53 +18,71 @@
|
||||
|
||||
package com.itsaky.androidide.language.groovy;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import static com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.ANNOTATION;
|
||||
import static com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.LITERAL;
|
||||
import static com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.OPERATOR;
|
||||
import static com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.TEXT_NORMAL;
|
||||
import static com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.TYPE_NAME;
|
||||
import static com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.forComment;
|
||||
import static com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.forKeyword;
|
||||
import static com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.forString;
|
||||
import static com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.get;
|
||||
|
||||
import com.itsaky.androidide.lexers.groovy.GroovyLexer;
|
||||
import com.itsaky.lsp.api.ILanguageServer;
|
||||
import com.itsaky.androidide.utils.CharSequenceReader;
|
||||
import com.itsaky.androidide.utils.Logger;
|
||||
|
||||
import org.antlr.v4.runtime.CharStreams;
|
||||
import org.antlr.v4.runtime.CodePointCharStream;
|
||||
import org.antlr.v4.runtime.Token;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.util.Stack;
|
||||
|
||||
import io.github.rosemoe.editor.struct.BlockLine;
|
||||
import io.github.rosemoe.editor.text.Content;
|
||||
import io.github.rosemoe.editor.text.TextAnalyzeResult;
|
||||
import io.github.rosemoe.editor.text.TextAnalyzer;
|
||||
import io.github.rosemoe.editor.syntax.EditorColorScheme;
|
||||
import io.github.rosemoe.sora.lang.analysis.SimpleAnalyzeManager;
|
||||
import io.github.rosemoe.sora.lang.styling.CodeBlock;
|
||||
import io.github.rosemoe.sora.lang.styling.MappedSpans;
|
||||
import io.github.rosemoe.sora.lang.styling.Styles;
|
||||
|
||||
public class GroovyAnalyzer extends io.github.rosemoe.editor.langs.AbstractCodeAnalyzer {
|
||||
|
||||
@Override
|
||||
public void analyze(ILanguageServer server, File file, @NonNull Content content, TextAnalyzeResult colors, @NonNull TextAnalyzer.AnalyzeThread.Delegate delegate) throws IOException {
|
||||
final var stream = CharStreams.fromReader (new StringReader (content.toString ()));
|
||||
final var lexer = new GroovyLexer (stream);
|
||||
final var stack = new Stack<BlockLine> ();
|
||||
public class GroovyAnalyzer extends SimpleAnalyzeManager<Void> {
|
||||
|
||||
private static final Logger LOG = Logger.instance ("GroovyAnalyzer");
|
||||
|
||||
@Override
|
||||
protected Styles analyze (StringBuilder text, Delegate<Void> delegate) {
|
||||
final var styles = new Styles ();
|
||||
final var colors = new MappedSpans.Builder ();
|
||||
final CodePointCharStream stream;
|
||||
try {
|
||||
stream = CharStreams.fromReader (new CharSequenceReader (text));
|
||||
} catch (IOException e) {
|
||||
LOG.error ("Unable to read text to analyze", e);
|
||||
return styles;
|
||||
}
|
||||
|
||||
final var lexer = new GroovyLexer (stream);
|
||||
final var stack = new Stack<CodeBlock> ();
|
||||
|
||||
Token token;
|
||||
int line, column, lastLine = 1, currSwitch = 0, maxSwitch = 0, type, previous = 0;
|
||||
boolean isFirst = true;
|
||||
|
||||
while (delegate.shouldAnalyze()) {
|
||||
token = lexer.nextToken ();
|
||||
|
||||
while (!delegate.isCancelled ()) {
|
||||
token = lexer.nextToken ();
|
||||
|
||||
if (token == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
line = token.getLine () - 1;
|
||||
column = token.getCharPositionInLine ();
|
||||
type = token.getType ();
|
||||
|
||||
|
||||
if (type == GroovyLexer.EOF) {
|
||||
lastLine = line;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
switch(type) {
|
||||
case GroovyLexer.WS :
|
||||
if(isFirst) {
|
||||
@@ -112,7 +130,7 @@ public class GroovyAnalyzer extends io.github.rosemoe.editor.langs.AbstractCodeA
|
||||
case GroovyLexer.VOID:
|
||||
case GroovyLexer.VOLATILE:
|
||||
case GroovyLexer.WHILE:
|
||||
colors.addIfNeeded(line, column, EditorColorScheme.KEYWORD);
|
||||
colors.addIfNeeded(line, column, forKeyword ());
|
||||
break;
|
||||
case GroovyLexer.DECIMAL_LITERAL:
|
||||
case GroovyLexer.HEX_LITERAL:
|
||||
@@ -123,11 +141,11 @@ public class GroovyAnalyzer extends io.github.rosemoe.editor.langs.AbstractCodeA
|
||||
case GroovyLexer.BOOL_LITERAL:
|
||||
case GroovyLexer.CHAR_LITERAL:
|
||||
case GroovyLexer.NULL_LITERAL:
|
||||
colors.addIfNeeded(line, column, EditorColorScheme.LITERAL);
|
||||
colors.addIfNeeded(line, column, get (LITERAL));
|
||||
break;
|
||||
case GroovyLexer.STRING_LITERAL :
|
||||
case GroovyLexer.SINGLE_QUOTE_STRING :
|
||||
checkAndAddHexString (token, EditorColorScheme.LITERAL, colors);
|
||||
colors.addIfNeeded (line, column, forString ());
|
||||
break;
|
||||
case GroovyLexer.LPAREN :
|
||||
case GroovyLexer.RPAREN :
|
||||
@@ -173,7 +191,7 @@ public class GroovyAnalyzer extends io.github.rosemoe.editor.langs.AbstractCodeA
|
||||
case GroovyLexer.COLONCOLON :
|
||||
case GroovyLexer.ELLIPSIS :
|
||||
case GroovyLexer.DOT :
|
||||
colors.addIfNeeded(line, column, EditorColorScheme.OPERATOR);
|
||||
colors.addIfNeeded(line, column, get (OPERATOR));
|
||||
break;
|
||||
case GroovyLexer.BOOLEAN:
|
||||
case GroovyLexer.BYTE :
|
||||
@@ -184,62 +202,65 @@ public class GroovyAnalyzer extends io.github.rosemoe.editor.langs.AbstractCodeA
|
||||
case GroovyLexer.INT :
|
||||
case GroovyLexer.LONG:
|
||||
case GroovyLexer.SHORT:
|
||||
colors.addIfNeeded(line, column, EditorColorScheme.TYPE_NAME);
|
||||
colors.addIfNeeded(line, column, get (TYPE_NAME));
|
||||
break;
|
||||
case GroovyLexer.COMMENT :
|
||||
case GroovyLexer.LINE_COMMENT :
|
||||
colors.addIfNeeded(line, column, EditorColorScheme.COMMENT);
|
||||
colors.addIfNeeded(line, column, forComment ());
|
||||
break;
|
||||
case GroovyLexer.AT :
|
||||
colors.addIfNeeded(line, column, EditorColorScheme.ANNOTATION);
|
||||
colors.addIfNeeded(line, column, get (ANNOTATION));
|
||||
break;
|
||||
case GroovyLexer.IDENTIFIER :
|
||||
if (previous == GroovyLexer.AT) {
|
||||
colors.addIfNeeded(line, column, EditorColorScheme.ANNOTATION);
|
||||
colors.addIfNeeded(line, column, get (ANNOTATION));
|
||||
break;
|
||||
}
|
||||
|
||||
colors.addIfNeeded(line, column, EditorColorScheme.TEXT_NORMAL);
|
||||
|
||||
colors.addIfNeeded(line, column, get (TEXT_NORMAL));
|
||||
break;
|
||||
case GroovyLexer.LBRACE :
|
||||
colors.addIfNeeded(line, column, EditorColorScheme.OPERATOR);
|
||||
colors.addIfNeeded(line, column, get(OPERATOR));
|
||||
if (stack.isEmpty()) {
|
||||
if (currSwitch > maxSwitch)
|
||||
maxSwitch = currSwitch;
|
||||
currSwitch = 0;
|
||||
}
|
||||
currSwitch++;
|
||||
BlockLine block = colors.obtainNewBlock();
|
||||
var block = styles.obtainNewBlock ();
|
||||
block.startLine = line;
|
||||
block.startColumn = column;
|
||||
stack.push(block);
|
||||
break;
|
||||
case GroovyLexer.RBRACE :
|
||||
colors.addIfNeeded(line, column, EditorColorScheme.OPERATOR);
|
||||
colors.addIfNeeded(line, column, get (OPERATOR));
|
||||
if (!stack.isEmpty()) {
|
||||
BlockLine block2 = stack.pop();
|
||||
var block2 = stack.pop();
|
||||
block2.endLine = line;
|
||||
block2.endColumn = column;
|
||||
if (block2.startLine != block2.endLine)
|
||||
colors.addBlockLine(block2);
|
||||
styles.addCodeBlock (block2);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
colors.addIfNeeded(line, column, EditorColorScheme.TEXT_NORMAL);
|
||||
colors.addIfNeeded(line, column, get (TEXT_NORMAL));
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
isFirst = false;
|
||||
if (type != GroovyLexer.WS) {
|
||||
previous = type;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (stack.isEmpty() && currSwitch > maxSwitch) {
|
||||
maxSwitch = currSwitch;
|
||||
}
|
||||
|
||||
|
||||
colors.determine(lastLine);
|
||||
colors.setSuppressSwitch(maxSwitch + 10);
|
||||
}
|
||||
styles.setSuppressSwitch(maxSwitch + 10);
|
||||
styles.spans = colors.build ();
|
||||
|
||||
return styles;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,50 +17,50 @@
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
**************************************************************************************/
|
||||
|
||||
|
||||
package com.itsaky.androidide.language.groovy;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.itsaky.lsp.models.CompletionItem;
|
||||
import com.itsaky.lsp.models.CompletionItemKind;
|
||||
import com.itsaky.lsp.models.InsertTextFormat;
|
||||
|
||||
import io.github.rosemoe.editor.interfaces.AutoCompleteProvider;
|
||||
import io.github.rosemoe.editor.text.Content;
|
||||
import io.github.rosemoe.editor.text.TextAnalyzeResult;
|
||||
import io.github.rosemoe.sora.lang.completion.CompletionHelper;
|
||||
import io.github.rosemoe.sora.lang.completion.CompletionPublisher;
|
||||
import io.github.rosemoe.sora.text.CharPosition;
|
||||
import io.github.rosemoe.sora.text.ContentReference;
|
||||
import io.github.rosemoe.sora.util.MyCharacter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class GroovyAutoComplete implements AutoCompleteProvider {
|
||||
public class GroovyAutoComplete {
|
||||
|
||||
private static final List<String> ANDROIDX_ARTIFACTS = createArtifacts();
|
||||
private static final List<String> CONFIGURATIONS = createConfigs();
|
||||
private static final List<String> OTHERS = createOtherCompletions();
|
||||
|
||||
@Override
|
||||
public List<CompletionItem> getAutoCompleteItems(Content content, String fileUri, String prefix, boolean isInCodeBlock, TextAnalyzeResult colors, int index, int line, int column) throws Exception {
|
||||
List<CompletionItem> result = new ArrayList<>();
|
||||
|
||||
for(String artifact : ANDROIDX_ARTIFACTS) {
|
||||
if(!artifact.toLowerCase(Locale.US).startsWith(prefix.toLowerCase(Locale.US))) continue;
|
||||
result.add(createCompletionItem(artifact, prefix, true));
|
||||
}
|
||||
|
||||
for(String config : CONFIGURATIONS) {
|
||||
if(!config.toLowerCase(Locale.US).startsWith(prefix.toLowerCase(Locale.US))) continue;
|
||||
result.add(createCompletionItem(config, prefix, false));
|
||||
}
|
||||
|
||||
for(String other : OTHERS) {
|
||||
if(!other.toLowerCase(Locale.US).startsWith(prefix.toLowerCase(Locale.US))) continue;
|
||||
result.add(createCompletionItem(other, prefix, false));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public void complete (ContentReference content, CharPosition position, CompletionPublisher publisher, Bundle extraArguments) {
|
||||
publisher.setUpdateThreshold (0);
|
||||
final var prefix = CompletionHelper.computePrefix (content, position, MyCharacter::isJavaIdentifierPart);
|
||||
for(String artifact : ANDROIDX_ARTIFACTS) {
|
||||
if(!artifact.toLowerCase(Locale.ROOT).startsWith(prefix.toLowerCase(Locale.ROOT))) continue;
|
||||
publisher.addItem (createCompletionItem(artifact));
|
||||
}
|
||||
|
||||
for(String config : CONFIGURATIONS) {
|
||||
if(!config.toLowerCase(Locale.ROOT).startsWith(prefix.toLowerCase(Locale.ROOT))) continue;
|
||||
publisher.addItem (createCompletionItem(config));
|
||||
}
|
||||
|
||||
for(String other : OTHERS) {
|
||||
if(!other.toLowerCase(Locale.ROOT).startsWith(prefix.toLowerCase(Locale.ROOT))) continue;
|
||||
publisher.addItem (createCompletionItem(other));
|
||||
}
|
||||
}
|
||||
|
||||
private CompletionItem createCompletionItem(String itemLabel, String prefix, boolean artifact) {
|
||||
private CompletionItem createCompletionItem (String itemLabel) {
|
||||
CompletionItem item = new CompletionItem();
|
||||
item.setLabel(itemLabel);
|
||||
item.setDetail("");
|
||||
@@ -72,7 +72,7 @@ public class GroovyAutoComplete implements AutoCompleteProvider {
|
||||
}
|
||||
|
||||
private static List<String> createArtifacts() {
|
||||
List<String> artifacts = new ArrayList<String>();
|
||||
var artifacts = new ArrayList<String>();
|
||||
artifacts.add("androidx.arch.core:core-common");
|
||||
artifacts.add("androidx.arch.core:core");
|
||||
artifacts.add("androidx.arch.core:core-testing");
|
||||
@@ -182,7 +182,7 @@ public class GroovyAutoComplete implements AutoCompleteProvider {
|
||||
}
|
||||
|
||||
private static List<String> createConfigs() {
|
||||
List<String> configs = new ArrayList<String>();
|
||||
var configs = new ArrayList<String>();
|
||||
configs.add("androidApis");
|
||||
configs.add("androidTestAnnotationProcessor");
|
||||
configs.add("androidTestApi");
|
||||
@@ -268,112 +268,111 @@ public class GroovyAutoComplete implements AutoCompleteProvider {
|
||||
}
|
||||
|
||||
private static List<String> createOtherCompletions() {
|
||||
List<String> others = new ArrayList<String>();
|
||||
|
||||
var others = new ArrayList<String> ();
|
||||
|
||||
// Common words that user may use
|
||||
others.add("android");
|
||||
others.add("applicationVariants");
|
||||
others.add("compileSdkVersion");
|
||||
others.add("buildToolsVersion");
|
||||
others.add("defaultConfig");
|
||||
others.add("applicationId");
|
||||
others.add("minSdkVersion");
|
||||
others.add("targetSdkVersion");
|
||||
others.add("versionCode");
|
||||
others.add("versionName");
|
||||
others.add("multiDexEnabled");
|
||||
others.add("vectorDrawables.useSupportLibrary");
|
||||
others.add("compileOptions");
|
||||
others.add("coreLibraryDesugaringEnabled");
|
||||
others.add("sourceCompatibility");
|
||||
others.add("targetCompatibility");
|
||||
others.add("JavaVersion.VERSION_1_8");
|
||||
others.add("JavaVersion.VERSION_1_7");
|
||||
|
||||
others.add ("android");
|
||||
others.add ("applicationVariants");
|
||||
others.add ("compileSdkVersion");
|
||||
others.add ("buildToolsVersion");
|
||||
others.add ("defaultConfig");
|
||||
others.add ("applicationId");
|
||||
others.add ("minSdkVersion");
|
||||
others.add ("targetSdkVersion");
|
||||
others.add ("versionCode");
|
||||
others.add ("versionName");
|
||||
others.add ("multiDexEnabled");
|
||||
others.add ("vectorDrawables.useSupportLibrary");
|
||||
others.add ("compileOptions");
|
||||
others.add ("coreLibraryDesugaringEnabled");
|
||||
others.add ("sourceCompatibility");
|
||||
others.add ("targetCompatibility");
|
||||
others.add ("JavaVersion.VERSION_1_8");
|
||||
others.add ("JavaVersion.VERSION_1_7");
|
||||
|
||||
// Lint options
|
||||
others.add("lintOptions");
|
||||
others.add("quiet");
|
||||
others.add("abortOnError");
|
||||
others.add("checkReleaseBuilds");
|
||||
others.add("ignoreWarnings");
|
||||
others.add("checkAllWarnings");
|
||||
others.add("warningsAsErrors");
|
||||
others.add("disable");
|
||||
others.add("enable");
|
||||
others.add("check");
|
||||
others.add("noLines");
|
||||
others.add("showAll");
|
||||
others.add("explainIssues");
|
||||
others.add("lintConfig");
|
||||
others.add("textReport");
|
||||
others.add("textOutput");
|
||||
others.add("xmlReport");
|
||||
others.add("xmlOutput");
|
||||
others.add("htmlReport");
|
||||
others.add("htmlOutput");
|
||||
others.add("fatal");
|
||||
others.add("error");
|
||||
others.add("warning");
|
||||
others.add("ignore");
|
||||
others.add("informational");
|
||||
others.add("baseline");
|
||||
others.add("checkTestSources");
|
||||
others.add("ignoreTestSources");
|
||||
others.add("checkGeneratedSources");
|
||||
others.add("checkDependencies");
|
||||
|
||||
others.add ("lintOptions");
|
||||
others.add ("quiet");
|
||||
others.add ("abortOnError");
|
||||
others.add ("checkReleaseBuilds");
|
||||
others.add ("ignoreWarnings");
|
||||
others.add ("checkAllWarnings");
|
||||
others.add ("warningsAsErrors");
|
||||
others.add ("disable");
|
||||
others.add ("enable");
|
||||
others.add ("check");
|
||||
others.add ("noLines");
|
||||
others.add ("showAll");
|
||||
others.add ("explainIssues");
|
||||
others.add ("lintConfig");
|
||||
others.add ("textReport");
|
||||
others.add ("textOutput");
|
||||
others.add ("xmlReport");
|
||||
others.add ("xmlOutput");
|
||||
others.add ("htmlReport");
|
||||
others.add ("htmlOutput");
|
||||
others.add ("fatal");
|
||||
others.add ("error");
|
||||
others.add ("warning");
|
||||
others.add ("ignore");
|
||||
others.add ("informational");
|
||||
others.add ("baseline");
|
||||
others.add ("checkTestSources");
|
||||
others.add ("ignoreTestSources");
|
||||
others.add ("checkGeneratedSources");
|
||||
others.add ("checkDependencies");
|
||||
|
||||
// buildFeatures options
|
||||
others.add("buildFeatures");
|
||||
others.add("aidl");
|
||||
others.add("buildConfig");
|
||||
others.add("compose");
|
||||
others.add("prefab");
|
||||
others.add("renderScript");
|
||||
others.add("resValues");
|
||||
others.add("shaders");
|
||||
others.add("viewBinding");
|
||||
others.add ("buildFeatures");
|
||||
others.add ("aidl");
|
||||
others.add ("buildConfig");
|
||||
others.add ("compose");
|
||||
others.add ("prefab");
|
||||
others.add ("renderScript");
|
||||
others.add ("resValues");
|
||||
others.add ("shaders");
|
||||
others.add ("viewBinding");
|
||||
|
||||
// Signing configs and all
|
||||
others.add("signingConfigs");
|
||||
others.add("debug");
|
||||
others.add("storeFile");
|
||||
others.add("storePassword");
|
||||
others.add("keyAlias");
|
||||
others.add("keyPassword");
|
||||
others.add("release");
|
||||
others.add("storeFile");
|
||||
others.add("storePassword");
|
||||
others.add("keyAlias");
|
||||
others.add("keyPassword");
|
||||
others.add("buildTypes");
|
||||
others.add("debug");
|
||||
others.add("minifyEnabled");
|
||||
others.add("proguardFiles");
|
||||
others.add("signingConfig");
|
||||
others.add("stringfog.enable");
|
||||
others.add("release");
|
||||
others.add("minifyEnabled");
|
||||
others.add("proguardFiles");
|
||||
others.add("signingConfig");
|
||||
others.add("stringfog.enable");
|
||||
others.add("packagingOptions");
|
||||
others.add("exclude");
|
||||
others.add ("signingConfigs");
|
||||
others.add ("debug");
|
||||
others.add ("storeFile");
|
||||
others.add ("storePassword");
|
||||
others.add ("keyAlias");
|
||||
others.add ("keyPassword");
|
||||
others.add ("release");
|
||||
others.add ("storeFile");
|
||||
others.add ("storePassword");
|
||||
others.add ("keyAlias");
|
||||
others.add ("keyPassword");
|
||||
others.add ("buildTypes");
|
||||
others.add ("debug");
|
||||
others.add ("minifyEnabled");
|
||||
others.add ("proguardFiles");
|
||||
others.add ("signingConfig");
|
||||
others.add ("stringfog.enable");
|
||||
others.add ("release");
|
||||
others.add ("minifyEnabled");
|
||||
others.add ("proguardFiles");
|
||||
others.add ("signingConfig");
|
||||
others.add ("stringfog.enable");
|
||||
others.add ("packagingOptions");
|
||||
others.add ("exclude");
|
||||
|
||||
// Maybe used in root project's build.gradle
|
||||
others.add("buildscript");
|
||||
others.add("ext");
|
||||
others.add("project.ext");
|
||||
others.add("repositories");
|
||||
others.add("maven");
|
||||
others.add("url");
|
||||
others.add("google()");
|
||||
others.add("mavenLocal()");
|
||||
others.add("mavenCentral()");
|
||||
others.add("jcenter()");
|
||||
others.add("classpath");
|
||||
others.add("allprojects");
|
||||
others.add("subprojects");
|
||||
others.add ("buildscript");
|
||||
others.add ("ext");
|
||||
others.add ("project.ext");
|
||||
others.add ("repositories");
|
||||
others.add ("maven");
|
||||
others.add ("url");
|
||||
others.add ("google()");
|
||||
others.add ("mavenLocal()");
|
||||
others.add ("mavenCentral()");
|
||||
others.add ("jcenter()");
|
||||
others.add ("classpath");
|
||||
others.add ("allprojects");
|
||||
others.add ("subprojects");
|
||||
return others;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,9 +17,13 @@
|
||||
**************************************************************************************/
|
||||
package com.itsaky.androidide.language.groovy;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.itsaky.androidide.language.BaseLanguage;
|
||||
import com.itsaky.androidide.lexers.groovy.GroovyLexer;
|
||||
import com.itsaky.androidide.utils.JavaCharacter;
|
||||
import com.itsaky.androidide.utils.CharSequenceReader;
|
||||
import com.itsaky.androidide.utils.Logger;
|
||||
|
||||
import org.antlr.v4.runtime.CharStreams;
|
||||
@@ -27,12 +31,15 @@ import org.antlr.v4.runtime.Token;
|
||||
|
||||
import java.io.StringReader;
|
||||
|
||||
import io.github.rosemoe.editor.interfaces.AutoCompleteProvider;
|
||||
import io.github.rosemoe.editor.interfaces.CodeAnalyzer;
|
||||
import io.github.rosemoe.editor.interfaces.NewlineHandler;
|
||||
import io.github.rosemoe.editor.text.CharPosition;
|
||||
import io.github.rosemoe.editor.text.TextUtils;
|
||||
import io.github.rosemoe.editor.widget.SymbolPairMatch;
|
||||
import io.github.rosemoe.sora.lang.analysis.AnalyzeManager;
|
||||
import io.github.rosemoe.sora.lang.completion.CompletionCancelledException;
|
||||
import io.github.rosemoe.sora.lang.completion.CompletionPublisher;
|
||||
import io.github.rosemoe.sora.lang.smartEnter.NewlineHandleResult;
|
||||
import io.github.rosemoe.sora.lang.smartEnter.NewlineHandler;
|
||||
import io.github.rosemoe.sora.text.CharPosition;
|
||||
import io.github.rosemoe.sora.text.ContentReference;
|
||||
import io.github.rosemoe.sora.text.TextUtils;
|
||||
import io.github.rosemoe.sora.widget.SymbolPairMatch;
|
||||
|
||||
public class GroovyLanguage extends BaseLanguage {
|
||||
|
||||
@@ -46,22 +53,6 @@ public class GroovyLanguage extends BaseLanguage {
|
||||
completer = new GroovyAutoComplete ();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CodeAnalyzer getAnalyzer () {
|
||||
return analyzer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AutoCompleteProvider getAutoCompleteProvider () {
|
||||
return completer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAutoCompleteChar (char ch) {
|
||||
return JavaCharacter.isJavaIdentifierPart (ch) || ch == '.';
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIndentAdvance (String p1) {
|
||||
try {
|
||||
GroovyLexer lexer = new GroovyLexer (CharStreams.fromReader (new StringReader (p1)));
|
||||
@@ -90,9 +81,49 @@ public class GroovyLanguage extends BaseLanguage {
|
||||
return new SymbolPairMatch.DefaultSymbolPairs ();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public boolean useTab () {
|
||||
return false;
|
||||
public AnalyzeManager getAnalyzeManager () {
|
||||
return analyzer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInterruptionLevel () {
|
||||
return INTERRUPTION_LEVEL_STRONG;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requireAutoComplete (
|
||||
@NonNull ContentReference content,
|
||||
@NonNull CharPosition position,
|
||||
@NonNull CompletionPublisher publisher,
|
||||
@NonNull Bundle extraArguments) throws CompletionCancelledException {
|
||||
|
||||
completer.complete (content, position, publisher, extraArguments);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIndentAdvance (@NonNull ContentReference content, int line, int column) {
|
||||
try {
|
||||
GroovyLexer lexer = new GroovyLexer (CharStreams.fromReader (new CharSequenceReader (content)));
|
||||
Token token;
|
||||
int advance = 0;
|
||||
while (((token = lexer.nextToken ()) != null && token.getType () != token.EOF)) {
|
||||
switch (token.getType ()) {
|
||||
case GroovyLexer.LBRACE:
|
||||
advance++;
|
||||
break;
|
||||
case GroovyLexer.RBRACE:
|
||||
advance--;
|
||||
break;
|
||||
}
|
||||
}
|
||||
advance = Math.max (0, advance);
|
||||
return advance * getTabSize ();
|
||||
} catch (Throwable e) {
|
||||
LOG.error ("Failed to calculate indent advance", e);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -107,24 +138,29 @@ public class GroovyLanguage extends BaseLanguage {
|
||||
return newlineHandlers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy () {
|
||||
|
||||
}
|
||||
|
||||
class BraceHandler implements NewlineHandler {
|
||||
|
||||
|
||||
@Override
|
||||
public boolean matchesRequirement (String beforeText, String afterText, CharPosition cursor) {
|
||||
public boolean matchesRequirement (String beforeText, String afterText) {
|
||||
beforeText = beforeText.trim ();
|
||||
afterText = afterText.trim ();
|
||||
return beforeText.endsWith ("{") && afterText.startsWith ("}");
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandleResult handleNewline (String beforeText, String afterText, int tabSize) {
|
||||
public NewlineHandleResult handleNewline (String beforeText, String afterText, int tabSize) {
|
||||
int count = TextUtils.countLeadingSpaceCount (beforeText, tabSize);
|
||||
int advanceBefore = getIndentAdvance (beforeText);
|
||||
int advanceAfter = getIndentAdvance (afterText);
|
||||
String text;
|
||||
StringBuilder sb = new StringBuilder ("\n").append (TextUtils.createIndent (count + advanceBefore, tabSize, useTab ())).append ('\n').append (text = TextUtils.createIndent (count + advanceAfter, tabSize, useTab ()));
|
||||
int shiftLeft = text.length () + 1;
|
||||
return new HandleResult (sb, shiftLeft);
|
||||
return new NewlineHandleResult (sb, shiftLeft);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,88 +16,99 @@
|
||||
*/
|
||||
package com.itsaky.androidide.language.java;
|
||||
|
||||
import static com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.ANNOTATION;
|
||||
import static com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.LITERAL;
|
||||
import static com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.OPERATOR;
|
||||
import static com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.TEXT_NORMAL;
|
||||
import static com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.TYPE_NAME;
|
||||
import static com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.forComment;
|
||||
import static com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.forKeyword;
|
||||
import static com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.forString;
|
||||
import static com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.get;
|
||||
import static com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.withoutCompletion;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.itsaky.androidide.lexers.java.JavaLexer;
|
||||
import com.itsaky.androidide.models.ConstantsBridge;
|
||||
import com.itsaky.androidide.utils.HighlightRangeHelper;
|
||||
import com.itsaky.androidide.utils.LSPUtils;
|
||||
import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE;
|
||||
import com.itsaky.androidide.utils.CharSequenceReader;
|
||||
import com.itsaky.androidide.utils.Logger;
|
||||
import com.itsaky.androidide.views.editor.IDEEditor;
|
||||
import com.itsaky.lsp.api.ILanguageServer;
|
||||
import com.itsaky.lsp.models.AnalyzeParams;
|
||||
import com.itsaky.lsp.models.DiagnosticItem;
|
||||
import com.itsaky.lsp.models.DiagnosticResult;
|
||||
import com.itsaky.lsp.models.DiagnosticSeverity;
|
||||
import com.itsaky.lsp.models.Position;
|
||||
import com.itsaky.lsp.models.Range;
|
||||
|
||||
import org.antlr.v4.runtime.CharStreams;
|
||||
import org.antlr.v4.runtime.CodePointCharStream;
|
||||
import org.antlr.v4.runtime.Token;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.StringReader;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Stack;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import io.github.rosemoe.editor.langs.AbstractCodeAnalyzer;
|
||||
import io.github.rosemoe.editor.struct.BlockLine;
|
||||
import io.github.rosemoe.editor.syntax.EditorColorScheme;
|
||||
import io.github.rosemoe.editor.text.CharPosition;
|
||||
import io.github.rosemoe.editor.text.Content;
|
||||
import io.github.rosemoe.editor.text.TextAnalyzeResult;
|
||||
import io.github.rosemoe.editor.text.TextAnalyzer;
|
||||
import io.github.rosemoe.sora.lang.analysis.SimpleAnalyzeManager;
|
||||
import io.github.rosemoe.sora.lang.styling.CodeBlock;
|
||||
import io.github.rosemoe.sora.lang.styling.MappedSpans;
|
||||
import io.github.rosemoe.sora.lang.styling.Span;
|
||||
import io.github.rosemoe.sora.lang.styling.Styles;
|
||||
|
||||
/**
|
||||
* Code analyzer for the Java Language
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
public class JavaAnalyzer extends AbstractCodeAnalyzer {
|
||||
public class JavaAnalyzer extends SimpleAnalyzeManager<Void> {
|
||||
|
||||
private final Map<Integer, List<Range>> stringMap = new HashMap<> ();
|
||||
private List<DiagnosticItem> diagnostics = new ArrayList<> ();
|
||||
private final ILanguageServer languageServer;
|
||||
|
||||
private HighlightRangeHelper helper;
|
||||
public JavaAnalyzer (ILanguageServer languageServer) {
|
||||
this.languageServer = languageServer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void analyze (@Nullable ILanguageServer languageServer,
|
||||
@Nullable File file,
|
||||
@NonNull Content content,
|
||||
@NonNull TextAnalyzeResult colors,
|
||||
@NonNull TextAnalyzer.AnalyzeThread.Delegate delegate
|
||||
) throws Exception {
|
||||
|
||||
if (languageServer != null && file != null) {
|
||||
CompletableFuture.runAsync (() -> {
|
||||
final var analyzer = languageServer.getCodeAnalyzer ();
|
||||
final var result = analyzer.analyze (new AnalyzeParams (file.toPath ()));
|
||||
|
||||
JavaAnalyzer.this.diagnostics = result.getDiagnostics ();
|
||||
JavaAnalyzer.this.helper = new HighlightRangeHelper (result.getSemanticHighlights ());
|
||||
|
||||
if (languageServer.getClient () != null) {
|
||||
languageServer.getClient ().publishDiagnostics (new DiagnosticResult (file.toPath (), JavaAnalyzer.this.diagnostics));
|
||||
}
|
||||
});
|
||||
protected Styles analyze (StringBuilder text, Delegate<Void> delegate) {
|
||||
final var args = getExtraArguments ();
|
||||
if (args.containsKey (IDEEditor.KEY_FILE)) {
|
||||
final var file = new File (args.getString (IDEEditor.KEY_FILE));
|
||||
if (languageServer != null && file.exists()) {
|
||||
CompletableFuture.runAsync (() -> {
|
||||
final var analyzer = languageServer.getCodeAnalyzer ();
|
||||
|
||||
diagnostics = analyzer.analyze (file.toPath ());
|
||||
|
||||
if (languageServer.getClient () != null) {
|
||||
languageServer.getClient ().publishDiagnostics (new DiagnosticResult (file.toPath (), diagnostics));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
final var stream = CharStreams.fromReader (new StringReader (content.toString ()));
|
||||
final CodePointCharStream stream;
|
||||
final var styles = new Styles ();
|
||||
final var colors = new MappedSpans.Builder ();
|
||||
try {
|
||||
stream = CharStreams.fromReader (new CharSequenceReader (text));
|
||||
} catch (IOException e) {
|
||||
LOG.error ("Unable to create char stream for text", e);
|
||||
return styles;
|
||||
}
|
||||
final var lexer = new JavaLexer (stream);
|
||||
final var stack = new Stack<BlockLine> ();
|
||||
final var stack = new Stack<CodeBlock> ();
|
||||
|
||||
Token token;
|
||||
int line, column, lastLine = 1, type, currSwitch = 0, maxSwitch = 0;
|
||||
int line, column, lastLine = 1, type, currSwitch = 0, maxSwitch = 0, previous = -1;
|
||||
boolean isFirst = true;
|
||||
|
||||
this.stringMap.clear ();
|
||||
|
||||
while (delegate.shouldAnalyze ()) {
|
||||
while (!delegate.isCancelled ()) {
|
||||
token = lexer.nextToken ();
|
||||
|
||||
if (token == null) {
|
||||
@@ -112,7 +123,8 @@ public class JavaAnalyzer extends AbstractCodeAnalyzer {
|
||||
lastLine = line;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
final var tokenLength = token.getText ().length ();
|
||||
switch (type) {
|
||||
case JavaLexer.WS:
|
||||
if (isFirst) {
|
||||
@@ -161,7 +173,7 @@ public class JavaAnalyzer extends AbstractCodeAnalyzer {
|
||||
case JavaLexer.VOLATILE:
|
||||
case JavaLexer.WHILE:
|
||||
case JavaLexer.VAR:
|
||||
colors.addIfNeeded (line, column, EditorColorScheme.KEYWORD);
|
||||
colors.addIfNeeded (line, column, forKeyword ());
|
||||
break;
|
||||
case JavaLexer.DECIMAL_LITERAL:
|
||||
case JavaLexer.HEX_LITERAL:
|
||||
@@ -172,24 +184,11 @@ public class JavaAnalyzer extends AbstractCodeAnalyzer {
|
||||
case JavaLexer.BOOL_LITERAL:
|
||||
case JavaLexer.CHAR_LITERAL:
|
||||
case JavaLexer.NULL_LITERAL:
|
||||
colors.addIfNeeded (line, column, EditorColorScheme.LITERAL);
|
||||
colors.addIfNeeded (line, column, get (LITERAL));
|
||||
break;
|
||||
case JavaLexer.STRING_LITERAL:
|
||||
checkAndAddHexString (token, EditorColorScheme.LITERAL, colors);
|
||||
|
||||
Position start = new Position (line, column);
|
||||
Position end = new Position (line, column + token.getText ().length ());
|
||||
colors.addStringRange (line, new Range (start, end));
|
||||
|
||||
{
|
||||
List<Range> ranges = stringMap.get (line);
|
||||
if (ranges == null) {
|
||||
ranges = new ArrayList<> ();
|
||||
}
|
||||
ranges.add (new Range (start, end));
|
||||
stringMap.put (line, ranges);
|
||||
}
|
||||
break;
|
||||
colors.addIfNeeded (line, column, forString ());
|
||||
break;
|
||||
case JavaLexer.LPAREN:
|
||||
case JavaLexer.RPAREN:
|
||||
case JavaLexer.LBRACK:
|
||||
@@ -234,7 +233,7 @@ public class JavaAnalyzer extends AbstractCodeAnalyzer {
|
||||
case JavaLexer.COLONCOLON:
|
||||
case JavaLexer.ELLIPSIS:
|
||||
case JavaLexer.DOT:
|
||||
colors.addIfNeeded (line, column, EditorColorScheme.OPERATOR);
|
||||
colors.addIfNeeded (line, column, get (OPERATOR));
|
||||
break;
|
||||
case JavaLexer.BOOLEAN:
|
||||
case JavaLexer.BYTE:
|
||||
@@ -245,154 +244,58 @@ public class JavaAnalyzer extends AbstractCodeAnalyzer {
|
||||
case JavaLexer.INT:
|
||||
case JavaLexer.LONG:
|
||||
case JavaLexer.SHORT:
|
||||
colors.addIfNeeded (line, column, EditorColorScheme.TYPE_NAME);
|
||||
colors.addIfNeeded (line, column, get (TYPE_NAME));
|
||||
break;
|
||||
case JavaLexer.BLOCK_COMMENT:
|
||||
colors.addIfNeeded (line, column, forComment ());
|
||||
break;
|
||||
case JavaLexer.LINE_COMMENT:
|
||||
colors.addIfNeeded (line, column, EditorColorScheme.COMMENT);
|
||||
|
||||
final CharPosition a = content.getIndexer ().getCharPosition (token.getStopIndex ());
|
||||
final Position s = new Position (line, column);
|
||||
final Position e = new Position (a.line, a.column);
|
||||
colors.addCommentRange (line, new Range (s, e));
|
||||
|
||||
String lineComment = token.getText ();
|
||||
if (lineComment != null) {
|
||||
if (lineComment.startsWith ("//")) {
|
||||
lineComment = lineComment.substring (2);
|
||||
}
|
||||
var commentType = SchemeAndroidIDE.COMMENT;
|
||||
|
||||
// Trim leading whitespaces
|
||||
int startsFrom = 0;
|
||||
for (int i = 0; i < lineComment.length (); i++) {
|
||||
if (Character.isWhitespace (lineComment.charAt (i))) {
|
||||
startsFrom++;
|
||||
} else {
|
||||
break;
|
||||
// highlight special line comments
|
||||
var commentText = token.getText ();
|
||||
if (commentText.length () > 2) {
|
||||
commentText = commentText.substring (2);
|
||||
commentText = commentText.trim ();
|
||||
|
||||
var mark = true;
|
||||
if ("todo".equalsIgnoreCase (commentText.substring (0, 4))) {
|
||||
commentType = SchemeAndroidIDE.TODO_COMMENT;
|
||||
} else if ("fixme".equalsIgnoreCase (commentText.substring (0, 5))) {
|
||||
commentType = SchemeAndroidIDE.FIXME_COMMENT;
|
||||
} else {
|
||||
mark = false;
|
||||
}
|
||||
|
||||
if (mark) {
|
||||
if (diagnostics == null) {
|
||||
diagnostics = new ArrayList<> ();
|
||||
}
|
||||
}
|
||||
|
||||
lineComment = lineComment.substring (startsFrom);
|
||||
|
||||
final String text = new String (lineComment.toCharArray ());
|
||||
lineComment = lineComment.toLowerCase (Locale.getDefault ());
|
||||
if (lineComment.startsWith ("todo ")) {
|
||||
diagnostics.add (LSPUtils.newInfoDiagnostic (line,
|
||||
column,
|
||||
token.getText ().length (),
|
||||
text,
|
||||
token.getText ()));
|
||||
} else if (lineComment.startsWith (ConstantsBridge.CUSTOM_COMMENT_WARNING_TOKEN)) {
|
||||
diagnostics.add (LSPUtils.newWarningDiagnostic (line,
|
||||
column,
|
||||
token.getText ().length (),
|
||||
text,
|
||||
token.getText ()));
|
||||
final var diagnostic = new DiagnosticItem ();
|
||||
diagnostic.setSeverity (DiagnosticSeverity.WARNING);
|
||||
diagnostic.setMessage (commentText);
|
||||
diagnostic.setCode ("special.comment");
|
||||
diagnostic.setRange (new Range (new Position (line, column), new Position (line, column + tokenLength)));
|
||||
diagnostic.setSource (commentText);
|
||||
diagnostics.add (diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
colors.addIfNeeded (line, column, withoutCompletion (commentType));
|
||||
|
||||
break;
|
||||
case JavaLexer.AT:
|
||||
colors.addIfNeeded (line, column, EditorColorScheme.ANNOTATION);
|
||||
colors.addIfNeeded (line, column, get (ANNOTATION));
|
||||
break;
|
||||
case JavaLexer.IDENTIFIER:
|
||||
|
||||
if (helper != null) {
|
||||
if (helper.isPackageName (line, column)) {
|
||||
colors.addIfNeeded (line, column, EditorColorScheme.PACKAGE_NAME);
|
||||
break;
|
||||
}
|
||||
|
||||
if (helper.isEnumType (line, column)) {
|
||||
colors.addIfNeeded (line, column, EditorColorScheme.ENUM_TYPE);
|
||||
break;
|
||||
}
|
||||
|
||||
if (helper.isClassName (line, column)) {
|
||||
colors.addIfNeeded (line, column, EditorColorScheme.TYPE_NAME);
|
||||
break;
|
||||
}
|
||||
|
||||
if (helper.isAnnotationType (line, column)) {
|
||||
colors.addIfNeeded (line, column, EditorColorScheme.ANNOTATION);
|
||||
break;
|
||||
}
|
||||
|
||||
if (helper.isInterface (line, column)) {
|
||||
colors.addIfNeeded (line, column, EditorColorScheme.INTERFACE);
|
||||
break;
|
||||
}
|
||||
|
||||
if (helper.isEnum (line, column)) {
|
||||
colors.addIfNeeded (line, column, EditorColorScheme.ENUM);
|
||||
break;
|
||||
}
|
||||
|
||||
if (helper.isStaticField (line, column)) {
|
||||
colors.addIfNeeded (line, column, EditorColorScheme.STATIC_FIELD);
|
||||
break;
|
||||
}
|
||||
|
||||
if (helper.isField (line, column)) {
|
||||
colors.addIfNeeded (line, column, EditorColorScheme.FIELD);
|
||||
break;
|
||||
}
|
||||
|
||||
if (helper.isParameter (line, column)) {
|
||||
colors.addIfNeeded (line, column, EditorColorScheme.PARAMETER);
|
||||
break;
|
||||
}
|
||||
|
||||
if (helper.isLocal (line, column)) {
|
||||
colors.addIfNeeded (line, column, EditorColorScheme.LOCAL_VARIABLE);
|
||||
break;
|
||||
}
|
||||
|
||||
if (helper.isExceptionParam (line, column)) {
|
||||
colors.addIfNeeded (line, column, EditorColorScheme.EXCEPTION_PARAM);
|
||||
break;
|
||||
}
|
||||
|
||||
if (helper.isMethodDeclaration (line, column)) {
|
||||
colors.addIfNeeded (line, column, EditorColorScheme.METHOD_DECLARATION);
|
||||
break;
|
||||
}
|
||||
|
||||
if (helper.isMethodInvocation (line, column)) {
|
||||
colors.addIfNeeded (line, column, EditorColorScheme.METHOD_INVOCATION);
|
||||
break;
|
||||
}
|
||||
|
||||
if (helper.isConstructor (line, column)) {
|
||||
colors.addIfNeeded (line, column, EditorColorScheme.CONSTRUCTOR);
|
||||
break;
|
||||
}
|
||||
|
||||
if (helper.isStaticInit (line, column)) {
|
||||
colors.addIfNeeded (line, column, EditorColorScheme.STATIC_INIT);
|
||||
break;
|
||||
}
|
||||
|
||||
if (helper.isInstanceInit (line, column)) {
|
||||
colors.addIfNeeded (line, column, EditorColorScheme.INSTANCE_INIT);
|
||||
break;
|
||||
}
|
||||
|
||||
if (helper.isTypeParam (line, column)) {
|
||||
colors.addIfNeeded (line, column, EditorColorScheme.TYPE_PARAM);
|
||||
break;
|
||||
}
|
||||
|
||||
if (helper.isResourceVariable (line, column)) {
|
||||
colors.addIfNeeded (line, column, EditorColorScheme.RESOURCE_VARIABLE);
|
||||
break;
|
||||
}
|
||||
int colorId = SchemeAndroidIDE.TEXT_NORMAL;
|
||||
if (previous == JavaLexer.AT) {
|
||||
colorId = SchemeAndroidIDE.ANNOTATION;
|
||||
}
|
||||
|
||||
colors.addIfNeeded (line, column, EditorColorScheme.TEXT_NORMAL);
|
||||
colors.addIfNeeded (line, column, get (colorId));
|
||||
break;
|
||||
case JavaLexer.LBRACE:
|
||||
colors.addIfNeeded (line, column, EditorColorScheme.OPERATOR);
|
||||
colors.addIfNeeded (line, column, get (OPERATOR));
|
||||
if (stack.isEmpty ()) {
|
||||
if (currSwitch > maxSwitch) {
|
||||
maxSwitch = currSwitch;
|
||||
@@ -400,66 +303,71 @@ public class JavaAnalyzer extends AbstractCodeAnalyzer {
|
||||
currSwitch = 0;
|
||||
}
|
||||
currSwitch++;
|
||||
BlockLine block = colors.obtainNewBlock ();
|
||||
var block = styles.obtainNewBlock ();
|
||||
block.startLine = line;
|
||||
block.startColumn = column;
|
||||
stack.push (block);
|
||||
break;
|
||||
case JavaLexer.RBRACE:
|
||||
colors.addIfNeeded (line, column, EditorColorScheme.OPERATOR);
|
||||
colors.addIfNeeded (line, column, get (OPERATOR));
|
||||
if (!stack.isEmpty ()) {
|
||||
BlockLine block2 = stack.pop ();
|
||||
var block2 = stack.pop ();
|
||||
block2.endLine = line;
|
||||
block2.endColumn = column;
|
||||
if (block2.startLine != block2.endLine) {
|
||||
colors.addBlockLine (block2);
|
||||
styles.addCodeBlock (block2);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
colors.addIfNeeded (line, column, EditorColorScheme.TEXT_NORMAL);
|
||||
colors.addIfNeeded (line, column, get (TEXT_NORMAL));
|
||||
break;
|
||||
}
|
||||
|
||||
isFirst = false;
|
||||
|
||||
if (type != JavaLexer.WS) {
|
||||
previous = type;
|
||||
}
|
||||
}
|
||||
|
||||
if (stack.isEmpty () && currSwitch > maxSwitch) {
|
||||
maxSwitch = currSwitch;
|
||||
}
|
||||
|
||||
colors.determine (lastLine);
|
||||
colors.setSuppressSwitch (maxSwitch + 10);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DiagnosticItem findDiagnosticContaining (int line, int column) {
|
||||
final var position = new Position (line, column);
|
||||
colors.determine (lastLine);
|
||||
styles.setSuppressSwitch (maxSwitch + 10);
|
||||
|
||||
// binary search the diagnostic item at the given position
|
||||
// compare by search position
|
||||
int left = 0; int right = diagnostics.size () - 1; int mid;
|
||||
while (left < right) {
|
||||
mid = (left + right) / 2;
|
||||
final var diagnostic = diagnostics.get (mid);
|
||||
final var range = diagnostic.getRange ();
|
||||
final var res = range.containsForBinarySearch (position);
|
||||
if (res < 0) {
|
||||
left = mid + 1;
|
||||
} else if (res > 0) {
|
||||
right = mid - 1;
|
||||
} else {
|
||||
return diagnostic;
|
||||
if (diagnostics != null && !diagnostics.isEmpty ()) {
|
||||
for (var d : diagnostics) {
|
||||
var start = d.getRange ().getStart ();
|
||||
var end = d.getRange ().getEnd ();
|
||||
|
||||
colors.markProblemRegion (convertToSpanFlag(d.getSeverity ()),
|
||||
start.getLine (),
|
||||
start.getColumn (),
|
||||
end.getLine (),
|
||||
end.getColumn ());
|
||||
}
|
||||
}
|
||||
return super.findDiagnosticContaining (line, column);
|
||||
|
||||
styles.spans = colors.build ();
|
||||
|
||||
return styles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DiagnosticItem> findDiagnosticsContainingLine (int line) {
|
||||
return diagnostics.stream ()
|
||||
.filter (diagnostic -> diagnostic.getRange ().containsLine (line))
|
||||
.collect (Collectors.toList ());
|
||||
@Contract(pure = true)
|
||||
private int convertToSpanFlag (@NonNull DiagnosticSeverity severity) {
|
||||
switch (severity) {
|
||||
case WARNING:
|
||||
return Span.FLAG_WARNING;
|
||||
case ERROR:
|
||||
return Span.FLAG_ERROR;
|
||||
case HINT:
|
||||
case INFO:
|
||||
default:
|
||||
return Span.FLAG_TYPO;
|
||||
}
|
||||
}
|
||||
|
||||
private static final Logger LOG = Logger.instance ("JavaAnalyzer");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/************************************************************************************
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
@@ -14,9 +14,11 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
**************************************************************************************/
|
||||
*/
|
||||
package com.itsaky.androidide.language.java;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.itsaky.androidide.app.StudioApp;
|
||||
@@ -24,58 +26,48 @@ import com.itsaky.androidide.language.BaseLanguage;
|
||||
import com.itsaky.androidide.language.CommonCompletionProvider;
|
||||
import com.itsaky.androidide.lexers.java.JavaLexer;
|
||||
import com.itsaky.androidide.lexers.java.JavaParser;
|
||||
import com.itsaky.androidide.utils.JavaCharacter;
|
||||
import com.itsaky.androidide.utils.Logger;
|
||||
import com.itsaky.androidide.views.editor.IDEEditor;
|
||||
|
||||
import org.antlr.v4.runtime.CharStreams;
|
||||
import org.antlr.v4.runtime.Token;
|
||||
|
||||
import java.io.StringReader;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import io.github.rosemoe.editor.interfaces.AutoCompleteProvider;
|
||||
import io.github.rosemoe.editor.interfaces.CodeAnalyzer;
|
||||
import io.github.rosemoe.editor.interfaces.NewlineHandler;
|
||||
import io.github.rosemoe.editor.text.CharPosition;
|
||||
import io.github.rosemoe.editor.text.TextUtils;
|
||||
import io.github.rosemoe.editor.widget.SymbolPairMatch;
|
||||
import io.github.rosemoe.sora.lang.analysis.AnalyzeManager;
|
||||
import io.github.rosemoe.sora.lang.completion.CompletionCancelledException;
|
||||
import io.github.rosemoe.sora.lang.completion.CompletionPublisher;
|
||||
import io.github.rosemoe.sora.lang.smartEnter.NewlineHandleResult;
|
||||
import io.github.rosemoe.sora.lang.smartEnter.NewlineHandler;
|
||||
import io.github.rosemoe.sora.text.CharPosition;
|
||||
import io.github.rosemoe.sora.text.ContentReference;
|
||||
import io.github.rosemoe.sora.text.TextUtils;
|
||||
import io.github.rosemoe.sora.widget.SymbolPairMatch;
|
||||
|
||||
public class JavaLanguage extends BaseLanguage {
|
||||
|
||||
private final JavaAnalyzer analyzer;
|
||||
private final AutoCompleteProvider complete;
|
||||
private JavaAnalyzer analyzer;
|
||||
private CommonCompletionProvider completer;
|
||||
|
||||
private final NewlineHandler[] newlineHandlers;
|
||||
|
||||
private static final Logger LOG = Logger.instance("JavaLanguage");
|
||||
|
||||
public JavaLanguage() {
|
||||
this.analyzer = new JavaAnalyzer ();
|
||||
this.complete = new CommonCompletionProvider (StudioApp.getInstance ().getJavaLanguageServer ());
|
||||
final var server = StudioApp.getInstance ().getJavaLanguageServer ();
|
||||
this.analyzer = new JavaAnalyzer (server);
|
||||
this.completer = new CommonCompletionProvider (server);
|
||||
|
||||
this.newlineHandlers = new NewlineHandler[1];
|
||||
this.newlineHandlers[0] = new BraceHandler();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CodeAnalyzer getAnalyzer() {
|
||||
return analyzer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AutoCompleteProvider getAutoCompleteProvider() {
|
||||
return complete;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAutoCompleteChar(char p1) {
|
||||
return JavaCharacter.isJavaIdentifierPart(p1) || p1 == '.';
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
public int getIndentAdvance(String p1) {
|
||||
try {
|
||||
JavaLexer lexer = new JavaLexer(CharStreams.fromReader(new StringReader(p1)));
|
||||
Token token = null;
|
||||
Token token;
|
||||
int advance = 0;
|
||||
while (((token = lexer.nextToken()) != null && token.getType() != token.EOF)) {
|
||||
switch (token.getType()) {
|
||||
@@ -92,6 +84,7 @@ public class JavaLanguage extends BaseLanguage {
|
||||
} catch (Throwable e) {
|
||||
LOG.error ("Error calculating indent advance", e);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -99,7 +92,33 @@ public class JavaLanguage extends BaseLanguage {
|
||||
public SymbolPairMatch getSymbolPairs() {
|
||||
return new JavaSymbolPairs ();
|
||||
}
|
||||
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public AnalyzeManager getAnalyzeManager () {
|
||||
return analyzer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInterruptionLevel () {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requireAutoComplete (@NonNull ContentReference content, @NonNull CharPosition position, @NonNull CompletionPublisher publisher, @NonNull Bundle extraArguments) throws CompletionCancelledException {
|
||||
if (!extraArguments.containsKey (IDEEditor.KEY_FILE)) {
|
||||
return;
|
||||
}
|
||||
final var file = Paths.get (extraArguments.getString (IDEEditor.KEY_FILE));
|
||||
publisher.setUpdateThreshold (0);
|
||||
publisher.addItems (new ArrayList<> (completer.complete (content, file, position)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIndentAdvance (@NonNull ContentReference content, int line, int column) {
|
||||
return getIndentAdvance (content.getLine (line).substring (0, column));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useTab() {
|
||||
return false;
|
||||
@@ -114,16 +133,22 @@ public class JavaLanguage extends BaseLanguage {
|
||||
public NewlineHandler[] getNewlineHandlers() {
|
||||
return newlineHandlers;
|
||||
}
|
||||
|
||||
class BraceHandler implements NewlineHandler {
|
||||
|
||||
@Override
|
||||
public boolean matchesRequirement(@NonNull String beforeText, String afterText, CharPosition cursor) {
|
||||
return beforeText.endsWith("{") && afterText.startsWith("}");
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandleResult handleNewline(String beforeText, String afterText, int tabSize) {
|
||||
|
||||
@Override
|
||||
public void destroy () {
|
||||
analyzer = null;
|
||||
completer = null;
|
||||
}
|
||||
|
||||
class BraceHandler implements NewlineHandler {
|
||||
|
||||
@Override
|
||||
public boolean matchesRequirement (String beforeText, String afterText) {
|
||||
return beforeText.endsWith("{") && afterText.startsWith("}");
|
||||
}
|
||||
|
||||
@Override
|
||||
public NewlineHandleResult handleNewline(String beforeText, String afterText, int tabSize) {
|
||||
int count = TextUtils.countLeadingSpaceCount(beforeText, tabSize);
|
||||
int advanceBefore = getIndentAdvance(beforeText);
|
||||
int advanceAfter = getIndentAdvance(afterText);
|
||||
@@ -133,7 +158,7 @@ public class JavaLanguage extends BaseLanguage {
|
||||
.append('\n')
|
||||
.append(text = TextUtils.createIndent(count + advanceAfter, tabSize, useTab()));
|
||||
int shiftLeft = text.length() + 1;
|
||||
return new HandleResult(sb, shiftLeft);
|
||||
return new NewlineHandleResult (sb, shiftLeft);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,49 +17,53 @@
|
||||
*/
|
||||
package com.itsaky.androidide.language.logs;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.itsaky.androidide.language.BaseLanguage;
|
||||
import com.itsaky.androidide.models.LogLine;
|
||||
import com.itsaky.lsp.api.ILanguageServer;
|
||||
import com.itsaky.lsp.models.CompletionItem;
|
||||
import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import io.github.rosemoe.editor.interfaces.AutoCompleteProvider;
|
||||
import io.github.rosemoe.editor.interfaces.CodeAnalyzer;
|
||||
import io.github.rosemoe.editor.interfaces.NewlineHandler;
|
||||
import io.github.rosemoe.editor.text.Content;
|
||||
import io.github.rosemoe.editor.text.TextAnalyzeResult;
|
||||
import io.github.rosemoe.editor.text.TextAnalyzer;
|
||||
import io.github.rosemoe.editor.widget.SymbolPairMatch;
|
||||
import io.github.rosemoe.sora.lang.analysis.AnalyzeManager;
|
||||
import io.github.rosemoe.sora.lang.analysis.SimpleAnalyzeManager;
|
||||
import io.github.rosemoe.sora.lang.completion.CompletionCancelledException;
|
||||
import io.github.rosemoe.sora.lang.completion.CompletionPublisher;
|
||||
import io.github.rosemoe.sora.lang.smartEnter.NewlineHandler;
|
||||
import io.github.rosemoe.sora.lang.styling.MappedSpans;
|
||||
import io.github.rosemoe.sora.lang.styling.Styles;
|
||||
import io.github.rosemoe.sora.text.CharPosition;
|
||||
import io.github.rosemoe.sora.text.ContentReference;
|
||||
import io.github.rosemoe.sora.widget.SymbolPairMatch;
|
||||
|
||||
public class LogLanguageImpl extends BaseLanguage {
|
||||
|
||||
private static final LogAnalyzer analyzer = new LogAnalyzer();
|
||||
private static final LogCompletor completor = new LogCompletor();
|
||||
|
||||
public void addLine(LogLine line) {
|
||||
analyzer.addLine (line);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public CodeAnalyzer getAnalyzer() {
|
||||
public AnalyzeManager getAnalyzeManager () {
|
||||
return analyzer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AutoCompleteProvider getAutoCompleteProvider() {
|
||||
return completor;
|
||||
public int getInterruptionLevel () {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAutoCompleteChar(char ch) {
|
||||
return false;
|
||||
public void requireAutoComplete (@NonNull ContentReference content, @NonNull CharPosition position, @NonNull CompletionPublisher publisher, @NonNull Bundle extraArguments) throws CompletionCancelledException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIndentAdvance(String content) {
|
||||
public int getIndentAdvance (@NonNull ContentReference content, int line, int column) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -83,7 +87,12 @@ public class LogLanguageImpl extends BaseLanguage {
|
||||
return new NewlineHandler[0];
|
||||
}
|
||||
|
||||
private static class LogAnalyzer extends io.github.rosemoe.editor.langs.AbstractCodeAnalyzer {
|
||||
@Override
|
||||
public void destroy () {
|
||||
|
||||
}
|
||||
|
||||
private static class LogAnalyzer extends SimpleAnalyzeManager<Void> {
|
||||
|
||||
private static final List<LogLine> lines = new ArrayList<>();
|
||||
|
||||
@@ -95,23 +104,21 @@ public class LogLanguageImpl extends BaseLanguage {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void analyze(ILanguageServer server, File file, Content content, TextAnalyzeResult colors, TextAnalyzer.AnalyzeThread.Delegate delegate) {
|
||||
protected Styles analyze (StringBuilder text, Delegate<Void> delegate) {
|
||||
final var styles = new Styles ();
|
||||
final var colors = new MappedSpans.Builder ();
|
||||
int lastLine = 0;
|
||||
for(int i=0;i<lines.size() && delegate.shouldAnalyze();i++) {
|
||||
for(int i=0;i<lines.size() && !delegate.isCancelled ();i++) {
|
||||
if(i==0) colors.addNormalIfNull();
|
||||
LogLine line = lines.get(i);
|
||||
colors.addIfNeeded(i, 0, line.priority);
|
||||
colors.addIfNeeded(i, 0, SchemeAndroidIDE.get (line.priority));
|
||||
lastLine = i;
|
||||
}
|
||||
colors.determine(lastLine);
|
||||
}
|
||||
}
|
||||
|
||||
private static class LogCompletor implements AutoCompleteProvider {
|
||||
|
||||
@Override
|
||||
public List<CompletionItem> getAutoCompleteItems(Content content, String fileUri, String prefix, boolean isInCodeBlock, TextAnalyzeResult colors, int index, int line, int column) {
|
||||
return new ArrayList<> ();
|
||||
|
||||
styles.spans = colors.build ();
|
||||
|
||||
return styles;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
/************************************************************************************
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
*
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
@@ -16,42 +14,62 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
**************************************************************************************/
|
||||
|
||||
*/
|
||||
package com.itsaky.androidide.language.xml;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import static com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.forComment;
|
||||
import static com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.forString;
|
||||
import static com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.get;
|
||||
import static com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.OPERATOR;
|
||||
import static com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.TEXT_SELECTED;
|
||||
|
||||
import com.itsaky.androidide.lexers.xml.XMLLexer;
|
||||
import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE;
|
||||
import com.itsaky.androidide.utils.CharSequenceReader;
|
||||
import com.itsaky.androidide.utils.Logger;
|
||||
import com.itsaky.lsp.api.ILanguageServer;
|
||||
|
||||
import org.antlr.v4.runtime.CharStreams;
|
||||
import org.antlr.v4.runtime.CodePointCharStream;
|
||||
import org.antlr.v4.runtime.Token;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.util.Stack;
|
||||
|
||||
import io.github.rosemoe.editor.text.Content;
|
||||
import io.github.rosemoe.editor.text.TextAnalyzeResult;
|
||||
import io.github.rosemoe.editor.text.TextAnalyzer;
|
||||
import io.github.rosemoe.editor.syntax.EditorColorScheme;
|
||||
import io.github.rosemoe.sora.lang.analysis.SimpleAnalyzeManager;
|
||||
import io.github.rosemoe.sora.lang.styling.CodeBlock;
|
||||
import io.github.rosemoe.sora.lang.styling.MappedSpans;
|
||||
import io.github.rosemoe.sora.lang.styling.Styles;
|
||||
|
||||
public class XMLAnalyzer extends io.github.rosemoe.editor.langs.AbstractCodeAnalyzer {
|
||||
/**
|
||||
* Simple analyzer implementation for the XML files.
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
public class XMLAnalyzer extends SimpleAnalyzeManager<Void> {
|
||||
|
||||
private static final Logger LOG = Logger.instance ("XMLAnalyzer");
|
||||
|
||||
@Override
|
||||
public void analyze (ILanguageServer server, File file, @NonNull Content content, TextAnalyzeResult colors, @NonNull TextAnalyzer.AnalyzeThread.Delegate delegate) throws IOException {
|
||||
CodePointCharStream stream = CharStreams.fromReader (new StringReader (content.toString ()));
|
||||
protected Styles analyze (StringBuilder text, Delegate<Void> delegate) {
|
||||
final var styles = new Styles ();
|
||||
final var colors = new MappedSpans.Builder ();
|
||||
|
||||
CodePointCharStream stream;
|
||||
try {
|
||||
stream = CharStreams.fromReader (new CharSequenceReader (text));
|
||||
} catch (IOException e) {
|
||||
LOG.error ("Unable to create stream for analyze", e);
|
||||
return styles;
|
||||
}
|
||||
|
||||
XMLLexer lexer = new XMLLexer (stream);
|
||||
Token token = null, previous = null;
|
||||
int line = 0, column = 0, lastLine = 1;
|
||||
Token token;
|
||||
int previous = 0;
|
||||
int line, column, lastLine = 1;
|
||||
var first = true;
|
||||
|
||||
while (delegate.shouldAnalyze ()) {
|
||||
final var stack = new Stack<CodeBlock> ();
|
||||
|
||||
while (!delegate.isCancelled ()) {
|
||||
token = lexer.nextToken ();
|
||||
if (token == null) {
|
||||
break;
|
||||
@@ -75,35 +93,62 @@ public class XMLAnalyzer extends io.github.rosemoe.editor.langs.AbstractCodeAnal
|
||||
|
||||
break;
|
||||
case XMLLexer.COMMENT:
|
||||
colors.addIfNeeded (line, column, EditorColorScheme.COMMENT);
|
||||
colors.addIfNeeded (line, column, forComment ());
|
||||
break;
|
||||
case XMLLexer.OPEN:
|
||||
case XMLLexer.OPEN_SLASH:
|
||||
case XMLLexer.CLOSE:
|
||||
case XMLLexer.SLASH:
|
||||
case XMLLexer.SLASH_CLOSE:
|
||||
case XMLLexer.SPECIAL_CLOSE:
|
||||
case XMLLexer.EQUALS:
|
||||
case XMLLexer.COLON:
|
||||
case XMLLexer.XMLDeclOpen:
|
||||
colors.addIfNeeded (line, column, EditorColorScheme.OPERATOR);
|
||||
colors.addIfNeeded (line, column, get (OPERATOR));
|
||||
break;
|
||||
case XMLLexer.SLASH_CLOSE:
|
||||
colors.addIfNeeded (line, column, get (OPERATOR));
|
||||
final var closeBlock = stack.pop ();
|
||||
closeBlock.endLine = line;
|
||||
closeBlock.endColumn = column;
|
||||
styles.addCodeBlock (closeBlock);
|
||||
break;
|
||||
case XMLLexer.STRING:
|
||||
checkAndAddHexString (token, EditorColorScheme.LITERAL, colors);
|
||||
colors.addIfNeeded (line, column, forString ());
|
||||
break;
|
||||
case XMLLexer.Name:
|
||||
checkAndAddHexString (token, previous.getType () == XMLLexer.OPEN || previous.getType () == XMLLexer.OPEN_SLASH ? EditorColorScheme.XML_TAG : EditorColorScheme.TEXT_NORMAL, colors);
|
||||
var type = SchemeAndroidIDE.TEXT_NORMAL;
|
||||
if (previous == XMLLexer.OPEN) {
|
||||
type = SchemeAndroidIDE.XML_TAG;
|
||||
final var block = styles.obtainNewBlock ();
|
||||
block.startLine = line;
|
||||
block.startColumn = column;
|
||||
stack.push (block);
|
||||
}
|
||||
|
||||
if (previous == XMLLexer.OPEN_SLASH) {
|
||||
type = SchemeAndroidIDE.XML_TAG;
|
||||
final var block = stack.pop ();
|
||||
block.endLine = line;
|
||||
block.endColumn = column;
|
||||
styles.addCodeBlock (block);
|
||||
}
|
||||
|
||||
colors.addIfNeeded (line, column, get (type));
|
||||
break;
|
||||
case XMLLexer.TEXT:
|
||||
checkAndAddHexString (token, EditorColorScheme.TEXT_NORMAL, colors);
|
||||
default:
|
||||
colors.addIfNeeded(line, column, EditorColorScheme.TEXT_NORMAL);
|
||||
colors.addIfNeeded (line, column, get (TEXT_SELECTED));
|
||||
break;
|
||||
}
|
||||
first = false;
|
||||
if (token.getType () != XMLLexer.SEA_WS) {
|
||||
previous = token;
|
||||
previous = token.getType ();
|
||||
}
|
||||
}
|
||||
colors.determine (lastLine);
|
||||
|
||||
styles.spans = colors.build ();
|
||||
|
||||
return styles;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,27 +17,37 @@
|
||||
*/
|
||||
package com.itsaky.androidide.language.xml;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.itsaky.androidide.app.StudioApp;
|
||||
import com.itsaky.androidide.language.BaseLanguage;
|
||||
import com.itsaky.androidide.language.CommonCompletionProvider;
|
||||
import com.itsaky.androidide.lexers.xml.XMLLexer;
|
||||
import com.itsaky.androidide.utils.JavaCharacter;
|
||||
import com.itsaky.androidide.utils.Logger;
|
||||
import com.itsaky.androidide.views.editor.IDEEditor;
|
||||
|
||||
import org.antlr.v4.runtime.CharStreams;
|
||||
import org.antlr.v4.runtime.Token;
|
||||
|
||||
import java.io.StringReader;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import io.github.rosemoe.editor.interfaces.AutoCompleteProvider;
|
||||
import io.github.rosemoe.editor.interfaces.CodeAnalyzer;
|
||||
import io.github.rosemoe.editor.interfaces.NewlineHandler;
|
||||
import io.github.rosemoe.editor.widget.SymbolPairMatch;
|
||||
import io.github.rosemoe.sora.lang.analysis.AnalyzeManager;
|
||||
import io.github.rosemoe.sora.lang.completion.CompletionCancelledException;
|
||||
import io.github.rosemoe.sora.lang.completion.CompletionPublisher;
|
||||
import io.github.rosemoe.sora.lang.smartEnter.NewlineHandler;
|
||||
import io.github.rosemoe.sora.text.CharPosition;
|
||||
import io.github.rosemoe.sora.text.ContentReference;
|
||||
import io.github.rosemoe.sora.widget.SymbolPairMatch;
|
||||
|
||||
public class XMLLanguage extends BaseLanguage {
|
||||
|
||||
private final XMLAnalyzer analyzer;
|
||||
private final AutoCompleteProvider completer;
|
||||
private XMLAnalyzer analyzer;
|
||||
private final CommonCompletionProvider completer;
|
||||
private final NewlineHandler[] newlineHandlers;
|
||||
|
||||
private static final Logger LOG = Logger.instance ("XMLLanguage");
|
||||
@@ -47,25 +57,13 @@ public class XMLLanguage extends BaseLanguage {
|
||||
this.analyzer = new XMLAnalyzer();
|
||||
this.newlineHandlers = new NewlineHandler[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public CodeAnalyzer getAnalyzer() {
|
||||
return analyzer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AutoCompleteProvider getAutoCompleteProvider() {
|
||||
return completer;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
public boolean isAutoCompleteChar(char ch) {
|
||||
return JavaCharacter.isJavaIdentifierPart(ch)
|
||||
|| ch == '<'
|
||||
|| ch == '/';
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
public int getIndentAdvance(String content) {
|
||||
try {
|
||||
XMLLexer lexer = new XMLLexer(CharStreams.fromReader(new StringReader(content)));
|
||||
@@ -99,7 +97,35 @@ public class XMLLanguage extends BaseLanguage {
|
||||
public SymbolPairMatch getSymbolPairs() {
|
||||
return new SymbolPairMatch.DefaultSymbolPairs();
|
||||
}
|
||||
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public AnalyzeManager getAnalyzeManager () {
|
||||
return analyzer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInterruptionLevel () {
|
||||
return INTERRUPTION_LEVEL_STRONG;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requireAutoComplete (@NonNull ContentReference content, @NonNull CharPosition position, @NonNull CompletionPublisher publisher, @NonNull Bundle extraArguments) throws CompletionCancelledException {
|
||||
if (!extraArguments.containsKey (IDEEditor.KEY_FILE)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final var file = Paths.get (extraArguments.getString (IDEEditor.KEY_FILE));
|
||||
publisher.setUpdateThreshold (0);
|
||||
publisher.addItems (new ArrayList<> (completer.complete (content, file, position)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIndentAdvance (@NonNull ContentReference content, int line, int column) {
|
||||
final var text = content.getLine (line).substring (0, column);
|
||||
return getIndentAdvance (text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useTab() {
|
||||
return false;
|
||||
@@ -114,4 +140,9 @@ public class XMLLanguage extends BaseLanguage {
|
||||
public NewlineHandler[] getNewlineHandlers() {
|
||||
return newlineHandlers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy () {
|
||||
analyzer = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,9 +23,7 @@ import android.text.SpannableStringBuilder;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.transition.TransitionManager;
|
||||
|
||||
import com.blankj.utilcode.util.FileIOUtils;
|
||||
import com.blankj.utilcode.util.FileUtils;
|
||||
@@ -46,14 +44,14 @@ import com.itsaky.androidide.tasks.TaskExecutor;
|
||||
import com.itsaky.androidide.utils.DialogUtils;
|
||||
import com.itsaky.androidide.utils.LSPUtils;
|
||||
import com.itsaky.androidide.utils.Logger;
|
||||
import com.itsaky.androidide.views.CodeEditorView;
|
||||
import com.itsaky.androidide.views.editor.CodeEditorView;
|
||||
import com.itsaky.androidide.views.editor.IDEEditor;
|
||||
import com.itsaky.lsp.api.ILanguageClient;
|
||||
import com.itsaky.lsp.models.CodeActionItem;
|
||||
import com.itsaky.lsp.models.DiagnosticItem;
|
||||
import com.itsaky.lsp.models.DiagnosticResult;
|
||||
import com.itsaky.lsp.models.Location;
|
||||
import com.itsaky.lsp.models.Range;
|
||||
import com.itsaky.lsp.models.SignatureInformation;
|
||||
import com.itsaky.lsp.models.TextEdit;
|
||||
import com.itsaky.toaster.Toaster;
|
||||
|
||||
@@ -65,8 +63,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import io.github.rosemoe.editor.text.Content;
|
||||
import io.github.rosemoe.editor.widget.CodeEditor;
|
||||
import io.github.rosemoe.sora.text.Content;
|
||||
|
||||
/**
|
||||
* AndroidIDE specific implementation of the LanguageClient
|
||||
@@ -124,7 +121,7 @@ public class IDELanguageClientImpl implements ILanguageClient {
|
||||
return activity ().getEditorForFile (file);
|
||||
}
|
||||
|
||||
public void showDiagnostic(DiagnosticItem diagnostic, final CodeEditor editor) {
|
||||
public void showDiagnostic(DiagnosticItem diagnostic, final IDEEditor editor) {
|
||||
if(activity() == null || activity().getDiagnosticBinding() == null) {
|
||||
hideDiagnostics();
|
||||
return;
|
||||
@@ -140,7 +137,7 @@ public class IDELanguageClientImpl implements ILanguageClient {
|
||||
binding.getRoot().setVisibility(View.VISIBLE);
|
||||
|
||||
final float[] cursor = editor.getCursorPosition();
|
||||
|
||||
|
||||
float x = editor.updateCursorAnchor() - (binding.getRoot().getWidth() / 2);
|
||||
float y = activity().getBinding().editorAppBarLayout.getHeight() + (cursor[0] - editor.getRowHeight() - editor.getOffsetY() - binding.getRoot().getHeight());
|
||||
binding.getRoot().setX(x);
|
||||
@@ -153,9 +150,9 @@ public class IDELanguageClientImpl implements ILanguageClient {
|
||||
* and requests code actions from language server
|
||||
*
|
||||
* @param diagnostic The diagnostic to show
|
||||
* @param editor The CodeEditor that requested
|
||||
* @param editor The IDEEditor that requested
|
||||
*/
|
||||
public void showDiagnosticAtBottom(final File file, final DiagnosticItem diagnostic, final CodeEditor editor) {
|
||||
public void showDiagnosticAtBottom(final File file, final DiagnosticItem diagnostic, final IDEEditor editor) {
|
||||
if(activity() == null || file == null || diagnostic == null) {
|
||||
hideBottomDiagnosticView(file);
|
||||
return;
|
||||
@@ -225,88 +222,6 @@ public class IDELanguageClientImpl implements ILanguageClient {
|
||||
frag.getBinding().diagnosticText.setClickable(false);
|
||||
}
|
||||
|
||||
public void showSignatureHelp (com.itsaky.lsp.models.SignatureHelp signature, File file) {
|
||||
if(signature == null) {
|
||||
hideSignatureHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
var info = signatureWithMostParams(signature);
|
||||
if(info == null) return;
|
||||
activity().getBinding().symbolText.setText(formatSignature(info, signature.getActiveParameter()));
|
||||
final var frag = findEditorByFile(file);
|
||||
if(frag != null) {
|
||||
final CodeEditor editor = frag.getEditor();
|
||||
final float[] cursor = editor.getCursorPosition();
|
||||
|
||||
float x = editor.updateCursorAnchor() - (activity().getBinding().symbolText.getWidth() / 2);
|
||||
float y = activity().getBinding().editorAppBarLayout.getHeight() + (cursor[0] - editor.getRowHeight() - editor.getOffsetY() - activity().getBinding().symbolText.getHeight());
|
||||
TransitionManager.beginDelayedTransition(activity().getBinding().getRoot());
|
||||
activity().getBinding().symbolText.setVisibility(View.VISIBLE);
|
||||
activity().positionViewWithinScreen(activity().getBinding().symbolText, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by {@link io.github.rosemoe.editor.widget.CodeEditor CodeEditor} to hide signature help in EditorActivity
|
||||
*/
|
||||
public void hideSignatureHelp() {
|
||||
if(activity() == null) return;
|
||||
TransitionManager.beginDelayedTransition(activity().getBinding().getRoot());
|
||||
activity().getBinding().symbolText.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the signature with most parameters
|
||||
*
|
||||
* @param signature The SignatureHelp provided by @{link IDELanguageServer}
|
||||
*/
|
||||
private com.itsaky.lsp.models.SignatureInformation signatureWithMostParams(com.itsaky.lsp.models.SignatureHelp signature) {
|
||||
com.itsaky.lsp.models.SignatureInformation signatureWithMostParams = null;
|
||||
int mostParamCount = 0;
|
||||
final var signatures = signature.getSignatures();
|
||||
for(int i=0;i<signatures.size();i++) {
|
||||
final var info = signatures.get(i);
|
||||
int count = info.getParameters().size();
|
||||
if(mostParamCount < count) {
|
||||
mostParamCount = count;
|
||||
signatureWithMostParams = info;
|
||||
}
|
||||
}
|
||||
return signatureWithMostParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats (highlights) a method signature
|
||||
*
|
||||
* @param signature Signature information
|
||||
* @param paramIndex Currently active parameter index
|
||||
*/
|
||||
@NonNull
|
||||
private CharSequence formatSignature(@NonNull SignatureInformation signature, int paramIndex) {
|
||||
String name = signature.getLabel();
|
||||
name = name.substring(0, name.indexOf("("));
|
||||
|
||||
SpannableStringBuilder sb = new SpannableStringBuilder();
|
||||
sb.append(name, new ForegroundColorSpan(0xffffffff), SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
sb.append("(", new ForegroundColorSpan(0xff4fc3f7), SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
var params = signature.getParameters();
|
||||
for(int i=0;i<params.size();i++) {
|
||||
int color = i == paramIndex ? 0xffff6060 : 0xffffffff;
|
||||
final var info = params.get(i);
|
||||
if(i == params.size() - 1) {
|
||||
sb.append(info.getLabel() + "", new ForegroundColorSpan(color), SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
} else {
|
||||
sb.append(info.getLabel() + "", new ForegroundColorSpan(color), SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
sb.append(",", new ForegroundColorSpan(0xff4fc3f7), SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
sb.append(" ");
|
||||
}
|
||||
}
|
||||
sb.append(")", new ForegroundColorSpan(0xff4fc3f7), SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
return sb;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publishDiagnostics(DiagnosticResult result) {
|
||||
boolean error = result == null;
|
||||
@@ -324,7 +239,7 @@ public class IDELanguageClientImpl implements ILanguageClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by {@link io.github.rosemoe.editor.widget.CodeEditor CodeEditor} to show locations in EditorActivity
|
||||
* Called by {@link IDEEditor IDEEditor} to show locations in EditorActivity
|
||||
*/
|
||||
public void showLocations(List<Location> locations) {
|
||||
|
||||
@@ -356,7 +271,7 @@ public class IDELanguageClientImpl implements ILanguageClient {
|
||||
Content content;
|
||||
if (frag != null && frag.getEditor () != null)
|
||||
content = frag.getEditor ().getText ();
|
||||
else content = new Content (null, FileIOUtils.readFile2String (file));
|
||||
else content = new Content (FileIOUtils.readFile2String (file));
|
||||
final List<SearchResult> matches = results.containsKey (file) ? results.get (file) : new ArrayList<> ();
|
||||
Objects.requireNonNull (matches).add (
|
||||
new SearchResult (
|
||||
@@ -383,11 +298,11 @@ public class IDELanguageClientImpl implements ILanguageClient {
|
||||
/**
|
||||
* Perform the given {@link CodeActionItem}
|
||||
*
|
||||
* @param editor The {@link CodeEditor} that invoked the code action request.
|
||||
* @param editor The {@link IDEEditor} that invoked the code action request.
|
||||
* This is required to reduce the time finding the code action from the edits.
|
||||
* @param action The action to perform
|
||||
*/
|
||||
public void performCodeAction(CodeEditor editor, CodeActionItem action) {
|
||||
public void performCodeAction(IDEEditor editor, CodeActionItem action) {
|
||||
if(activity() == null || editor == null || action == null) {
|
||||
StudioApp.getInstance().toast(R.string.msg_cannot_perform_fix, Toaster.Type.ERROR);
|
||||
return;
|
||||
@@ -422,7 +337,7 @@ public class IDELanguageClientImpl implements ILanguageClient {
|
||||
return groups;
|
||||
}
|
||||
|
||||
private void showAvailableQuickfixes (CodeEditor editor, List<CodeActionItem> actions) {
|
||||
private void showAvailableQuickfixes (IDEEditor editor, List<CodeActionItem> actions) {
|
||||
final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder (activity ());
|
||||
builder.setTitle(R.string.msg_code_actions);
|
||||
builder.setItems(asArray(actions), (d, w) -> {
|
||||
@@ -442,14 +357,19 @@ public class IDELanguageClientImpl implements ILanguageClient {
|
||||
return arr;
|
||||
}
|
||||
|
||||
private Boolean performCodeActionAsync(final CodeEditor editor, final CodeActionItem action) {
|
||||
private Boolean performCodeActionAsync(final IDEEditor editor, final CodeActionItem action) {
|
||||
final var changes = action.getChanges();
|
||||
if(changes.isEmpty()) {
|
||||
return Boolean.FALSE;
|
||||
}
|
||||
|
||||
for(var change : changes) {
|
||||
final File file = change.getFile ().toFile ();
|
||||
final var path = change.getFile ();
|
||||
if (path == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final File file = path.toFile ();
|
||||
if(!file.exists()) {
|
||||
continue;
|
||||
}
|
||||
@@ -480,7 +400,7 @@ public class IDELanguageClientImpl implements ILanguageClient {
|
||||
return Boolean.TRUE;
|
||||
}
|
||||
|
||||
private void editInEditor (final CodeEditor editor, final TextEdit edit) {
|
||||
private void editInEditor (final IDEEditor editor, final TextEdit edit) {
|
||||
final Range range = edit.getRange();
|
||||
final int startLine = range.getStart().getLine();
|
||||
final int startCol = range.getStart().getColumn ();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/************************************************************************************
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
@@ -14,16 +14,18 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
**************************************************************************************/
|
||||
*/
|
||||
package com.itsaky.androidide.models;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.itsaky.androidide.R;
|
||||
import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE;
|
||||
|
||||
import java.util.Locale;
|
||||
import io.github.rosemoe.editor.syntax.EditorColorScheme;
|
||||
|
||||
public class LogLine {
|
||||
|
||||
@@ -42,14 +44,10 @@ public class LogLine {
|
||||
public boolean formatted;
|
||||
|
||||
// It makes things easier in LogLanguageImpl
|
||||
public static final int INFO = EditorColorScheme.LOG_INFO;
|
||||
public static final int DEBUG = EditorColorScheme.LOG_DEBUG;
|
||||
public static final int ERROR = EditorColorScheme.LOG_ERROR;
|
||||
public static final int WARNING = EditorColorScheme.LOG_WARNING;
|
||||
|
||||
public LogLine(@NonNull LogLine src ) {
|
||||
this(src.date, src.time, src.pid, src.tid, src.priorityChar, src.tag, src.message);
|
||||
}
|
||||
public static final int INFO = SchemeAndroidIDE.LOG_INFO;
|
||||
public static final int DEBUG = SchemeAndroidIDE.LOG_DEBUG;
|
||||
public static final int ERROR = SchemeAndroidIDE.LOG_ERROR;
|
||||
public static final int WARNING = SchemeAndroidIDE.LOG_WARNING;
|
||||
|
||||
public LogLine (String date, String time, String pid, String tid, String priorityChar, String tag, String message) {
|
||||
this (date, time, pid, tid, priorityChar, tag, message, true);
|
||||
@@ -122,6 +120,7 @@ public class LogLine {
|
||||
return ContextCompat.getColor(ctx, id);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.formatted ? String.format("%-6s %-13s %-6s %-6s %-2s %-40s %s", date, time, pid, tid, priorityChar, tag, message) : this.unformatted;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
/************************************************************************************
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
*
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
@@ -16,21 +14,58 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
**************************************************************************************/
|
||||
*/
|
||||
package com.itsaky.androidide.syntax.colorschemes;
|
||||
|
||||
import io.github.rosemoe.editor.syntax.EditorColorScheme;
|
||||
import org.w3c.dom.Text;
|
||||
|
||||
import io.github.rosemoe.sora.lang.styling.TextStyle;
|
||||
import io.github.rosemoe.sora.widget.schemes.EditorColorScheme;
|
||||
|
||||
/**
|
||||
* Base class for editor color schemes in AndroidIDE.
|
||||
* If you're implementing a color scheme for the IDE,
|
||||
* this should be the base class instead of {@link EditorColorScheme}.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
public class SchemeAndroidIDE extends EditorColorScheme {
|
||||
|
||||
private static int current = EditorColorScheme.END_COLOR_ID;
|
||||
|
||||
public static final int LOG_INFO = current++;
|
||||
public static final int LOG_DEBUG = current++;
|
||||
public static final int LOG_ERROR = current++;
|
||||
public static final int LOG_WARNING = current++;
|
||||
public static final int DIAGNOSTIC_ERROR = current++;
|
||||
public static final int DIAGNOSTIC_WARNING = current++;
|
||||
public static final int DIAGNOSTIC_HINT = current++;
|
||||
public static final int DIAGNOSTIC_INFO = current++;
|
||||
public static final int STDERR = current++;
|
||||
public static final int STDOUT = current++;
|
||||
public static final int XML_TAG = current++;
|
||||
public static final int FIELD = current++;
|
||||
public static final int STATIC_FIELD = current++;
|
||||
public static final int PACKAGE_NAME = current++;
|
||||
public static final int ENUM_TYPE = current++;
|
||||
public static final int INTERFACE = current++;
|
||||
public static final int ENUM = current++;
|
||||
public static final int PARAMETER = current++;
|
||||
public static final int CONSTRUCTOR = current++;
|
||||
public static final int STATIC_INIT = current++;
|
||||
public static final int INSTANCE_INIT = current++;
|
||||
public static final int TYPE_PARAM = current++;
|
||||
public static final int RESOURCE_VARIABLE = current++;
|
||||
public static final int EXCEPTION_PARAM = current++;
|
||||
public static final int METHOD_DECLARATION = current++;
|
||||
public static final int METHOD_INVOCATION = current++;
|
||||
public static final int TYPE_NAME = current++;
|
||||
public static final int LOCAL_VARIABLE = current++;
|
||||
public static final int TODO_COMMENT = current++;
|
||||
public static final int FIXME_COMMENT = current++;
|
||||
|
||||
@Override
|
||||
public void applyDefault () {
|
||||
// Change the default comment color
|
||||
// This will make sure that all javadoc elements
|
||||
// which are not implemented in this scheme are
|
||||
// same as other comments
|
||||
COMMENT_DEFAULT = 0xffbdbdbd;
|
||||
|
||||
// Apply default colors
|
||||
super.applyDefault ();
|
||||
|
||||
@@ -58,7 +93,6 @@ public class SchemeAndroidIDE extends EditorColorScheme {
|
||||
setColor (MATCHED_TEXT_BACKGROUND, 0xffFF8F00);
|
||||
setColor (NON_PRINTABLE_CHAR, 0xffdddddd);
|
||||
setColor (KEYWORD, 0xffff6060);
|
||||
setColor (COMMENT, COMMENT_DEFAULT);
|
||||
setColor (OPERATOR, 0xff4fc3f7);
|
||||
setColor (LITERAL, 0xff8bc34a);
|
||||
setColor (TYPE_NAME, 0xff4fc3f7);
|
||||
@@ -88,5 +122,51 @@ public class SchemeAndroidIDE extends EditorColorScheme {
|
||||
|
||||
setColor (STDERR, 0xfff44336);
|
||||
setColor (STDOUT, 0xff4CAF50);
|
||||
|
||||
setColor (TODO_COMMENT, 0xffffc400);
|
||||
setColor (FIXME_COMMENT, 0xffffab00);
|
||||
setColor (COMMENT, 0xffbdbdbd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates to {@link TextStyle#makeStyle(int)}
|
||||
* @param id The color id.
|
||||
* @return The style flags.
|
||||
*/
|
||||
public static long get (int id) {
|
||||
return TextStyle.makeStyle (id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create style for keywords. Convenient method to avoid calling {@link TextStyle#makeStyle(int, int, boolean, boolean, boolean)}
|
||||
* @return The default style for keywords.
|
||||
*/
|
||||
public static long forKeyword () {
|
||||
return TextStyle.makeStyle (KEYWORD, WHOLE_BACKGROUND, true, false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create style for string literals. The returned style sets the {@link TextStyle#NO_COMPLETION_BIT}.
|
||||
* @return The style for string literals.
|
||||
*/
|
||||
public static long forString () {
|
||||
return TextStyle.makeStyle (LITERAL, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create color style for default comments.
|
||||
* @return The style for {@link #COMMENT}.
|
||||
*/
|
||||
public static long forComment () {
|
||||
return TextStyle.makeStyle (COMMENT, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create style for the given id without completions.
|
||||
* @param id The id to create style for.
|
||||
* @return The style for the id.
|
||||
*/
|
||||
public static long withoutCompletion (int id) {
|
||||
return TextStyle.makeStyle (id, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
/************************************************************************************
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
*
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
@@ -15,8 +13,7 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
**************************************************************************************/
|
||||
*/
|
||||
package com.itsaky.androidide.syntax.highlighters;
|
||||
|
||||
import android.text.SpannableStringBuilder;
|
||||
@@ -24,13 +21,14 @@ import android.text.style.BackgroundColorSpan;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import com.itsaky.androidide.lexers.java.JavaLexer;
|
||||
import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE;
|
||||
import io.github.rosemoe.editor.syntax.EditorColorScheme;
|
||||
import java.io.StringReader;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.antlr.v4.runtime.CharStreams;
|
||||
import org.antlr.v4.runtime.Token;
|
||||
|
||||
import io.github.rosemoe.sora.widget.schemes.EditorColorScheme;
|
||||
|
||||
public class JavaHighlighter implements Highlighter {
|
||||
|
||||
@Override
|
||||
@@ -159,7 +157,7 @@ public class JavaHighlighter implements Highlighter {
|
||||
case JavaLexer.INT :
|
||||
case JavaLexer.LONG:
|
||||
case JavaLexer.SHORT:
|
||||
sb.append(token.getText(), new ForegroundColorSpan(scheme.getColor(EditorColorScheme.TYPE_NAME)), SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
sb.append(token.getText(), new ForegroundColorSpan(scheme.getColor(SchemeAndroidIDE.TYPE_NAME)), SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
break;
|
||||
case JavaLexer.BLOCK_COMMENT :
|
||||
case JavaLexer.LINE_COMMENT :
|
||||
|
||||
@@ -26,8 +26,6 @@ import com.itsaky.androidide.tasks.TaskExecutor;
|
||||
import com.itsaky.lsp.models.Position;
|
||||
import com.itsaky.lsp.models.Range;
|
||||
|
||||
import io.github.rosemoe.editor.text.CharPosition;
|
||||
import io.github.rosemoe.editor.text.Content;
|
||||
import java.io.File;
|
||||
import java.io.FileFilter;
|
||||
import java.util.ArrayList;
|
||||
@@ -38,6 +36,9 @@ import java.util.concurrent.Callable;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import io.github.rosemoe.sora.text.CharPosition;
|
||||
import io.github.rosemoe.sora.text.Content;
|
||||
|
||||
/**
|
||||
* This class provides API to search in files recursively
|
||||
*
|
||||
@@ -97,7 +98,7 @@ public class RecursiveFileSearcher {
|
||||
if(file.isDirectory()) continue;
|
||||
final String text = FileIOUtils.readFile2String(file);
|
||||
if(text == null || text.trim().isEmpty()) continue;
|
||||
final Content content = new Content(null, text);
|
||||
final Content content = new Content(text);
|
||||
final List<SearchResult> ranges = new ArrayList<>();
|
||||
Matcher matcher = Pattern.compile(Pattern.quote(this.query)).matcher(text);
|
||||
while(matcher.find()) {
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
/************************************************************************************
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
*
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
**************************************************************************************/
|
||||
|
||||
|
||||
package com.itsaky.androidide.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import io.github.rosemoe.editor.widget.CodeEditor;
|
||||
|
||||
public class Editor extends CodeEditor {
|
||||
|
||||
public Editor(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public Editor(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public Editor(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
this(context, attrs, defStyleAttr, 0);
|
||||
}
|
||||
|
||||
public Editor(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
/************************************************************************************
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
*
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
@@ -16,29 +14,17 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
**************************************************************************************/
|
||||
|
||||
|
||||
*/
|
||||
package com.itsaky.androidide.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.Gravity;
|
||||
import android.widget.Button;
|
||||
import android.widget.HorizontalScrollView;
|
||||
import android.widget.LinearLayout;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.widget.TextViewCompat;
|
||||
import com.itsaky.androidide.R;
|
||||
import com.itsaky.androidide.app.StudioApp;
|
||||
import com.itsaky.androidide.utils.TypefaceUtils;
|
||||
import io.github.rosemoe.editor.widget.CodeEditor;
|
||||
import io.github.rosemoe.editor.widget.SymbolChannel;
|
||||
import android.util.TypedValue;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.itsaky.androidide.adapters.SymbolInputAdapter;
|
||||
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.itsaky.androidide.adapters.SymbolInputAdapter;
|
||||
import com.itsaky.androidide.views.editor.IDEEditor;
|
||||
|
||||
public class SymbolInputView extends RecyclerView {
|
||||
|
||||
@@ -57,7 +43,7 @@ public class SymbolInputView extends RecyclerView {
|
||||
setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false));
|
||||
}
|
||||
|
||||
public void bindEditor(CodeEditor editor) {
|
||||
public void bindEditor(IDEEditor editor) {
|
||||
adapter = new SymbolInputAdapter(editor);
|
||||
setAdapter(adapter);
|
||||
setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false));
|
||||
|
||||
+401
-384
@@ -1,385 +1,402 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.itsaky.androidide.views;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.blankj.utilcode.util.FileIOUtils;
|
||||
import com.blankj.utilcode.util.SizeUtils;
|
||||
import com.itsaky.androidide.adapters.CompletionListAdapter;
|
||||
import com.itsaky.androidide.app.StudioApp;
|
||||
import com.itsaky.androidide.databinding.LayoutCodeEditorBinding;
|
||||
import com.itsaky.androidide.language.groovy.GroovyLanguage;
|
||||
import com.itsaky.androidide.language.java.JavaLanguage;
|
||||
import com.itsaky.androidide.language.xml.XMLLanguage;
|
||||
import com.itsaky.androidide.lexers.xml.XMLLexer;
|
||||
import com.itsaky.androidide.managers.PreferenceManager;
|
||||
import com.itsaky.androidide.models.ConstantsBridge;
|
||||
import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE;
|
||||
import com.itsaky.androidide.utils.LSPUtils;
|
||||
import com.itsaky.androidide.utils.Logger;
|
||||
import com.itsaky.androidide.utils.TypefaceUtils;
|
||||
import com.itsaky.inflater.values.ValuesTableFactory;
|
||||
import com.itsaky.lsp.models.Range;
|
||||
|
||||
import org.antlr.v4.runtime.CharStreams;
|
||||
import org.antlr.v4.runtime.Token;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.StringReader;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import io.github.rosemoe.editor.interfaces.EditorEventListener;
|
||||
import io.github.rosemoe.editor.langs.EmptyLanguage;
|
||||
import io.github.rosemoe.editor.widget.CodeEditor;
|
||||
|
||||
/**
|
||||
* A view that handles opened code editors.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
@SuppressLint("ViewConstructor") // This view is always dynamically created.
|
||||
public class CodeEditorView extends FrameLayout {
|
||||
|
||||
private final File file;
|
||||
private final LayoutCodeEditorBinding binding;
|
||||
|
||||
private boolean isModified;
|
||||
private boolean isFirstCreate;
|
||||
|
||||
private static final Logger LOG = Logger.instance ("CodeEditorView");
|
||||
|
||||
private final EditorEventListener mEventListener = new EditorEventListener () {
|
||||
@Override
|
||||
public boolean onRequestFormat (CodeEditor editor, boolean async) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onFormatFail (CodeEditor editor, Throwable cause) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFormatSucceed (CodeEditor editor) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewTextSet (CodeEditor editor) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeReplace (CodeEditor editor, CharSequence content) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetSelection (int startLine, int startCol, int endLine, int endCol) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterDelete (CodeEditor editor, CharSequence content, int startLine, int startColumn, int endLine, int endColumn, CharSequence deletedContent) {
|
||||
isModified = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterInsert (CodeEditor editor, CharSequence content, int startLine, int startColumn, int endLine, int endColumn, CharSequence insertedContent) {
|
||||
isModified = true;
|
||||
if (file.getName ().endsWith (".xml")) {
|
||||
boolean isOpen = false;
|
||||
try {
|
||||
isOpen = editor.getText ().charAt (editor.getCursor ().getLeft () - 2) == '<';
|
||||
} catch (Throwable th) {
|
||||
LOG.error (th);
|
||||
}
|
||||
|
||||
if (isOpen && insertedContent.toString ().equals ("/")) {
|
||||
closeCurrentTag (editor.getText ().toString (), endLine, endColumn);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public CodeEditorView (@NonNull Context context, @NonNull File file, final @NonNull Range selection) {
|
||||
super (context);
|
||||
this.file = file;
|
||||
this.isModified = false;
|
||||
this.isFirstCreate = true;
|
||||
|
||||
this.binding = LayoutCodeEditorBinding.inflate (LayoutInflater.from (context));
|
||||
this.binding.editor.setOverScrollEnabled (false);
|
||||
this.binding.editor.setTypefaceText (TypefaceUtils.jetbrainsMono ());
|
||||
this.binding.editor.setTextActionMode (CodeEditor.TextActionMode.ACTION_MODE);
|
||||
this.binding.editor.setHighlightCurrentBlock (true);
|
||||
this.binding.editor.setEventListener (mEventListener);
|
||||
this.binding.editor.setAutoCompletionOnComposing (true);
|
||||
this.binding.editor.setAutoCompletionItemAdapter (new CompletionListAdapter ());
|
||||
this.binding.editor.setLineColorsEnabled (true);
|
||||
this.binding.editor.setDividerWidth (SizeUtils.dp2px (1));
|
||||
this.binding.editor.setColorScheme (new SchemeAndroidIDE ());
|
||||
|
||||
this.binding.diagnosticTextContainer.setVisibility (GONE);
|
||||
|
||||
removeAllViews ();
|
||||
addView (this.binding.getRoot ());
|
||||
|
||||
selection.validate();
|
||||
CompletableFuture.runAsync (() -> {
|
||||
final var contents = FileIOUtils.readFile2String (file);
|
||||
binding.editor.post (() -> {
|
||||
binding.editor.setText (contents);
|
||||
postRead ();
|
||||
if (LSPUtils.isEqual (selection.getStart (), selection.getEnd ())) {
|
||||
getEditor ().setSelection (selection.getStart ().getLine (), selection.getStart ().getColumn ());
|
||||
} else {
|
||||
getEditor ().setSelectionRegion (
|
||||
selection.getStart ().getLine (),
|
||||
selection.getStart ().getColumn (),
|
||||
selection.getEnd ().getLine (),
|
||||
selection.getEnd ().getColumn ()
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
configureEditorIfNeeded ();
|
||||
}
|
||||
|
||||
public boolean save () {
|
||||
|
||||
if (getFile () == null) {
|
||||
LOG.error ("Cannot save file. File instance is null.");
|
||||
return false;
|
||||
}
|
||||
|
||||
final var path = getFile ().toPath ();
|
||||
if (!isModified() && Files.exists (path)) {
|
||||
LOG.info (getFile ());
|
||||
LOG.info ("File was not modified. Skipping save operation.");
|
||||
return false;
|
||||
}
|
||||
|
||||
final var lines = new ArrayList<> (getEditor ().getText ().getLines ());
|
||||
try {
|
||||
|
||||
Files.write (getFile ().toPath (), lines);
|
||||
notifySaved ();
|
||||
isModified = false;
|
||||
|
||||
return true;
|
||||
} catch (Throwable e) {
|
||||
LOG.error ("Failed to save file", getFile (), "\n", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public Editor getEditor () {
|
||||
return binding.editor;
|
||||
}
|
||||
|
||||
public LayoutCodeEditorBinding getBinding () {
|
||||
return binding;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public File getFile () {
|
||||
return file;
|
||||
}
|
||||
|
||||
public String getText () {
|
||||
return binding.editor.getText ().toString ();
|
||||
}
|
||||
|
||||
public boolean isModified () {
|
||||
return isModified;
|
||||
}
|
||||
|
||||
public void onPause () {
|
||||
// unimplemented
|
||||
}
|
||||
|
||||
public void onResume () {
|
||||
configureEditorIfNeeded ();
|
||||
}
|
||||
|
||||
public void undo () {
|
||||
if (binding.editor.canUndo ()) {
|
||||
binding.editor.undo ();
|
||||
}
|
||||
}
|
||||
|
||||
public void redo () {
|
||||
if (binding.editor.canRedo ()) {
|
||||
binding.editor.redo ();
|
||||
}
|
||||
}
|
||||
|
||||
public void findDefinition () {
|
||||
binding.editor.findDefinition ();
|
||||
}
|
||||
|
||||
public void findReferences () {
|
||||
binding.editor.findReferences ();
|
||||
}
|
||||
|
||||
public void commentLine () {
|
||||
binding.editor.commentLine ();
|
||||
}
|
||||
|
||||
public void uncommentLine () {
|
||||
binding.editor.uncommentLine ();
|
||||
}
|
||||
|
||||
public void beginSearch () {
|
||||
binding.editor.beginSearchMode ();
|
||||
}
|
||||
|
||||
protected void postRead () {
|
||||
if (file.isFile () && file.getName ().endsWith (".java")) {
|
||||
binding.editor.setEditorLanguage (new JavaLanguage (), StudioApp.getInstance ().getJavaLanguageServer ());
|
||||
} else if (file.isFile () && file.getName ().endsWith (".xml")) {
|
||||
binding.editor.setEditorLanguage (new XMLLanguage (), StudioApp.getInstance ().getXMLLanguageServer ());
|
||||
} else if (file.isFile () && file.getName ().endsWith (".gradle")) {
|
||||
binding.editor.setEditorLanguage (new GroovyLanguage (), null);
|
||||
} else {
|
||||
binding.editor.setEditorLanguage (new EmptyLanguage (), null);
|
||||
}
|
||||
|
||||
// File must be set only after setting the language
|
||||
// This will make sure that textDocument/didOpen is sent
|
||||
binding.editor.setFile (getFile ());
|
||||
}
|
||||
|
||||
private void configureEditorIfNeeded () {
|
||||
boolean sizeChanged = isFirstCreate || ConstantsBridge.EDITOR_PREF_SIZE_CHANGED;
|
||||
boolean flagsChanged = isFirstCreate || ConstantsBridge.EDITOR_PREF_FLAGS_CHANGED;
|
||||
boolean drawHexChanged = isFirstCreate || ConstantsBridge.EDITOR_PREF_DRAW_HEX_CHANGED;
|
||||
final PreferenceManager prefs = StudioApp.getInstance ().getPrefManager ();
|
||||
|
||||
if (sizeChanged) {
|
||||
float textSize = prefs.getFloat (PreferenceManager.KEY_EDITOR_FONT_SIZE);
|
||||
if (textSize < 6 || textSize > 32) {
|
||||
textSize = 14;
|
||||
}
|
||||
|
||||
binding.editor.setTextSize (textSize);
|
||||
ConstantsBridge.EDITOR_PREF_SIZE_CHANGED = false;
|
||||
}
|
||||
|
||||
if (flagsChanged) {
|
||||
int flags = 0;
|
||||
if (prefs.getBoolean (PreferenceManager.KEY_EDITOR_FLAG_WS_LEADING, true)) {
|
||||
flags |= CodeEditor.FLAG_DRAW_WHITESPACE_LEADING;
|
||||
}
|
||||
|
||||
if (prefs.getBoolean (PreferenceManager.KEY_EDITOR_FLAG_WS_TRAILING, false)) {
|
||||
flags |= CodeEditor.FLAG_DRAW_WHITESPACE_TRAILING;
|
||||
}
|
||||
|
||||
if (prefs.getBoolean (PreferenceManager.KEY_EDITOR_FLAG_WS_INNER, true)) {
|
||||
flags |= CodeEditor.FLAG_DRAW_WHITESPACE_INNER;
|
||||
}
|
||||
|
||||
if (prefs.getBoolean (PreferenceManager.KEY_EDITOR_FLAG_WS_EMPTY_LINE, true)) {
|
||||
flags |= CodeEditor.FLAG_DRAW_WHITESPACE_FOR_EMPTY_LINE;
|
||||
}
|
||||
|
||||
if (prefs.getBoolean (PreferenceManager.KEY_EDITOR_FLAG_LINE_BREAK, true)) {
|
||||
flags |= CodeEditor.FLAG_DRAW_LINE_SEPARATOR;
|
||||
}
|
||||
|
||||
binding.editor.setNonPrintablePaintingFlags (flags);
|
||||
ConstantsBridge.EDITOR_PREF_FLAGS_CHANGED = false;
|
||||
}
|
||||
|
||||
if (drawHexChanged) {
|
||||
binding.editor.setLineColorsEnabled (prefs.getBoolean (PreferenceManager.KEY_EDITOR_DRAW_HEX, true));
|
||||
ConstantsBridge.EDITOR_PREF_DRAW_HEX_CHANGED = false;
|
||||
}
|
||||
|
||||
isFirstCreate = false;
|
||||
}
|
||||
|
||||
private void closeCurrentTag (String text, int line, int col) {
|
||||
try {
|
||||
XMLLexer lexer = new XMLLexer (CharStreams.fromReader (new StringReader (text)));
|
||||
Token token;
|
||||
boolean wasSlash = false, wasOpen = false;
|
||||
ArrayList<String> currentNames = new ArrayList<> ();
|
||||
while (((token = lexer.nextToken ()) != null && token.getType () != token.EOF)) {
|
||||
final int type = token.getType ();
|
||||
if (type == XMLLexer.OPEN) {
|
||||
wasOpen = true;
|
||||
} else if (type == XMLLexer.Name) {
|
||||
if (wasOpen && wasSlash && currentNames.size () > 0) {
|
||||
currentNames.remove (0);
|
||||
} else if (wasOpen) {
|
||||
currentNames.add (0, token.getText ());
|
||||
wasOpen = false;
|
||||
}
|
||||
} else if (type == XMLLexer.OPEN_SLASH) {
|
||||
int l = token.getLine () - 1;
|
||||
int c = token.getCharPositionInLine ();
|
||||
if (l == line && c == col) {
|
||||
break;
|
||||
} else if (currentNames.size () > 0) {
|
||||
currentNames.remove (0);
|
||||
}
|
||||
} else if (type == XMLLexer.SLASH_CLOSE
|
||||
|| type == XMLLexer.SPECIAL_CLOSE) {
|
||||
if (currentNames.size () > 0 && token.getText ().trim ().endsWith ("/>")) {
|
||||
currentNames.remove (0);
|
||||
}
|
||||
} else if (type == XMLLexer.SLASH) {
|
||||
wasSlash = true;
|
||||
} else {
|
||||
wasOpen = wasSlash = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentNames.size () > 0) {
|
||||
binding.editor.getText ().insert (line, col + 2, currentNames.get (0));
|
||||
}
|
||||
} catch (Throwable th) {
|
||||
LOG.error ("Unable to close current tag", th);
|
||||
}
|
||||
}
|
||||
|
||||
private void notifySaved () {
|
||||
binding.editor.didSave ();
|
||||
|
||||
if (Objects.requireNonNull (getFile ()).getName ().endsWith (".xml")) {
|
||||
ValuesTableFactory.syncWithFile (getFile ());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this files as saved. Even if it not saved.
|
||||
*/
|
||||
public void markAsSaved () {
|
||||
isModified = false;
|
||||
notifySaved ();
|
||||
}
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.itsaky.androidide.views.editor;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.blankj.utilcode.util.FileIOUtils;
|
||||
import com.blankj.utilcode.util.SizeUtils;
|
||||
import com.itsaky.androidide.adapters.CompletionListAdapter;
|
||||
import com.itsaky.androidide.app.StudioApp;
|
||||
import com.itsaky.androidide.databinding.LayoutCodeEditorBinding;
|
||||
import com.itsaky.androidide.language.groovy.GroovyLanguage;
|
||||
import com.itsaky.androidide.language.java.JavaLanguage;
|
||||
import com.itsaky.androidide.language.xml.XMLLanguage;
|
||||
import com.itsaky.androidide.lexers.xml.XMLLexer;
|
||||
import com.itsaky.androidide.managers.PreferenceManager;
|
||||
import com.itsaky.androidide.models.ConstantsBridge;
|
||||
import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE;
|
||||
import com.itsaky.androidide.utils.LSPUtils;
|
||||
import com.itsaky.androidide.utils.Logger;
|
||||
import com.itsaky.androidide.utils.TypefaceUtils;
|
||||
import com.itsaky.androidide.views.editor.IDEEditor;
|
||||
import com.itsaky.inflater.values.ValuesTableFactory;
|
||||
import com.itsaky.lsp.models.Range;
|
||||
|
||||
import org.antlr.v4.runtime.CharStreams;
|
||||
import org.antlr.v4.runtime.Token;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.StringReader;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import io.github.rosemoe.sora.event.ContentChangeEvent;
|
||||
import io.github.rosemoe.sora.lang.EmptyLanguage;
|
||||
import io.github.rosemoe.sora.text.Content;
|
||||
|
||||
/**
|
||||
* A view that handles opened code editors.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
@SuppressLint("ViewConstructor") // This view is always dynamically created.
|
||||
public class CodeEditorView extends FrameLayout {
|
||||
|
||||
private final File file;
|
||||
private final LayoutCodeEditorBinding binding;
|
||||
|
||||
private boolean isModified;
|
||||
private boolean isFirstCreate;
|
||||
|
||||
private static final Logger LOG = Logger.instance ("CodeEditorView");
|
||||
|
||||
public CodeEditorView (@NonNull Context context, @NonNull File file, final @NonNull Range selection) {
|
||||
super (context);
|
||||
this.file = file;
|
||||
this.isModified = false;
|
||||
this.isFirstCreate = true;
|
||||
|
||||
this.binding = LayoutCodeEditorBinding.inflate (LayoutInflater.from (context));
|
||||
this.binding.editor.setTypefaceText (TypefaceUtils.jetbrainsMono ());
|
||||
this.binding.editor.setHighlightCurrentBlock (true);
|
||||
this.binding.editor.getProps().autoCompletionOnComposing = true;
|
||||
this.binding.editor.setAutoCompletionItemAdapter (new CompletionListAdapter ());
|
||||
this.binding.editor.setDividerWidth (SizeUtils.dp2px (1));
|
||||
this.binding.editor.setColorScheme (new SchemeAndroidIDE ());
|
||||
this.binding.editor.subscribeEvent (ContentChangeEvent.class, ((event, unsubscribe) -> handleContentChange (event)));
|
||||
|
||||
this.binding.diagnosticTextContainer.setVisibility (GONE);
|
||||
|
||||
removeAllViews ();
|
||||
addView (this.binding.getRoot ());
|
||||
|
||||
selection.validate();
|
||||
CompletableFuture.runAsync (() -> {
|
||||
final var contents = FileIOUtils.readFile2String (file);
|
||||
binding.editor.post (() -> {
|
||||
binding.editor.setText (contents, createEditorArgs());
|
||||
postRead ();
|
||||
if (LSPUtils.isEqual (selection.getStart (), selection.getEnd ())) {
|
||||
getEditor ().setSelection (selection.getStart ().getLine (), selection.getStart ().getColumn ());
|
||||
} else {
|
||||
getEditor ().setSelectionRegion (
|
||||
selection.getStart ().getLine (),
|
||||
selection.getStart ().getColumn (),
|
||||
selection.getEnd ().getLine (),
|
||||
selection.getEnd ().getColumn ()
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
configureEditorIfNeeded ();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Bundle createEditorArgs () {
|
||||
var bundle = new Bundle ();
|
||||
if (getFile () == null) {
|
||||
return bundle;
|
||||
}
|
||||
|
||||
bundle.putString (IDEEditor.KEY_FILE, getFile ().getAbsolutePath ());
|
||||
return bundle;
|
||||
}
|
||||
|
||||
private void handleContentChange (@NonNull ContentChangeEvent event) {
|
||||
isModified = true;
|
||||
|
||||
if (event.getAction () == ContentChangeEvent.ACTION_INSERT) {
|
||||
final var editor = event.getEditor ();
|
||||
final var content = event.getChangedText ();
|
||||
final var endLine = event.getChangeEnd ().line;
|
||||
final var endColumn = event.getChangeEnd ().column;
|
||||
if (file.getName ().endsWith (".xml")) {
|
||||
boolean isOpen = false;
|
||||
try {
|
||||
isOpen = editor.getText ().charAt (editor.getCursor ().getLeft () - 2) == '<';
|
||||
} catch (Throwable th) {
|
||||
LOG.error (th);
|
||||
}
|
||||
|
||||
if (isOpen && "/".contentEquals (content)) {
|
||||
closeCurrentTag (editor.getText ().toString (), endLine, endColumn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean save () {
|
||||
|
||||
if (getFile () == null) {
|
||||
LOG.error ("Cannot save file. File instance is null.");
|
||||
return false;
|
||||
}
|
||||
|
||||
final var path = getFile ().toPath ();
|
||||
if (!isModified() && Files.exists (path)) {
|
||||
LOG.info (getFile ());
|
||||
LOG.info ("File was not modified. Skipping save operation.");
|
||||
return false;
|
||||
}
|
||||
|
||||
final var lines = getLines (getEditor ().getText ());
|
||||
try {
|
||||
|
||||
Files.write (getFile ().toPath (), lines);
|
||||
notifySaved ();
|
||||
isModified = false;
|
||||
|
||||
return true;
|
||||
} catch (Throwable e) {
|
||||
LOG.error ("Failed to save file", getFile (), "\n", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private List<CharSequence> getLines (Content text) {
|
||||
final var count = text.getLineCount ();
|
||||
final var result = new ArrayList<CharSequence> ();
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
result.add (text.getLine (i));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public IDEEditor getEditor () {
|
||||
return binding.editor;
|
||||
}
|
||||
|
||||
public LayoutCodeEditorBinding getBinding () {
|
||||
return binding;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public File getFile () {
|
||||
return file;
|
||||
}
|
||||
|
||||
public String getText () {
|
||||
return binding.editor.getText ().toString ();
|
||||
}
|
||||
|
||||
public boolean isModified () {
|
||||
return isModified;
|
||||
}
|
||||
|
||||
public void onPause () {
|
||||
// unimplemented
|
||||
}
|
||||
|
||||
public void onResume () {
|
||||
configureEditorIfNeeded ();
|
||||
}
|
||||
|
||||
public void undo () {
|
||||
if (binding.editor.canUndo ()) {
|
||||
binding.editor.undo ();
|
||||
}
|
||||
}
|
||||
|
||||
public void redo () {
|
||||
if (binding.editor.canRedo ()) {
|
||||
binding.editor.redo ();
|
||||
}
|
||||
}
|
||||
|
||||
public void findDefinition () {
|
||||
binding.editor.findDefinition ();
|
||||
}
|
||||
|
||||
public void findReferences () {
|
||||
binding.editor.findReferences ();
|
||||
}
|
||||
|
||||
public void commentLine () {
|
||||
binding.editor.commentLine ();
|
||||
}
|
||||
|
||||
public void uncommentLine () {
|
||||
binding.editor.uncommentLine ();
|
||||
}
|
||||
|
||||
public void beginSearch () {
|
||||
binding.editor.beginSearchMode ();
|
||||
}
|
||||
|
||||
protected void postRead () {
|
||||
if (file.isFile () && file.getName ().endsWith (".java")) {
|
||||
binding.editor.setEditorLanguage (new JavaLanguage ());
|
||||
binding.editor.setLanguageServer (StudioApp.getInstance ().getJavaLanguageServer ());
|
||||
} else if (file.isFile () && file.getName ().endsWith (".xml")) {
|
||||
binding.editor.setEditorLanguage (new XMLLanguage ());
|
||||
binding.editor.setLanguageServer (StudioApp.getInstance ().getXMLLanguageServer ());
|
||||
} else if (file.isFile () && file.getName ().endsWith (".gradle")) {
|
||||
binding.editor.setEditorLanguage (new GroovyLanguage ());
|
||||
} else {
|
||||
binding.editor.setEditorLanguage (new EmptyLanguage ());
|
||||
}
|
||||
|
||||
// File must be set only after setting the language server
|
||||
// This will make sure that textDocument/didOpen is sent
|
||||
binding.editor.setFile (getFile ());
|
||||
}
|
||||
|
||||
private void configureEditorIfNeeded () {
|
||||
boolean sizeChanged = isFirstCreate || ConstantsBridge.EDITOR_PREF_SIZE_CHANGED;
|
||||
boolean flagsChanged = isFirstCreate || ConstantsBridge.EDITOR_PREF_FLAGS_CHANGED;
|
||||
boolean drawHexChanged = isFirstCreate || ConstantsBridge.EDITOR_PREF_DRAW_HEX_CHANGED;
|
||||
final PreferenceManager prefs = StudioApp.getInstance ().getPrefManager ();
|
||||
|
||||
if (sizeChanged) {
|
||||
float textSize = prefs.getFloat (PreferenceManager.KEY_EDITOR_FONT_SIZE);
|
||||
if (textSize < 6 || textSize > 32) {
|
||||
textSize = 14;
|
||||
}
|
||||
|
||||
binding.editor.setTextSize (textSize);
|
||||
ConstantsBridge.EDITOR_PREF_SIZE_CHANGED = false;
|
||||
}
|
||||
|
||||
if (flagsChanged) {
|
||||
int flags = 0;
|
||||
if (prefs.getBoolean (PreferenceManager.KEY_EDITOR_FLAG_WS_LEADING, true)) {
|
||||
flags |= IDEEditor.FLAG_DRAW_WHITESPACE_LEADING;
|
||||
}
|
||||
|
||||
if (prefs.getBoolean (PreferenceManager.KEY_EDITOR_FLAG_WS_TRAILING, false)) {
|
||||
flags |= IDEEditor.FLAG_DRAW_WHITESPACE_TRAILING;
|
||||
}
|
||||
|
||||
if (prefs.getBoolean (PreferenceManager.KEY_EDITOR_FLAG_WS_INNER, true)) {
|
||||
flags |= IDEEditor.FLAG_DRAW_WHITESPACE_INNER;
|
||||
}
|
||||
|
||||
if (prefs.getBoolean (PreferenceManager.KEY_EDITOR_FLAG_WS_EMPTY_LINE, true)) {
|
||||
flags |= IDEEditor.FLAG_DRAW_WHITESPACE_FOR_EMPTY_LINE;
|
||||
}
|
||||
|
||||
if (prefs.getBoolean (PreferenceManager.KEY_EDITOR_FLAG_LINE_BREAK, true)) {
|
||||
flags |= IDEEditor.FLAG_DRAW_LINE_SEPARATOR;
|
||||
}
|
||||
|
||||
binding.editor.setNonPrintablePaintingFlags (flags);
|
||||
ConstantsBridge.EDITOR_PREF_FLAGS_CHANGED = false;
|
||||
}
|
||||
|
||||
if (drawHexChanged) {
|
||||
// TODO Do something about this
|
||||
// Maybe use spans...
|
||||
// binding.editor.setLineColorsEnabled (prefs.getBoolean (PreferenceManager.KEY_EDITOR_DRAW_HEX, true));
|
||||
ConstantsBridge.EDITOR_PREF_DRAW_HEX_CHANGED = false;
|
||||
}
|
||||
|
||||
isFirstCreate = false;
|
||||
}
|
||||
|
||||
private void closeCurrentTag (String text, int line, int col) {
|
||||
try {
|
||||
XMLLexer lexer = new XMLLexer (CharStreams.fromReader (new StringReader (text)));
|
||||
Token token;
|
||||
boolean wasSlash = false, wasOpen = false;
|
||||
ArrayList<String> currentNames = new ArrayList<> ();
|
||||
while (((token = lexer.nextToken ()) != null && token.getType () != token.EOF)) {
|
||||
final int type = token.getType ();
|
||||
if (type == XMLLexer.OPEN) {
|
||||
wasOpen = true;
|
||||
} else if (type == XMLLexer.Name) {
|
||||
if (wasOpen && wasSlash && currentNames.size () > 0) {
|
||||
currentNames.remove (0);
|
||||
} else if (wasOpen) {
|
||||
currentNames.add (0, token.getText ());
|
||||
wasOpen = false;
|
||||
}
|
||||
} else if (type == XMLLexer.OPEN_SLASH) {
|
||||
int l = token.getLine () - 1;
|
||||
int c = token.getCharPositionInLine ();
|
||||
if (l == line && c == col) {
|
||||
break;
|
||||
} else if (currentNames.size () > 0) {
|
||||
currentNames.remove (0);
|
||||
}
|
||||
} else if (type == XMLLexer.SLASH_CLOSE
|
||||
|| type == XMLLexer.SPECIAL_CLOSE) {
|
||||
if (currentNames.size () > 0 && token.getText ().trim ().endsWith ("/>")) {
|
||||
currentNames.remove (0);
|
||||
}
|
||||
} else if (type == XMLLexer.SLASH) {
|
||||
wasSlash = true;
|
||||
} else {
|
||||
wasOpen = wasSlash = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentNames.size () > 0) {
|
||||
binding.editor.getText ().insert (line, col + 2, currentNames.get (0));
|
||||
}
|
||||
} catch (Throwable th) {
|
||||
LOG.error ("Unable to close current tag", th);
|
||||
}
|
||||
}
|
||||
|
||||
private void notifySaved () {
|
||||
binding.editor.didSave ();
|
||||
|
||||
if (Objects.requireNonNull (getFile ()).getName ().endsWith (".xml")) {
|
||||
ValuesTableFactory.syncWithFile (getFile ());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this files as saved. Even if it not saved.
|
||||
*/
|
||||
public void markAsSaved () {
|
||||
isModified = false;
|
||||
notifySaved ();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,612 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.itsaky.androidide.views.editor;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.blankj.utilcode.util.ThreadUtils;
|
||||
import com.itsaky.androidide.R;
|
||||
import com.itsaky.androidide.app.StudioApp;
|
||||
import com.itsaky.androidide.lsp.IDELanguageClientImpl;
|
||||
import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE;
|
||||
import com.itsaky.androidide.utils.Logger;
|
||||
import com.itsaky.lsp.api.ILanguageServer;
|
||||
import com.itsaky.lsp.models.CodeActionParams;
|
||||
import com.itsaky.lsp.models.CodeActionResult;
|
||||
import com.itsaky.lsp.models.DefinitionResult;
|
||||
import com.itsaky.lsp.models.DiagnosticItem;
|
||||
import com.itsaky.lsp.models.DocumentChangeEvent;
|
||||
import com.itsaky.lsp.models.DocumentCloseEvent;
|
||||
import com.itsaky.lsp.models.DocumentOpenEvent;
|
||||
import com.itsaky.lsp.models.DocumentSaveEvent;
|
||||
import com.itsaky.lsp.models.ExpandSelectionParams;
|
||||
import com.itsaky.lsp.models.Position;
|
||||
import com.itsaky.lsp.models.Range;
|
||||
import com.itsaky.lsp.models.ReferenceParams;
|
||||
import com.itsaky.lsp.models.ReferenceResult;
|
||||
import com.itsaky.lsp.models.ShowDocumentParams;
|
||||
import com.itsaky.lsp.models.SignatureHelp;
|
||||
import com.itsaky.lsp.models.SignatureHelpParams;
|
||||
import com.itsaky.toaster.Toaster;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import io.github.rosemoe.sora.event.ContentChangeEvent;
|
||||
import io.github.rosemoe.sora.widget.CodeEditor;
|
||||
import io.github.rosemoe.sora.widget.layout.Layout;
|
||||
|
||||
public class IDEEditor extends CodeEditor {
|
||||
|
||||
private int mFileVersion;
|
||||
private File file;
|
||||
|
||||
private ILanguageServer mLanguageServer;
|
||||
private IDELanguageClientImpl mLanguageClient;
|
||||
private SignatureHelpWindow mSignatureHelpWindow;
|
||||
|
||||
public static final String KEY_FILE = "editor_file";
|
||||
private static final Logger LOG = Logger.instance ("IDEEditor");
|
||||
|
||||
public IDEEditor (Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public IDEEditor (Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public IDEEditor (Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
this(context, attrs, defStyleAttr, 0);
|
||||
}
|
||||
|
||||
public IDEEditor (Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
|
||||
setColorScheme (new SchemeAndroidIDE ());
|
||||
subscribeEvent (ContentChangeEvent.class, (event, unsubscribe) -> notifyContentChanged ());
|
||||
|
||||
// default editor input type + no suggestions flag
|
||||
setInputType (EditorInfo.TYPE_CLASS_TEXT
|
||||
| EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE
|
||||
| EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS
|
||||
| EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set selection to the given range.
|
||||
* @param range The range to select.
|
||||
*/
|
||||
public void setSelection (@NonNull Range range) {
|
||||
setSelectionRegion (range.getStart ().getLine (),
|
||||
range.getStart ().getColumn (),
|
||||
range.getEnd ().getLine (),
|
||||
range.getEnd ().getColumn ());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the file that this editor is currently editing.
|
||||
* @param file The file to set.
|
||||
*/
|
||||
public void setFile (File file) {
|
||||
this.file = file;
|
||||
|
||||
if (file != null && mLanguageServer != null) {
|
||||
final var text = getText ().toString ();
|
||||
final var event = new DocumentOpenEvent (file.toPath (), text, mFileVersion = 0);
|
||||
mLanguageServer.getDocumentHandler ().onFileOpened (event);
|
||||
}
|
||||
|
||||
if (file != null) {
|
||||
getExtraArguments ().putString (KEY_FILE, file.getAbsolutePath ());
|
||||
} else {
|
||||
getExtraArguments ().remove (KEY_FILE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file that this editor is currently editing.
|
||||
* @return The file instance.
|
||||
*/
|
||||
public File getFile () {
|
||||
return file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the language server that this editor will connect with.
|
||||
* If the language client is not set, it'll be set to {@link IDELanguageClientImpl}.
|
||||
*
|
||||
* @param server The server to set. Provide <code>null</code> to
|
||||
* disable all the language server features.
|
||||
*/
|
||||
public void setLanguageServer (ILanguageServer server) {
|
||||
this.mLanguageServer = server;
|
||||
|
||||
if (mLanguageClient == null && IDELanguageClientImpl.isInitialized ()) {
|
||||
mLanguageClient = IDELanguageClientImpl.getInstance ();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append the given text at the end of the editor's content.
|
||||
* @param text The text to append.
|
||||
* @return The line at which the text was appended.
|
||||
*/
|
||||
public int append (CharSequence text) {
|
||||
final var content = getText ();
|
||||
if (getLineCount () <= 0) {
|
||||
return 0;
|
||||
}
|
||||
final int line = getLineCount () - 1;
|
||||
int col = content.getColumnCount (line);
|
||||
if (col < 0) {
|
||||
col = 0;
|
||||
}
|
||||
content.insert (line, col, text);
|
||||
return line;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the selection of the editor's cursor
|
||||
* to the last line of the it's content.
|
||||
*/
|
||||
public void goToEnd () {
|
||||
final var line = getText ().getLineCount () - 1;
|
||||
setSelection (line, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see CodeEditor#updateCursorAnchor()
|
||||
*/
|
||||
public float updateCursorAnchor () {
|
||||
return super.updateCursorAnchor ();
|
||||
}
|
||||
|
||||
/**
|
||||
* The the coordinates of the left cursor.
|
||||
* @return A <code>float[]</code> containing the cursor position.
|
||||
*/
|
||||
public float[] getCursorPosition () {
|
||||
return getLayout().getCharLayoutOffset (getCursor ().getLeftLine (), getCursor ().getLeftColumn ());
|
||||
}
|
||||
|
||||
/**
|
||||
* Comment the line of the left cursor.
|
||||
*/
|
||||
public void commentLine () {
|
||||
if (getFile () == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final var text = getText ();
|
||||
final var name = getFile ().getName ();
|
||||
int line = getCursor ().getLeftLine ();
|
||||
if (name.endsWith (".java") || name.endsWith (".gradle")) {
|
||||
while (line >= getCursor ().getLeftLine () && line <= getCursor ().getRightLine ()) {
|
||||
if (!text.getLineString (line).trim ().startsWith ("//")) {
|
||||
text.insert (line, 0, "//");
|
||||
}
|
||||
line++;
|
||||
}
|
||||
} else if (name.endsWith (".xml")) {
|
||||
while (line >= getCursor ().getLeftLine () && line <= getCursor ().getRightLine ()) {
|
||||
final String lineString = text.getLineString (line);
|
||||
if (!lineString.trim ().startsWith ("<!--")
|
||||
&& !lineString.trim ().endsWith ("-->")) {
|
||||
text.replace (line, 0, line, text.getColumnCount (line), "<!--".concat (lineString).concat ("-->"));
|
||||
}
|
||||
line++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uncomment the current line
|
||||
*/
|
||||
public void uncommentLine () {
|
||||
if (getFile () == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final var text = getText ();
|
||||
final String name = getFile ().getName ();
|
||||
int line = getCursor ().getLeftLine ();
|
||||
if (name.endsWith (".java") || name.endsWith (".gradle")) {
|
||||
while (line >= getCursor ().getLeftLine () && line <= getCursor ().getRightLine ()) {
|
||||
String l = text.getLineString (line);
|
||||
if (l.trim ().startsWith ("//")) {
|
||||
int i = l.indexOf ("//");
|
||||
text.delete (line, i, line, i + 2);
|
||||
}
|
||||
line++;
|
||||
}
|
||||
} else if (name.endsWith (".xml")) {
|
||||
final String commentStart = "<!--";
|
||||
final String commentEnd = "-->";
|
||||
while (line >= getCursor ().getLeftLine () && line <= getCursor ().getRightLine ()) {
|
||||
String l = text.getLineString (line);
|
||||
if (l.trim ().startsWith (commentStart)) {
|
||||
int i = l.indexOf (commentStart);
|
||||
text.delete (line, i, line, i + commentStart.length ());
|
||||
}
|
||||
if (l.trim ().endsWith (commentEnd)) {
|
||||
int count = text.getColumnCount (line);
|
||||
text.delete (line, count - commentEnd.length (), line, count);
|
||||
}
|
||||
line++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If any language server is set, asks the language server
|
||||
* to find the definition of token at the cursor position.
|
||||
* <p>
|
||||
* If the server returns a valid response, and the file specified
|
||||
* in the response is same the file in this editor,
|
||||
* the range specified in the response will be selected.
|
||||
* </p>
|
||||
*/
|
||||
@SuppressWarnings ("deprecation")
|
||||
public void findDefinition () {
|
||||
if (getFile () == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final ProgressDialog pd = ProgressDialog.show (getContext (), null, getContext ().getString (R.string.msg_finding_definition));
|
||||
|
||||
try {
|
||||
final CompletableFuture<DefinitionResult> future = CompletableFuture.supplyAsync (() -> {
|
||||
final var provider = mLanguageServer.getDefinitionProvider ();
|
||||
final var params = new com.itsaky.lsp.models.DefinitionParams (
|
||||
getFile ().toPath (),
|
||||
new com.itsaky.lsp.models.Position (
|
||||
getCursor ().getLeftLine (),
|
||||
getCursor ().getLeftColumn ()
|
||||
)
|
||||
);
|
||||
|
||||
return provider.findDefinitions (params);
|
||||
});
|
||||
|
||||
future.whenComplete ((result, error) -> {
|
||||
|
||||
if (result == null
|
||||
|| mLanguageClient == null
|
||||
|| future.isCancelled ()
|
||||
|| future.isCompletedExceptionally ()
|
||||
) {
|
||||
LOG.error ("An error occurred while finding definition", error);
|
||||
showDefinitionNotFound (pd);
|
||||
return;
|
||||
}
|
||||
|
||||
final var locations = result.getLocations ();
|
||||
if (locations.size () <= 0) {
|
||||
LOG.error ("No definitions found", "Size:", locations.size ());
|
||||
showDefinitionNotFound (pd);
|
||||
return;
|
||||
}
|
||||
|
||||
ThreadUtils.runOnUiThread (() -> {
|
||||
if (locations.size () == 1) {
|
||||
var location = locations.get (0);
|
||||
if (location.getFile ().equals (getFile ().toPath ())) {
|
||||
setSelection (location.getRange ());
|
||||
return;
|
||||
}
|
||||
mLanguageClient.showDocument (new ShowDocumentParams (location.getFile (), location.getRange ()));
|
||||
} else {
|
||||
mLanguageClient.showLocations (locations);
|
||||
}
|
||||
});
|
||||
|
||||
dismissOnUiThread (pd);
|
||||
});
|
||||
} catch (Throwable th) {
|
||||
LOG.error ("An error occurred while finding definition", th);
|
||||
showDefinitionNotFound (pd);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the user that no definitions can be found.
|
||||
* @param pd The {@link ProgressDialog} that was shown when requesting the definitions.
|
||||
*/
|
||||
@SuppressWarnings ("deprecation")
|
||||
private void showDefinitionNotFound (final ProgressDialog pd) {
|
||||
ThreadUtils.runOnUiThread (() -> {
|
||||
StudioApp.getInstance ().toast (R.string.msg_no_definition, Toaster.Type.ERROR);
|
||||
pd.dismiss ();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* If any language server instance is set,
|
||||
* finds the references to of the token at the current cursor position.
|
||||
*
|
||||
* If the server returns a valid response, that response is forwarded
|
||||
* to the {@link IDELanguageClientImpl}.
|
||||
*/
|
||||
public void findReferences () {
|
||||
if (getFile () == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@SuppressWarnings ("deprecation")
|
||||
final ProgressDialog pd = ProgressDialog.show (getContext (), null, getContext ().getString (R.string.msg_finding_references));
|
||||
|
||||
try {
|
||||
final CompletableFuture<ReferenceResult> future = CompletableFuture.supplyAsync (() -> {
|
||||
final var provider = mLanguageServer.getReferenceProvider ();
|
||||
final var referenceParams = new ReferenceParams (
|
||||
getFile ().toPath (),
|
||||
new com.itsaky.lsp.models.Position (
|
||||
getCursor ().getLeftLine (),
|
||||
getCursor ().getLeftColumn ()),
|
||||
true);
|
||||
return provider.findReferences (referenceParams);
|
||||
});
|
||||
|
||||
future.whenComplete ((result, error) -> {
|
||||
|
||||
if (result == null
|
||||
|| mLanguageClient == null
|
||||
|| future.isCancelled ()
|
||||
|| future.isCompletedExceptionally ()
|
||||
) {
|
||||
LOG.error ("An error occurred while finding references", error);
|
||||
showReferencesNotFound (pd);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.getLocations ().isEmpty ()) {
|
||||
showReferencesNotFound (pd);
|
||||
return;
|
||||
} else {
|
||||
if (result.getLocations ().size () == 1) {
|
||||
final var loc = result.getLocations ().get (0);
|
||||
if (loc.getFile ().equals (getFile ().toPath ())) {
|
||||
setSelection (loc.getRange ());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ThreadUtils.runOnUiThread (() -> mLanguageClient.showLocations (result.getLocations ()));
|
||||
}
|
||||
|
||||
dismissOnUiThread (pd);
|
||||
});
|
||||
} catch (Throwable th) {
|
||||
LOG.error ("An error occurred while finding references", th);
|
||||
showReferencesNotFound (pd);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the user that no references were found for the selected token.
|
||||
* @param pd The {@link ProgressDialog} that was shown when requesting references.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
private void showReferencesNotFound (final ProgressDialog pd) {
|
||||
ThreadUtils.runOnUiThread (() -> {
|
||||
StudioApp.getInstance ().toast (R.string.msg_no_references, Toaster.Type.ERROR);
|
||||
pd.dismiss ();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* If any language server is set, requests signature help at the cursor's position.
|
||||
* On a valid response, shows the signature help in a popup window.
|
||||
*/
|
||||
@SuppressWarnings("unused") // accessed using reflection in CompletionItem class
|
||||
public void signatureHelp () {
|
||||
if (mLanguageServer != null && getFile () != null) {
|
||||
final CompletableFuture<SignatureHelp> future = CompletableFuture.supplyAsync (() -> {
|
||||
final var provider = mLanguageServer.getSignatureHelpProvider ();
|
||||
return provider.provideSignatures (new SignatureHelpParams (getFile ().toPath (), new com.itsaky.lsp.models.Position (
|
||||
getCursor ().getLeftLine (),
|
||||
getCursor ().getLeftColumn ()
|
||||
)));
|
||||
});
|
||||
|
||||
future.whenComplete ((help, error) -> {
|
||||
if (help == null
|
||||
|| mLanguageClient == null
|
||||
|| future.isCancelled ()
|
||||
|| future.isCompletedExceptionally ()
|
||||
) {
|
||||
LOG.error ("An error occurred while finding signature help", error);
|
||||
return;
|
||||
}
|
||||
|
||||
ThreadUtils.runOnUiThread (() -> showSignatureHelp (help));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void showSignatureHelp (SignatureHelp help) {
|
||||
getSignatureHelpWindow ().setSignatureHelp (help);
|
||||
getSignatureHelpWindow ().show ();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismisses the given dialog on the UI thread.
|
||||
* @param dialog The dialog to dismiss.
|
||||
*/
|
||||
private void dismissOnUiThread (@NonNull final Dialog dialog) {
|
||||
ThreadUtils.runOnUiThread (dialog::dismiss);
|
||||
}
|
||||
|
||||
/**
|
||||
* If any language server is set, notify the server that the file in this editor was saved.
|
||||
*/
|
||||
public void didSave () {
|
||||
if (mLanguageServer != null && getFile () != null) {
|
||||
mLanguageServer.getDocumentHandler ().onFileSaved (
|
||||
new DocumentSaveEvent (getFile ().toPath ())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the language server that the file in this editor is about to be closed.
|
||||
*/
|
||||
public void close () {
|
||||
if (mLanguageServer != null && getFile () != null) {
|
||||
mLanguageServer.getDocumentHandler ().onFileClosed (
|
||||
new DocumentCloseEvent (getFile ().toPath ())
|
||||
);
|
||||
LOG.info ("'textDocument/didClose' was sent to the language server.");
|
||||
} else {
|
||||
LOG.info ("No language server is available for this file");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request code actions from server at the given position containing given diagnostics
|
||||
*
|
||||
* @return A {@link CompletableFuture}. May return {@code null}.
|
||||
*/
|
||||
public CompletableFuture<CodeActionResult> codeActions () {
|
||||
// TODO Implement this properly
|
||||
return CompletableFuture.completedFuture (new CodeActionResult ());
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests code actions for the given diagnostics to the language server.
|
||||
* @param diagnostics The diagnostics to request code actions for.
|
||||
* @return The {@link CodeActionResult} from the server.
|
||||
*/
|
||||
public CompletableFuture<CodeActionResult> codeActions (List<DiagnosticItem> diagnostics) {
|
||||
if (mLanguageServer == null || mLanguageClient == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return CompletableFuture.supplyAsync (() -> mLanguageServer.getCodeActionProvider ()
|
||||
.codeActions (new CodeActionParams (
|
||||
getFile ().toPath (),
|
||||
getCursorRange (),
|
||||
diagnostics
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the language server to provided a semantically larger selection than the current selection.
|
||||
* If a valid response is received, that range will be selected.
|
||||
*/
|
||||
public void expandSelection () {
|
||||
if (mLanguageServer == null || getFile () == null) {
|
||||
LOG.error ("Cannot expand selection. Language server or file is null");
|
||||
return;
|
||||
}
|
||||
|
||||
//noinspection deprecation
|
||||
final var pd = ProgressDialog.show (getContext (), null, getContext ().getString (R.string.please_wait), true, false);
|
||||
final CompletableFuture<Range> future = CompletableFuture.supplyAsync (() ->
|
||||
mLanguageServer.getSelectionProvider ()
|
||||
.expandSelection (
|
||||
new ExpandSelectionParams (
|
||||
getFile ().toPath (),
|
||||
getCursorRange ()
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
future.whenComplete (((range, throwable) -> {
|
||||
|
||||
pd.dismiss ();
|
||||
|
||||
if (throwable != null) {
|
||||
LOG.error ("Error computing expanded selection range", throwable);
|
||||
return;
|
||||
}
|
||||
|
||||
setSelection (range);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cursor's position in the form of {@link Position}.
|
||||
* @return The {@link Position} of the cursor.
|
||||
*/
|
||||
public Position getCursorAsLSPPosition () {
|
||||
return new Position (getCursor ().getLeftLine (), getCursor ().getLeftColumn ());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cursor's selection range in the form of {@link Range}.
|
||||
* @return The {@link Range} of the cursor.
|
||||
*/
|
||||
public Range getCursorRange () {
|
||||
final var cursor = getCursor ();
|
||||
final var start = new Position (cursor.getLeftLine (), cursor.getLeftColumn ());
|
||||
final var end = new Position (cursor.getRightLine (), cursor.getRightColumn ());
|
||||
return new Range (start, end);
|
||||
}
|
||||
|
||||
// FIXME This is temporary. Find another way to show diagnostic messages in editor.
|
||||
private Layout getLayout () {
|
||||
final var c = CodeEditor.class;
|
||||
final Field f;
|
||||
try {
|
||||
f = c.getDeclaredField ("mLayout");
|
||||
f.setAccessible (true);
|
||||
return (Layout) f.get (this);
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException (e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the language server that the content of this file
|
||||
* has been changed.
|
||||
*/
|
||||
private void notifyContentChanged () {
|
||||
|
||||
if (getFile () == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
StudioApp.getInstance ()
|
||||
.getJavaLanguageServer ()
|
||||
.getDocumentHandler ()
|
||||
.onContentChange (
|
||||
new DocumentChangeEvent (
|
||||
getFile ().toPath (),
|
||||
getText (),
|
||||
mFileVersion + 1
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private SignatureHelpWindow getSignatureHelpWindow () {
|
||||
if (mSignatureHelpWindow == null) {
|
||||
mSignatureHelpWindow = new SignatureHelpWindow (this);
|
||||
}
|
||||
|
||||
return mSignatureHelpWindow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.views.editor;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.GradientDrawable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.style.BackgroundColorSpan;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.util.TypedValue;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.blankj.utilcode.util.SizeUtils;
|
||||
import com.itsaky.androidide.R;
|
||||
import com.itsaky.lsp.models.SignatureHelp;
|
||||
import com.itsaky.lsp.models.SignatureInformation;
|
||||
|
||||
import io.github.rosemoe.sora.widget.CodeEditor;
|
||||
import io.github.rosemoe.sora.widget.base.EditorPopupWindow;
|
||||
|
||||
/**
|
||||
* An {@link EditorPopupWindow} used to show signature help.
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
public class SignatureHelpWindow extends EditorPopupWindow {
|
||||
|
||||
private final TextView text;
|
||||
|
||||
/**
|
||||
* Create a signature help popup window for editor
|
||||
*
|
||||
* @param editor The editor.
|
||||
*/
|
||||
public SignatureHelpWindow (@NonNull CodeEditor editor) {
|
||||
super (editor, getFeatureFlags ());
|
||||
|
||||
final var context = editor.getContext ();
|
||||
final var dp8 = SizeUtils.dp2px (8);
|
||||
final var dp4 = SizeUtils.dp2px (4);
|
||||
|
||||
this.text = new TextView (context);
|
||||
this.text.setBackground (createBackground ());
|
||||
this.text.setTextColor (ContextCompat.getColor (context, R.color.primaryTextColor));
|
||||
this.text.setTextSize (TypedValue.COMPLEX_UNIT_SP, 14);
|
||||
this.text.setClickable (false);
|
||||
this.text.setFocusable (false);
|
||||
this.text.setPaddingRelative (dp8, dp4, dp8, dp4);
|
||||
|
||||
setContentView (this.text);
|
||||
}
|
||||
|
||||
private Drawable createBackground () {
|
||||
GradientDrawable background = new GradientDrawable ();
|
||||
background.setShape (GradientDrawable.RECTANGLE);
|
||||
background.setColor (0xff212121);
|
||||
background.setStroke (1, 0xffffffff);
|
||||
background.setCornerRadius (8);
|
||||
return background;
|
||||
}
|
||||
|
||||
public void setSignatureHelp (SignatureHelp signature) {
|
||||
if (signature == null || signature.getSignatures ().isEmpty ()) {
|
||||
if (isShowing ()) {
|
||||
dismiss ();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
final var signatureText = createSignatureText (signature);
|
||||
|
||||
if (signatureText == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.text.setText (signatureText);
|
||||
}
|
||||
|
||||
private CharSequence createSignatureText (SignatureHelp signature) {
|
||||
final var signatures = signature.getSignatures ();
|
||||
final var activeSignature = signature.getActiveSignature ();
|
||||
final var activeParameter = signature.getActiveParameter ();
|
||||
final SpannableStringBuilder sb = new SpannableStringBuilder ();
|
||||
|
||||
if (activeSignature < 0 || activeParameter < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (activeSignature >= signatures.size ()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// remove all with non-applicable signatures
|
||||
signatures.removeIf (info -> info.getParameters ().size () >= activeParameter);
|
||||
|
||||
for (var i = 0; i<signatures.size (); i++) {
|
||||
final var info = signatures.get (i);
|
||||
sb.append (formatSignature (info, activeParameter, i == activeSignature));
|
||||
sb.append ('\n');
|
||||
|
||||
}
|
||||
|
||||
return sb;
|
||||
}
|
||||
|
||||
private static int getFeatureFlags () {
|
||||
return FEATURE_SCROLL_AS_CONTENT | FEATURE_HIDE_WHEN_FAST_SCROLL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats (highlights) a method signature
|
||||
* @param signature Signature information
|
||||
* @param paramIndex Currently active parameter index
|
||||
* @param isCurrentSignature <code>true</code> if the given signature is the active signature.
|
||||
*/
|
||||
@NonNull
|
||||
private CharSequence formatSignature (@NonNull SignatureInformation signature, int paramIndex, boolean isCurrentSignature) {
|
||||
String name = signature.getLabel();
|
||||
name = name.substring(0, name.indexOf("("));
|
||||
|
||||
SpannableStringBuilder sb = new SpannableStringBuilder();
|
||||
sb.append(name, new ForegroundColorSpan (0xffffffff), SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
sb.append("(", new ForegroundColorSpan(0xff4fc3f7), SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
var params = signature.getParameters();
|
||||
for(int i=0;i<params.size();i++) {
|
||||
int color = i == paramIndex ? 0xffff6060 : 0xffffffff;
|
||||
final var info = params.get(i);
|
||||
if(i == params.size() - 1) {
|
||||
sb.append(info.getLabel() + "", new ForegroundColorSpan(color), SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
} else {
|
||||
sb.append(info.getLabel() + "", new ForegroundColorSpan(color), SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
sb.append(",", new ForegroundColorSpan(0xff4fc3f7), SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
sb.append(" ");
|
||||
}
|
||||
}
|
||||
sb.append(")", new ForegroundColorSpan(0xff4fc3f7), SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
if (isCurrentSignature) {
|
||||
sb.setSpan (new BackgroundColorSpan (Color.parseColor ("#e0e0e0")), 0, sb.length (), SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
return sb;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 Rosemoe
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.github.rosemoe.editor.annotations;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* This annotation marks those fields, methods and constructors experimentally created.
|
||||
* <p>
|
||||
* Methods, fields and constructors with this annotation is very subject to keep or delete.
|
||||
* For that reason, they are not stable for production use.
|
||||
*/
|
||||
@Target({ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
public @interface Experimental {
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 Rosemoe
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.github.rosemoe.editor.interfaces;
|
||||
|
||||
import com.itsaky.lsp.models.CompletionItem;
|
||||
|
||||
import io.github.rosemoe.editor.text.Content;
|
||||
import io.github.rosemoe.editor.text.TextAnalyzeResult;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Interface for auto completion analysis
|
||||
*
|
||||
* @author Rose
|
||||
*/
|
||||
public interface AutoCompleteProvider {
|
||||
|
||||
/**
|
||||
* Analyze auto complete items
|
||||
*
|
||||
*
|
||||
* @param content
|
||||
* @param prefix The prefix of input to match
|
||||
* @param isInCodeBlock Whether auto complete position is in code block
|
||||
* @param colors Last analyze result
|
||||
* @param line The line of cursor
|
||||
* @return Analyzed items
|
||||
*/
|
||||
List<CompletionItem> getAutoCompleteItems(Content content, String fileUri, String prefix, boolean isInCodeBlock, TextAnalyzeResult colors, int index, int line, int column) throws Exception;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 Rosemoe
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.github.rosemoe.editor.interfaces;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.itsaky.lsp.api.ILanguageServer;
|
||||
import com.itsaky.lsp.models.DiagnosticItem;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
import io.github.rosemoe.editor.text.Content;
|
||||
import io.github.rosemoe.editor.text.TextAnalyzeResult;
|
||||
import io.github.rosemoe.editor.text.TextAnalyzer;
|
||||
|
||||
/**
|
||||
* Interface for analyzing highlight
|
||||
*
|
||||
* @author Rose
|
||||
*/
|
||||
public interface CodeAnalyzer {
|
||||
/**
|
||||
* Find the diagnostic containing the specific position
|
||||
*
|
||||
* @param line The line to find
|
||||
* @param column The column to find
|
||||
* @return The found {@link DiagnosticItem} or {@code null}
|
||||
*/
|
||||
DiagnosticItem findDiagnosticContaining(int line, int column);
|
||||
|
||||
/**
|
||||
* Find all diagnostics at the specified line
|
||||
*
|
||||
* @param line The line to find
|
||||
* @return A list containing all diagnostics at the line. Should never be null.
|
||||
*/
|
||||
List<DiagnosticItem> findDiagnosticsContainingLine(int line);
|
||||
|
||||
/**
|
||||
* Analyze spans for the given input
|
||||
*
|
||||
* @param languageServer The language server provided by this language. Maybe null.
|
||||
* @param file The file opened in the editor
|
||||
* @param content The input text
|
||||
* @param colors Result dest
|
||||
* @param delegate Delegate between thread and analyzer
|
||||
* @throws Exception
|
||||
*
|
||||
* @see TextAnalyzer#analyze(Content)
|
||||
* @see TextAnalyzer.AnalyzeThread.Delegate#shouldAnalyze()
|
||||
*/
|
||||
void analyze(@Nullable ILanguageServer languageServer, @Nullable File file, Content content, TextAnalyzeResult colors, TextAnalyzer.AnalyzeThread.Delegate delegate) throws Exception;
|
||||
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 Rosemoe
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.github.rosemoe.editor.interfaces;
|
||||
|
||||
import io.github.rosemoe.editor.text.Content;
|
||||
import io.github.rosemoe.editor.widget.CodeEditor;
|
||||
|
||||
/**
|
||||
* Listener for editor
|
||||
*
|
||||
* @author Rose
|
||||
*/
|
||||
public interface EditorEventListener {
|
||||
|
||||
/**
|
||||
* Before format
|
||||
*
|
||||
* @return true if you want to cancel this operation
|
||||
*/
|
||||
boolean onRequestFormat(CodeEditor editor, boolean async);
|
||||
|
||||
/**
|
||||
* Format failed
|
||||
*
|
||||
* @return true if you do not want editor to handle this exception
|
||||
*/
|
||||
boolean onFormatFail(CodeEditor editor, Throwable cause);
|
||||
|
||||
/**
|
||||
* Format succeeded
|
||||
*/
|
||||
void onFormatSucceed(CodeEditor editor);
|
||||
|
||||
/**
|
||||
* CodeEditor's setText is called
|
||||
*/
|
||||
void onNewTextSet(CodeEditor editor);
|
||||
|
||||
/**
|
||||
* @see io.github.rosemoe.editor.text.ContentListener#afterDelete(Content, int, int, int, int, CharSequence)
|
||||
* Note:do not change content at this time
|
||||
*/
|
||||
void afterDelete(CodeEditor editor, CharSequence content, int startLine, int startColumn, int endLine, int endColumn, CharSequence deletedContent);
|
||||
|
||||
/**
|
||||
* @see io.github.rosemoe.editor.text.ContentListener#afterInsert(Content, int, int, int, int, CharSequence)
|
||||
* Note:do not change content at this time
|
||||
*/
|
||||
void afterInsert(CodeEditor editor, CharSequence content, int startLine, int startColumn, int endLine, int endColumn, CharSequence insertedContent);
|
||||
|
||||
/**
|
||||
* @see io.github.rosemoe.editor.text.ContentListener#beforeReplace(Content)
|
||||
* Note:do not change content at this time
|
||||
*/
|
||||
void beforeReplace(CodeEditor editor, CharSequence content);
|
||||
|
||||
void onSetSelection(int startLine, int startCol, int endLine, int endCol);
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 Rosemoe
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.github.rosemoe.editor.interfaces;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import io.github.rosemoe.editor.widget.SymbolPairMatch;
|
||||
|
||||
/**
|
||||
* Language for editor
|
||||
* <p>
|
||||
* A Language helps editor to highlight text and provide auto-completion.
|
||||
* Implement this interface when you want to add new language support for editor.
|
||||
* <p>
|
||||
* <strong>NOTE:</strong> A language must not be single instance.
|
||||
* One language instance should always serves for only one editor.
|
||||
* It means that you should not give a language object to other editor instances
|
||||
* after it has been applied to one editor.
|
||||
* This is to provide better connection between auto completion provider and code analyzer.
|
||||
*
|
||||
* @author Rose
|
||||
*/
|
||||
public interface EditorLanguage {
|
||||
|
||||
/**
|
||||
* Get CodeAnalyzer of this language object
|
||||
*
|
||||
* @return CodeAnalyzer
|
||||
*/
|
||||
CodeAnalyzer getAnalyzer();
|
||||
|
||||
/**
|
||||
* Get AutoCompleteProvider of this language object
|
||||
*
|
||||
* @return AutoCompleteProvider
|
||||
*/
|
||||
AutoCompleteProvider getAutoCompleteProvider();
|
||||
|
||||
/**
|
||||
* Called by editor to check whether this is a character for auto completion
|
||||
*
|
||||
* @param ch Character to check
|
||||
* @return Whether is character for auto completion
|
||||
*/
|
||||
boolean isAutoCompleteChar(char ch);
|
||||
|
||||
/**
|
||||
* Get advance for indent
|
||||
*
|
||||
* @param content Content of a line if
|
||||
* @return Advance space count
|
||||
*/
|
||||
int getIndentAdvance(String content);
|
||||
|
||||
/**
|
||||
* Whether use tab to format
|
||||
*
|
||||
* @return Whether use tab
|
||||
*/
|
||||
boolean useTab();
|
||||
|
||||
/**
|
||||
* Format the given content
|
||||
*
|
||||
* @param text Content to format
|
||||
* @return Formatted code
|
||||
*/
|
||||
CharSequence format(CharSequence text);
|
||||
|
||||
/**
|
||||
* Returns language specified symbol pairs.
|
||||
* The method is called only once when the language is applied.
|
||||
*/
|
||||
SymbolPairMatch getSymbolPairs();
|
||||
|
||||
/**
|
||||
* Get newline handlers of this language.
|
||||
* This method is called each time the user presses ENTER key.
|
||||
* <p>
|
||||
* Pay attention to the performance as this method is called frequently
|
||||
*
|
||||
* @return NewlineHandlers , maybe null
|
||||
*/
|
||||
NewlineHandler[] getNewlineHandlers();
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 Rosemoe
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.github.rosemoe.editor.interfaces;
|
||||
import io.github.rosemoe.editor.text.CharPosition;
|
||||
|
||||
/**
|
||||
* Perform text processing when user enters '\n' and selection size is 0
|
||||
*/
|
||||
public interface NewlineHandler {
|
||||
|
||||
/**
|
||||
* Checks whether the given input matches the requirement to invoke this handler
|
||||
*
|
||||
* @param beforeText Text of line before cursor
|
||||
* @param afterText Text of line after cursor
|
||||
* @return Whether this handler should be called
|
||||
*/
|
||||
boolean matchesRequirement(String beforeText, String afterText, CharPosition cursor);
|
||||
|
||||
/**
|
||||
* Handle newline and return processed content to insert
|
||||
*
|
||||
* @param beforeText Text of line before cursor
|
||||
* @param afterText Text of line after cursor
|
||||
* @return Actual content to insert
|
||||
*/
|
||||
HandleResult handleNewline(String beforeText, String afterText, int tabSize);
|
||||
|
||||
class HandleResult {
|
||||
|
||||
/**
|
||||
* Text to insert
|
||||
*/
|
||||
public final CharSequence text;
|
||||
|
||||
/**
|
||||
* Count to shift left from the end of {@link HandleResult#text}
|
||||
*/
|
||||
public final int shiftLeft;
|
||||
|
||||
public HandleResult(CharSequence text, int shiftLeft) {
|
||||
this.text = text;
|
||||
this.shiftLeft = shiftLeft;
|
||||
if (shiftLeft < 0 || shiftLeft > text.length()) {
|
||||
throw new IllegalArgumentException("invalid shiftLeft");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package io.github.rosemoe.editor.langs;
|
||||
|
||||
import android.graphics.Color;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.itsaky.lsp.models.DiagnosticItem;
|
||||
|
||||
import org.antlr.v4.runtime.Token;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import io.github.rosemoe.editor.interfaces.CodeAnalyzer;
|
||||
import io.github.rosemoe.editor.struct.Span;
|
||||
import io.github.rosemoe.editor.text.TextAnalyzeResult;
|
||||
|
||||
public abstract class AbstractCodeAnalyzer implements CodeAnalyzer {
|
||||
|
||||
@Override
|
||||
public DiagnosticItem findDiagnosticContaining(int line, int column) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DiagnosticItem> findDiagnosticsContainingLine(int line) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
protected void checkAndAddHexString (@NonNull Token token, final int defaultColorId, TextAnalyzeResult result) {
|
||||
final var line = token.getLine () - 1;
|
||||
final var column = token.getCharPositionInLine ();
|
||||
final var m = HEX.matcher (token.getText ());
|
||||
if (m.find ()) {
|
||||
try {
|
||||
// If the text is: '"#f44336"'
|
||||
// Then then the inverted commas will have a separate span and the string value will have a separate span
|
||||
|
||||
// Highlight opening '"'
|
||||
final var first = result.addIfNeeded (line, column, defaultColorId);
|
||||
first.underlineHeight = 0f;
|
||||
first.underlineColor = 0;
|
||||
|
||||
// Highlight color value
|
||||
final var span = first.copy ();
|
||||
span.column += 1;
|
||||
span.underlineColor = Color.parseColor (token.getText ().substring (m.start (), m.end ()));
|
||||
span.underlineHeight = Span.HEX_COLOR_UNDERLINE_HEIGHT;
|
||||
result.add (span.line, span);
|
||||
|
||||
// Highlight closing '"'
|
||||
final var second = first.copy ();
|
||||
second.column += token.getText ().length () - 1;
|
||||
second.underlineColor = 0;
|
||||
second.underlineHeight = 0f;
|
||||
result.add (second.line, second);
|
||||
|
||||
} catch (Throwable th) {
|
||||
result.addIfNeeded (line, column, defaultColorId);
|
||||
}
|
||||
} else {
|
||||
result.addIfNeeded (line, column, defaultColorId);
|
||||
}
|
||||
}
|
||||
|
||||
private final Pattern HEX = Pattern.compile ("#[a-fA-F0-9]{3,8}");
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 Rosemoe
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.github.rosemoe.editor.langs;
|
||||
|
||||
import com.itsaky.lsp.api.ILanguageServer;
|
||||
import com.itsaky.lsp.models.CompletionItem;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import io.github.rosemoe.editor.interfaces.AutoCompleteProvider;
|
||||
import io.github.rosemoe.editor.interfaces.CodeAnalyzer;
|
||||
import io.github.rosemoe.editor.interfaces.EditorLanguage;
|
||||
import io.github.rosemoe.editor.interfaces.NewlineHandler;
|
||||
import io.github.rosemoe.editor.text.Content;
|
||||
import io.github.rosemoe.editor.text.TextAnalyzeResult;
|
||||
import io.github.rosemoe.editor.text.TextAnalyzer;
|
||||
import io.github.rosemoe.editor.widget.SymbolPairMatch;
|
||||
|
||||
/**
|
||||
* Empty language without any effect
|
||||
*
|
||||
* @author Rose
|
||||
*/
|
||||
public class EmptyLanguage implements EditorLanguage {
|
||||
|
||||
@Override
|
||||
public CharSequence format(CharSequence text) {
|
||||
return text;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SymbolPairMatch getSymbolPairs() {
|
||||
return new SymbolPairMatch.DefaultSymbolPairs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public NewlineHandler[] getNewlineHandlers() {
|
||||
return new NewlineHandler[0];
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public CodeAnalyzer getAnalyzer() {
|
||||
return new EmptyCodeAnalyzer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AutoCompleteProvider getAutoCompleteProvider() {
|
||||
return new EmptyAutoCompleteProvider();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAutoCompleteChar(char ch) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIndentAdvance(String content) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useTab() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static class EmptyAutoCompleteProvider implements AutoCompleteProvider {
|
||||
|
||||
@Override
|
||||
public List<CompletionItem> getAutoCompleteItems(Content content, String fileUri, String prefix, boolean isInCodeBlock, TextAnalyzeResult colors, int index, int line, int column) {
|
||||
return new ArrayList<> ();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class EmptyCodeAnalyzer extends io.github.rosemoe.editor.langs.AbstractCodeAnalyzer {
|
||||
|
||||
@Override
|
||||
public void analyze(ILanguageServer server, File file, Content content, TextAnalyzeResult colors, TextAnalyzer.AnalyzeThread.Delegate delegate) {
|
||||
colors.addNormalIfNull();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 Rosemoe
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.github.rosemoe.editor.struct;
|
||||
|
||||
/**
|
||||
* Code block info model
|
||||
*
|
||||
* @author Rose
|
||||
*/
|
||||
public class BlockLine {
|
||||
|
||||
/**
|
||||
* Start line of code block
|
||||
*/
|
||||
public int startLine;
|
||||
|
||||
/**
|
||||
* Start column of code block
|
||||
*/
|
||||
public int startColumn;
|
||||
|
||||
/**
|
||||
* End line of code block
|
||||
*/
|
||||
public int endLine;
|
||||
|
||||
/**
|
||||
* End column of code block
|
||||
*/
|
||||
public int endColumn;
|
||||
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 Rosemoe
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.github.rosemoe.editor.struct;
|
||||
|
||||
/**
|
||||
* Model for code navigation
|
||||
*
|
||||
* @author Rose
|
||||
*/
|
||||
@SuppressWarnings("CanBeFinal")
|
||||
public class NavigationItem {
|
||||
|
||||
/**
|
||||
* The line position
|
||||
*/
|
||||
public int line;
|
||||
|
||||
/**
|
||||
* The description
|
||||
*/
|
||||
public String label;
|
||||
|
||||
/**
|
||||
* Create a new navigation
|
||||
*
|
||||
* @param line The line position
|
||||
* @param label The description
|
||||
*/
|
||||
public NavigationItem(int line, String label) {
|
||||
this.line = line;
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 Rosemoe
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.github.rosemoe.editor.struct;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
|
||||
import io.github.rosemoe.editor.syntax.EditorColorScheme;
|
||||
|
||||
/**
|
||||
* The span model
|
||||
*
|
||||
* @author Rose
|
||||
*/
|
||||
public class Span {
|
||||
|
||||
private static final BlockingQueue<Span> cacheQueue = new ArrayBlockingQueue<> (8192 * 2);
|
||||
public int line;
|
||||
public int column;
|
||||
public int colorId;
|
||||
public int underlineColor = 0;
|
||||
public float underlineHeight;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString () {
|
||||
return "Span{" +
|
||||
"line=" + line +
|
||||
", column=" + column +
|
||||
", colorId=" + colorId +
|
||||
", underlineColor=" + underlineColor +
|
||||
", underlineHeight=" + underlineHeight +
|
||||
'}';
|
||||
}
|
||||
|
||||
public static final float DEFAULT_UNDERLINE_HEIGHT = 0.1f;
|
||||
public static final float HEX_COLOR_UNDERLINE_HEIGHT = 0.27f;
|
||||
|
||||
/**
|
||||
* Create a new span
|
||||
*
|
||||
* @param line Start line of span
|
||||
* @param column Start column of span
|
||||
* @param colorId Type of span
|
||||
* @see Span#obtain(int, int)
|
||||
*/
|
||||
private Span (int line, int column, int colorId) {
|
||||
this.line = line;
|
||||
this.column = column;
|
||||
this.colorId = colorId;
|
||||
this.underlineColor = 0;
|
||||
this.underlineHeight = DEFAULT_UNDERLINE_HEIGHT;
|
||||
}
|
||||
|
||||
public static Span obtain (int line, int column, int colorId) {
|
||||
Span span = cacheQueue.poll ();
|
||||
if (span == null) {
|
||||
return new Span (line, column, colorId);
|
||||
} else {
|
||||
span.line = line;
|
||||
span.column = column;
|
||||
span.colorId = colorId;
|
||||
return span;
|
||||
}
|
||||
}
|
||||
|
||||
public static void recycleAll (Collection<Span> spans) {
|
||||
for (Span span : spans) {
|
||||
if (!span.recycle ()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a underline for this region
|
||||
* Zero for no underline
|
||||
*
|
||||
* @param color Color for this underline (not color id of {@link EditorColorScheme})
|
||||
* @return Self
|
||||
*/
|
||||
public Span setUnderlineColor (int color) {
|
||||
underlineColor = color;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the height of the underline of this span
|
||||
*
|
||||
* @param height The new height of the span's underline
|
||||
*/
|
||||
public void setUnderlineHeight (float height) {
|
||||
this.underlineHeight = height;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get span start column
|
||||
*
|
||||
* @return Start column
|
||||
*/
|
||||
public int getColumn () {
|
||||
return column;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set column of this span
|
||||
*/
|
||||
public Span setColumn (int column) {
|
||||
this.column = column;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a copy of this span
|
||||
*/
|
||||
public Span copy () {
|
||||
Span copy = obtain (line, column, colorId);
|
||||
copy.setUnderlineColor (underlineColor);
|
||||
return copy;
|
||||
}
|
||||
|
||||
public boolean recycle () {
|
||||
colorId = column = underlineColor = 0;
|
||||
return cacheQueue.offer (this);
|
||||
}
|
||||
}
|
||||
@@ -1,836 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 Rosemoe
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.github.rosemoe.editor.text;
|
||||
|
||||
import com.itsaky.androidide.utils.Symbols;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import io.github.rosemoe.editor.annotations.Experimental;
|
||||
import io.github.rosemoe.editor.widget.CodeEditor;
|
||||
import io.github.rosemoe.struct.BlockLinkedList;
|
||||
|
||||
/**
|
||||
* This class saves the text content for editor and maintains line widths
|
||||
*
|
||||
* @author Rose
|
||||
*/
|
||||
public class Content implements CharSequence {
|
||||
|
||||
public final static int DEFAULT_MAX_UNDO_STACK_SIZE = 100;
|
||||
public final static int DEFAULT_LIST_CAPACITY = 1000;
|
||||
|
||||
private static int sInitialListCapacity;
|
||||
|
||||
static {
|
||||
setInitialLineCapacity (DEFAULT_LIST_CAPACITY);
|
||||
}
|
||||
|
||||
private final List<ContentLine> mLines;
|
||||
private final List<ContentListener> mListeners;
|
||||
private final UndoManager mUndoManager;
|
||||
private Indexer mIndexer;
|
||||
private int mTextLength;
|
||||
private int mNestedBatchEdit;
|
||||
private Cursor mCursor;
|
||||
private LineRemoveListener mLineListener;
|
||||
|
||||
private final CodeEditor editor;
|
||||
|
||||
/**
|
||||
* Use a BlockLinkedList instead of ArrayList.
|
||||
* <p>
|
||||
* This can be faster while inserting in large text.
|
||||
* But in other conditions, it is quite slow.
|
||||
* <p>
|
||||
* Disabled by default.
|
||||
*/
|
||||
@Experimental
|
||||
public static boolean useBlock = false;
|
||||
|
||||
/**
|
||||
* This constructor will create a Content object with no text
|
||||
*/
|
||||
public Content (CodeEditor editor) {
|
||||
this (editor, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* This constructor will create a Content object with the given source
|
||||
* If you give us null,it will just create a empty Content object
|
||||
*
|
||||
* @param src The source of Content
|
||||
*/
|
||||
public Content (CodeEditor editor, CharSequence src) {
|
||||
if (src == null) {
|
||||
src = "";
|
||||
}
|
||||
this.editor = editor;
|
||||
mTextLength = 0;
|
||||
mNestedBatchEdit = 0;
|
||||
if (!useBlock) {
|
||||
mLines = new ArrayList<> (getInitialLineCapacity ());
|
||||
} else {
|
||||
mLines = new BlockLinkedList<> (5000);
|
||||
}
|
||||
mLines.add (new ContentLine ());
|
||||
mListeners = new ArrayList<> ();
|
||||
mUndoManager = new UndoManager (this);
|
||||
setMaxUndoStackSize (Content.DEFAULT_MAX_UNDO_STACK_SIZE);
|
||||
mIndexer = new NoCacheIndexer (this);
|
||||
if (src.length () == 0) {
|
||||
setUndoEnabled (true);
|
||||
return;
|
||||
}
|
||||
setUndoEnabled (false);
|
||||
insert (0, 0, src);
|
||||
setUndoEnabled (true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default capacity of text line list
|
||||
*
|
||||
* @return Default capacity
|
||||
*/
|
||||
public static int getInitialLineCapacity () {
|
||||
return Content.sInitialListCapacity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default capacity of text line list
|
||||
*
|
||||
* @param capacity Default capacity
|
||||
*/
|
||||
public static void setInitialLineCapacity (int capacity) {
|
||||
if (capacity <= 0) {
|
||||
throw new IllegalArgumentException ("capacity can not be negative or zero");
|
||||
}
|
||||
sInitialListCapacity = capacity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the two ContentLine have the same content
|
||||
*
|
||||
* @param a ContentLine
|
||||
* @param b another ContentLine
|
||||
* @return Whether equals in content
|
||||
*/
|
||||
private static boolean equals (ContentLine a, ContentLine b) {
|
||||
if (a.length () != b.length ()) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < a.length (); i++) {
|
||||
if (a.charAt (i) != b.charAt (i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public char charAt (int index) {
|
||||
checkIndex (index);
|
||||
CharPosition p = getIndexer ().getCharPosition (index);
|
||||
return charAt (p.line, p.column);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int length () {
|
||||
return mTextLength;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence subSequence (int start, int end) {
|
||||
if (start > end) {
|
||||
throw new StringIndexOutOfBoundsException ("start > end");
|
||||
}
|
||||
CharPosition s = getIndexer ().getCharPosition (start);
|
||||
CharPosition e = getIndexer ().getCharPosition (end);
|
||||
return subContent (s.getLine (), s.getColumn (), e.getLine (), e.getColumn ());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a line listener
|
||||
*
|
||||
* @param lis the listener,maybe null
|
||||
* @see LineRemoveListener
|
||||
*/
|
||||
public void setLineListener (LineRemoveListener lis) {
|
||||
this.mLineListener = lis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the character at the given position
|
||||
* If (column == getColumnCount(line)),it returns '\n'
|
||||
* IndexOutOfBoundsException is thrown
|
||||
*
|
||||
* @param line The line position of character
|
||||
* @param column The column position of character
|
||||
* @return The character at the given position
|
||||
*/
|
||||
public char charAt (int line, int column) {
|
||||
if (!checkLineAndColumn (line, column, true)) {
|
||||
return 0;
|
||||
}
|
||||
if (column == getColumnCount (line)) {
|
||||
return '\n';
|
||||
}
|
||||
return mLines.get (line).charAt (column);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get raw data of line
|
||||
* The result is not expected to be modified
|
||||
*
|
||||
* @param line Line
|
||||
* @return Raw ContentLine used by Content
|
||||
*/
|
||||
public ContentLine getLine (int line) {
|
||||
return mLines.get (line);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get how many lines there are
|
||||
*
|
||||
* @return Line count
|
||||
*/
|
||||
public int getLineCount () {
|
||||
return mLines.size ();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get how many characters is on the given line
|
||||
* If (line < 0 or line >= getLineCount()),it will throw a IndexOutOfBoundsException
|
||||
*
|
||||
* @param line The line to get
|
||||
* @return Character count on line
|
||||
*/
|
||||
public int getColumnCount (int line) {
|
||||
checkLine (line);
|
||||
return mLines.get (line).length ();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the given line text without '\n' character
|
||||
*
|
||||
* @param line The line to get
|
||||
* @return New String object of this line
|
||||
*/
|
||||
public String getLineString (int line) {
|
||||
checkLine (line);
|
||||
return mLines.get (line).toString ();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get characters of line
|
||||
*/
|
||||
public void getLineChars (int line, char[] dest) {
|
||||
mLines.get (line).getChars (0, getColumnCount (line), dest, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the lines of this content.
|
||||
* @return The content lines.
|
||||
*/
|
||||
public List<ContentLine> getLines () {
|
||||
return mLines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the (line,column) position to index
|
||||
* This task will usually completed by {@link Indexer}
|
||||
*
|
||||
* @param line Line of index
|
||||
* @param column Column on line of index
|
||||
* @return Transformed index for the given arguments
|
||||
*/
|
||||
public int getCharIndex (int line, int column) {
|
||||
return getIndexer ().getCharIndex (line, column);
|
||||
}
|
||||
|
||||
/**
|
||||
* Append text to the end of this content
|
||||
*
|
||||
* @param text Text to append
|
||||
*/
|
||||
public int append (CharSequence text) {
|
||||
if (getLineCount () <= 0) {
|
||||
return 0;
|
||||
}
|
||||
final int line = getLineCount () - 1;
|
||||
int col = getColumnCount (line);
|
||||
if (col < 0) {
|
||||
col = 0;
|
||||
}
|
||||
insert (line, col, text);
|
||||
return line;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert content to this object
|
||||
*
|
||||
* @param line The insertion's line position
|
||||
* @param column The insertion's column position
|
||||
* @param text The text you want to insert at the position
|
||||
*/
|
||||
public void insert (int line, int column, CharSequence text) {
|
||||
if (!checkLineAndColumn (line, column, true)) {
|
||||
return;
|
||||
}
|
||||
if (text == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
text = text.toString ().replace ("\t", Symbols.createTabSpaces ());
|
||||
//-----Notify------
|
||||
if (mCursor != null) {
|
||||
mCursor.beforeInsert (line, column);
|
||||
}
|
||||
|
||||
int workLine = line;
|
||||
int workIndex = column;
|
||||
if (workIndex == -1) {
|
||||
workIndex = 0;
|
||||
}
|
||||
ContentLine currLine = mLines.get (workLine);
|
||||
for (int i = 0; i < text.length (); i++) {
|
||||
char c = text.charAt (i);
|
||||
if (c == '\n') {
|
||||
ContentLine newLine = new ContentLine ();
|
||||
newLine.append (currLine, workIndex, currLine.length ());
|
||||
currLine.delete (workIndex, currLine.length ());
|
||||
mLines.add (workLine + 1, newLine);
|
||||
currLine = newLine;
|
||||
workIndex = 0;
|
||||
workLine++;
|
||||
} else {
|
||||
currLine.insert (workIndex, c);
|
||||
workIndex++;
|
||||
}
|
||||
}
|
||||
mTextLength += text.length ();
|
||||
this.dispatchAfterInsert (line, column, workLine, workIndex, text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete character in [start,end)
|
||||
*
|
||||
* @param start Start position in content
|
||||
* @param end End position in content
|
||||
*/
|
||||
public void delete (int start, int end) {
|
||||
checkIndex (start);
|
||||
checkIndex (end);
|
||||
CharPosition startPos = getIndexer ().getCharPosition (start);
|
||||
CharPosition endPos = getIndexer ().getCharPosition (end);
|
||||
if (start != end) {
|
||||
delete (startPos.line, startPos.column, endPos.line, endPos.column);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete text in the given region
|
||||
*
|
||||
* @param startLine The start line position
|
||||
* @param columnOnStartLine The start column position
|
||||
* @param endLine The end line position
|
||||
* @param columnOnEndLine The end column position
|
||||
*/
|
||||
public void delete (int startLine, int columnOnStartLine, int endLine, int columnOnEndLine) {
|
||||
StringBuilder changedContent = new StringBuilder ();
|
||||
if (startLine == endLine) {
|
||||
if (!checkLineAndColumn (endLine, columnOnEndLine, true)) {
|
||||
return;
|
||||
}
|
||||
if (!checkLineAndColumn (startLine, columnOnStartLine == -1 ? 0 : columnOnStartLine, true)) {
|
||||
return;
|
||||
}
|
||||
int beginIdx = columnOnStartLine;
|
||||
if (columnOnStartLine == -1) {
|
||||
beginIdx = 0;
|
||||
}
|
||||
if (beginIdx > columnOnEndLine) {
|
||||
throw new IllegalArgumentException ("start > end");
|
||||
}
|
||||
ContentLine curr = mLines.get (startLine);
|
||||
int len = curr.length ();
|
||||
if (beginIdx < 0 || beginIdx > len || columnOnEndLine > len) {
|
||||
throw new StringIndexOutOfBoundsException ("column start or column end is out of bounds");
|
||||
}
|
||||
|
||||
//-----Notify------
|
||||
if (mCursor != null) {
|
||||
if (columnOnStartLine != -1) {
|
||||
mCursor.beforeDelete (startLine, columnOnStartLine, endLine, columnOnEndLine);
|
||||
} else {
|
||||
mCursor.beforeDelete (startLine == 0 ? 0 : startLine - 1, startLine == 0 ? 0 : getColumnCount (startLine - 1), endLine, columnOnEndLine);
|
||||
}
|
||||
}
|
||||
|
||||
changedContent.append (curr, beginIdx, columnOnEndLine);
|
||||
curr.delete (beginIdx, columnOnEndLine);
|
||||
mTextLength -= columnOnEndLine - columnOnStartLine;
|
||||
if (columnOnStartLine == -1) {
|
||||
if (startLine == 0) {
|
||||
mTextLength++;
|
||||
} else {
|
||||
ContentLine previous = mLines.get (startLine - 1);
|
||||
previous.append (curr);
|
||||
ContentLine rm = mLines.remove (startLine);
|
||||
if (mLineListener != null) {
|
||||
mLineListener.onRemove (this, rm);
|
||||
}
|
||||
changedContent.insert (0, '\n');
|
||||
startLine--;
|
||||
columnOnStartLine = getColumnCount (startLine);
|
||||
}
|
||||
}
|
||||
} else if (startLine < endLine) {
|
||||
if (!checkLineAndColumn (startLine, columnOnStartLine, true)) {
|
||||
return;
|
||||
}
|
||||
if (!checkLineAndColumn (endLine, columnOnEndLine, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
//-----Notify------
|
||||
if (mCursor != null) {
|
||||
mCursor.beforeDelete (startLine, columnOnStartLine, endLine, columnOnEndLine);
|
||||
}
|
||||
|
||||
for (int i = 0; i < endLine - startLine - 1; i++) {
|
||||
ContentLine line = mLines.remove (startLine + 1);
|
||||
if (mLineListener != null) {
|
||||
mLineListener.onRemove (this, line);
|
||||
}
|
||||
mTextLength -= line.length () + 1;
|
||||
changedContent.append ('\n').append (line);
|
||||
}
|
||||
int currEnd = startLine + 1;
|
||||
ContentLine start = mLines.get (startLine);
|
||||
ContentLine end = mLines.get (currEnd);
|
||||
mTextLength -= start.length () - columnOnStartLine;
|
||||
changedContent.insert (0, start, columnOnStartLine, start.length ());
|
||||
start.delete (columnOnStartLine, start.length ());
|
||||
mTextLength -= columnOnEndLine;
|
||||
changedContent.append ('\n').append (end, 0, columnOnEndLine);
|
||||
end.delete (0, columnOnEndLine);
|
||||
mTextLength--;
|
||||
ContentLine r = mLines.remove (currEnd);
|
||||
if (mLineListener != null) {
|
||||
mLineListener.onRemove (this, r);
|
||||
}
|
||||
start.append (end);
|
||||
} else {
|
||||
throw new IllegalArgumentException ("start line > end line");
|
||||
}
|
||||
this.dispatchAfterDelete (startLine, columnOnStartLine, endLine, columnOnEndLine, changedContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the text in the given region
|
||||
* This action will completed by calling {@link Content#delete(int, int, int, int)} and {@link Content#insert(int, int, CharSequence)}
|
||||
*
|
||||
* @param startLine The start line position
|
||||
* @param columnOnStartLine The start column position
|
||||
* @param endLine The end line position
|
||||
* @param columnOnEndLine The end column position
|
||||
* @param text The text to replace old text
|
||||
*/
|
||||
public void replace (int startLine, int columnOnStartLine, int endLine, int columnOnEndLine, CharSequence text) {
|
||||
if (text == null) {
|
||||
throw new IllegalArgumentException ("text can not be null");
|
||||
}
|
||||
this.dispatchBeforeReplace ();
|
||||
delete (startLine, columnOnStartLine, endLine, columnOnEndLine);
|
||||
insert (startLine, columnOnStartLine, text);
|
||||
}
|
||||
|
||||
/**
|
||||
* When you are going to use {@link CharSequence#charAt(int)} frequently,you are required to call
|
||||
* this method.Because the way Content save text,it is usually slow to transform index to
|
||||
* (line,column) from the start of text when the text is big.
|
||||
* By calling this method,you will be able to get faster because calling this will
|
||||
* cause the ITextContent object use a Indexer with cache.
|
||||
* The performance is highly improved while linearly getting characters.
|
||||
*
|
||||
* @param initialIndex The Indexer with cache will take it into this index to its cache
|
||||
*/
|
||||
public void beginStreamCharGetting (int initialIndex) {
|
||||
mIndexer = new CachedIndexer (this);
|
||||
mIndexer.getCharPosition (initialIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* When you finished calling {@link CharSequence#charAt(int)} frequently,you can call this method
|
||||
* to free the Indexer with cache.
|
||||
* This is not forced.
|
||||
*/
|
||||
public void endStreamCharGetting () {
|
||||
mIndexer = new NoCacheIndexer (this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Undo the last modification
|
||||
* NOTE:When there are too much modification,old modification will be deleted from UndoManager
|
||||
*/
|
||||
public void undo () {
|
||||
mUndoManager.undo (this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redo the last modification
|
||||
*/
|
||||
public void redo () {
|
||||
mUndoManager.redo (this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether we can undo
|
||||
*
|
||||
* @return Whether we can undo
|
||||
*/
|
||||
public boolean canUndo () {
|
||||
return mUndoManager.canUndo ();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether we can redo
|
||||
*
|
||||
* @return Whether we can redo
|
||||
*/
|
||||
public boolean canRedo () {
|
||||
return mUndoManager.canRedo ();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether UndoManager is enabled
|
||||
*
|
||||
* @return Whether UndoManager is enabled
|
||||
*/
|
||||
public boolean isUndoEnabled () {
|
||||
return mUndoManager.isUndoEnabled ();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether enable the UndoManager.
|
||||
* If false,any modification will not be taken down and previous modification that
|
||||
* is already in UndoManager will be removed.Does not make changes to content.
|
||||
*
|
||||
* @param enabled New state for UndoManager
|
||||
*/
|
||||
public void setUndoEnabled (boolean enabled) {
|
||||
mUndoManager.setUndoEnabled (enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current max stack size of UndoManager
|
||||
*
|
||||
* @return current max stack size
|
||||
*/
|
||||
public int getMaxUndoStackSize () {
|
||||
return mUndoManager.getMaxUndoStackSize ();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the max size of stack in UndoManager
|
||||
*
|
||||
* @param maxSize New max size
|
||||
*/
|
||||
public void setMaxUndoStackSize (int maxSize) {
|
||||
mUndoManager.setMaxUndoStackSize (maxSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* A delegate method.
|
||||
* Notify the UndoManager to begin batch edit(enter a new layer).
|
||||
* NOTE: batch edit in Android can be nested.
|
||||
*
|
||||
* @return Whether in batch edit
|
||||
*/
|
||||
public boolean beginBatchEdit () {
|
||||
mNestedBatchEdit++;
|
||||
return isInBatchEdit ();
|
||||
}
|
||||
|
||||
/**
|
||||
* A delegate method.
|
||||
* Notify the UndoManager to end batch edit(exit current layer).
|
||||
*
|
||||
* @return Whether in batch edit
|
||||
*/
|
||||
public boolean endBatchEdit () {
|
||||
mNestedBatchEdit--;
|
||||
if (mNestedBatchEdit < 0) {
|
||||
mNestedBatchEdit = 0;
|
||||
}
|
||||
return isInBatchEdit ();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether we are in batch edit
|
||||
*
|
||||
* @return Whether in batch edit
|
||||
*/
|
||||
public boolean isInBatchEdit () {
|
||||
return mNestedBatchEdit > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new {@link ContentListener} to the Content
|
||||
*
|
||||
* @param listener The listener to add
|
||||
*/
|
||||
public void addContentListener (ContentListener listener) {
|
||||
if (listener == null) {
|
||||
throw new IllegalArgumentException ("listener can not be null");
|
||||
}
|
||||
if (listener instanceof Indexer) {
|
||||
throw new IllegalArgumentException ("Permission denied");
|
||||
}
|
||||
if (!mListeners.contains (listener)) {
|
||||
mListeners.add (listener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the given listener of this Content
|
||||
*
|
||||
* @param listener The listener to remove
|
||||
*/
|
||||
public void removeContentListener (ContentListener listener) {
|
||||
if (listener instanceof Indexer) {
|
||||
throw new IllegalArgumentException ("Permission denied");
|
||||
}
|
||||
mListeners.remove (listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the using {@link Indexer} object
|
||||
*
|
||||
* @return Indexer for this object
|
||||
*/
|
||||
public Indexer getIndexer () {
|
||||
if (mIndexer.getClass () != CachedIndexer.class && mCursor != null) {
|
||||
return mCursor.getIndexer ();
|
||||
}
|
||||
return mIndexer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick method to get sub string of this object
|
||||
*
|
||||
* @param startLine The start line position
|
||||
* @param startColumn The start column position
|
||||
* @param endLine The end line position
|
||||
* @param endColumn The end column position
|
||||
* @return sub sequence of this Content
|
||||
*/
|
||||
public Content subContent (int startLine, int startColumn, int endLine, int endColumn) {
|
||||
Content c = new Content (this.editor);
|
||||
c.setUndoEnabled (false);
|
||||
if (startLine == endLine) {
|
||||
c.insert (0, 0, mLines.get (startLine).subSequence (startColumn, endColumn));
|
||||
} else if (startLine < endLine) {
|
||||
c.insert (0, 0, mLines.get (startLine).subSequence (startColumn, mLines.get (startLine).length ()));
|
||||
for (int i = startLine + 1; i < endLine; i++) {
|
||||
c.mLines.add (new ContentLine (mLines.get (i)));
|
||||
c.mTextLength += mLines.get (i).length () + 1;
|
||||
}
|
||||
ContentLine end = mLines.get (endLine);
|
||||
c.mLines.add (new ContentLine ().insert (0, end, 0, endColumn));
|
||||
c.mTextLength += endColumn + 1;
|
||||
} else {
|
||||
throw new IllegalArgumentException ("start > end");
|
||||
}
|
||||
c.setUndoEnabled (true);
|
||||
return c;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals (Object anotherObject) {
|
||||
if (anotherObject instanceof Content) {
|
||||
Content content = (Content) anotherObject;
|
||||
if (content.getLineCount () != this.getLineCount ()) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < this.getLineCount (); i++) {
|
||||
if (!equals (mLines.get (i), content.mLines.get (i))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString () {
|
||||
StringBuilder sb = new StringBuilder ();
|
||||
boolean first = true;
|
||||
for (ContentLine line : mLines) {
|
||||
if (!first) {
|
||||
sb.append ('\n');
|
||||
} else {
|
||||
first = false;
|
||||
}
|
||||
sb.append (line);
|
||||
}
|
||||
return sb.toString ();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the text in StringBuilder form
|
||||
* Used by TextColorProvider
|
||||
* This can improve the speed in char getting for tokenizing
|
||||
*
|
||||
* @return StringBuilder form of Content
|
||||
*/
|
||||
public StringBuilder toStringBuilder () {
|
||||
StringBuilder sb = new StringBuilder ();
|
||||
sb.ensureCapacity (mTextLength + 10);
|
||||
boolean first = true;
|
||||
final int lines = getLineCount ();
|
||||
for (int i = 0; i < lines; i++) {
|
||||
ContentLine line = mLines.get (i);
|
||||
if (!first) {
|
||||
sb.append ('\n');
|
||||
} else {
|
||||
first = false;
|
||||
}
|
||||
line.appendTo (sb);
|
||||
}
|
||||
return sb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Cursor for editor (Create if there is not)
|
||||
*
|
||||
* @return Cursor
|
||||
*/
|
||||
public Cursor getCursor () {
|
||||
if (mCursor == null) {
|
||||
mCursor = new Cursor (this.editor, this);
|
||||
}
|
||||
return mCursor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch events to listener before replacement
|
||||
*/
|
||||
private void dispatchBeforeReplace () {
|
||||
mUndoManager.beforeReplace (this);
|
||||
if (mCursor != null) {
|
||||
mCursor.beforeReplace ();
|
||||
}
|
||||
if (mIndexer instanceof ContentListener) {
|
||||
((ContentListener) mIndexer).beforeReplace (this);
|
||||
}
|
||||
for (ContentListener lis : mListeners) {
|
||||
lis.beforeReplace (this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch events to listener after deletion
|
||||
*
|
||||
* @param a Start line
|
||||
* @param b Start Column
|
||||
* @param c End line
|
||||
* @param d End column
|
||||
* @param e Text deleted
|
||||
*/
|
||||
private void dispatchAfterDelete (int a, int b, int c, int d, CharSequence e) {
|
||||
mUndoManager.afterDelete (this, a, b, c, d, e);
|
||||
if (mCursor != null) {
|
||||
mCursor.afterDelete (a, b, c, d, e);
|
||||
}
|
||||
if (mIndexer instanceof ContentListener) {
|
||||
((ContentListener) mIndexer).afterDelete (this, a, b, c, d, e);
|
||||
}
|
||||
for (ContentListener lis : mListeners) {
|
||||
lis.afterDelete (this, a, b, c, d, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch events to listener after insertion
|
||||
*
|
||||
* @param a Start line
|
||||
* @param b Start Column
|
||||
* @param c End line
|
||||
* @param d End column
|
||||
* @param e Text deleted
|
||||
*/
|
||||
private void dispatchAfterInsert (int a, int b, int c, int d, CharSequence e) {
|
||||
mUndoManager.afterInsert (this, a, b, c, d, e);
|
||||
if (mCursor != null) {
|
||||
mCursor.afterInsert (a, b, c, d, e);
|
||||
}
|
||||
if (mIndexer instanceof ContentListener) {
|
||||
((ContentListener) mIndexer).afterInsert (this, a, b, c, d, e);
|
||||
}
|
||||
for (ContentListener lis : mListeners) {
|
||||
lis.afterInsert (this, a, b, c, d, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the index is valid
|
||||
*
|
||||
* @param index Index to check
|
||||
*/
|
||||
protected void checkIndex (int index) {
|
||||
if (index > length ()) {
|
||||
throw new StringIndexOutOfBoundsException ("Index " + index + " out of bounds. length:" + length ());
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isPositionValid (int line, int column) {
|
||||
return checkLineAndColumn (line, column, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the line is valid
|
||||
*
|
||||
* @param line Line to check
|
||||
*/
|
||||
protected void checkLine (int line) {
|
||||
if (line >= getLineCount ()) {
|
||||
throw new StringIndexOutOfBoundsException ("Line " + line + " out of bounds. line count:" + getLineCount ());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the line and column is valid
|
||||
*
|
||||
* @param line The line to check
|
||||
* @param column The column to check
|
||||
* @param allowEqual Whether allow (column == getColumnCount(line))
|
||||
*/
|
||||
protected boolean checkLineAndColumn (int line, int column, boolean allowEqual) {
|
||||
checkLine (line);
|
||||
int len = mLines.get (line).length ();
|
||||
if (column > len || (!allowEqual && column == len)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 Rosemoe
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.github.rosemoe.editor.text;
|
||||
|
||||
import android.graphics.Paint;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Cache to measure text quickly
|
||||
* This is very useful when text is long
|
||||
* Use this to make editor 20x faster than before
|
||||
* It is not thread-safe
|
||||
*
|
||||
* @author Rose
|
||||
*/
|
||||
public class FontCache {
|
||||
|
||||
private final float[] cache;
|
||||
|
||||
private final char[] buffer;
|
||||
|
||||
public FontCache() {
|
||||
cache = new float[65536];
|
||||
buffer = new char[3];
|
||||
}
|
||||
|
||||
private static boolean isEmoji(char ch) {
|
||||
return ch == 0xd83c || ch == 0xd83d || ch == 0xd83e;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear caches of font
|
||||
*/
|
||||
public void clearCache() {
|
||||
Arrays.fill(cache, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Measure a single character
|
||||
*/
|
||||
public float measureChar(char ch, Paint p) {
|
||||
float width = cache[(int) ch];
|
||||
if (width == 0) {
|
||||
buffer[0] = ch;
|
||||
width = p.measureText(buffer, 0, 1);
|
||||
cache[(int) ch] = width;
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
/*
|
||||
* Measure text
|
||||
*/
|
||||
public float measureText(char[] chars, int start, int end, Paint p) {
|
||||
float width = 0f;
|
||||
for (int i = start; i < end; i++) {
|
||||
char ch = chars[i];
|
||||
if (isEmoji(ch) && i + 1 < end) {
|
||||
buffer[0] = ch;
|
||||
buffer[1] = chars[++i];
|
||||
if( i + 1 < end ){
|
||||
buffer [2] = chars [++i];
|
||||
if( !isEmoji(buffer [1]) || isEmoji(buffer [2]) ){
|
||||
//当第2个字符不是Emoji或第3字符是Emoji时,只测量两个字符
|
||||
i--;
|
||||
width += p.measureText(buffer, 0, 2);
|
||||
}
|
||||
else{
|
||||
width += p.measureText(buffer, 0, 3);
|
||||
}
|
||||
}
|
||||
else{
|
||||
width += p.measureText(buffer, 0, 2);
|
||||
}
|
||||
} else {
|
||||
width += measureChar(ch, p);
|
||||
}
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Measure text
|
||||
*/
|
||||
public float measureText(CharSequence str, int start, int end, Paint p) {
|
||||
float width = 0f;
|
||||
for (int i = start; i < end; i++) {
|
||||
char ch = str.charAt(i);
|
||||
if (isEmoji(ch) && i + 1 < end) {
|
||||
buffer[0] = ch;
|
||||
buffer[1] = str.charAt(++i);
|
||||
if( i + 1 < end ){
|
||||
buffer [2] = str.charAt(++i);
|
||||
if( !isEmoji(buffer [1]) || isEmoji(buffer [2]) ){
|
||||
//当第2个字符不是Emoji或第3字符是Emoji时,只测量两个字符
|
||||
buffer [2] = 0;
|
||||
i--;
|
||||
width += p.measureText(buffer, 0, 2);
|
||||
}
|
||||
else{
|
||||
width += p.measureText(buffer, 0, 3);
|
||||
}
|
||||
}
|
||||
else{
|
||||
width += p.measureText(buffer, 0, 2);
|
||||
}
|
||||
} else {
|
||||
width += measureChar(ch, p);
|
||||
}
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 Rosemoe
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.github.rosemoe.editor.text;
|
||||
|
||||
import io.github.rosemoe.editor.interfaces.EditorLanguage;
|
||||
|
||||
public class FormatThread extends Thread {
|
||||
|
||||
private CharSequence mText;
|
||||
|
||||
private EditorLanguage mLanguage;
|
||||
|
||||
private FormatResultReceiver mReceiver;
|
||||
|
||||
public FormatThread(CharSequence text, EditorLanguage language, FormatResultReceiver receiver) {
|
||||
mText = text;
|
||||
mLanguage = language;
|
||||
mReceiver = receiver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
CharSequence result = null;
|
||||
try {
|
||||
CharSequence chars = ((mText instanceof Content) ? (((Content) mText).toStringBuilder()) : new StringBuilder(mText));
|
||||
result = mLanguage.format(chars);
|
||||
} catch (Throwable e) {
|
||||
if (mReceiver != null) {
|
||||
mReceiver.onFormatFail(e);
|
||||
}
|
||||
}
|
||||
if (mReceiver != null) {
|
||||
mReceiver.onFormatSucceed(mText, result);
|
||||
}
|
||||
mReceiver = null;
|
||||
mLanguage = null;
|
||||
mText = null;
|
||||
}
|
||||
|
||||
public interface FormatResultReceiver {
|
||||
|
||||
void onFormatSucceed(CharSequence originalText, CharSequence newText);
|
||||
|
||||
void onFormatFail(Throwable throwable);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 Rosemoe
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.github.rosemoe.editor.text;
|
||||
|
||||
/**
|
||||
* A helper class for ITextContent to transform (line,column) and index
|
||||
*
|
||||
* @author Rose
|
||||
*/
|
||||
public interface Indexer {
|
||||
|
||||
/**
|
||||
* Get the index of (line,column)
|
||||
*
|
||||
* @param line The line position of index
|
||||
* @param column The column position of index
|
||||
* @return Calculated index
|
||||
*/
|
||||
int getCharIndex(int line, int column);
|
||||
|
||||
/**
|
||||
* Get the line position of index
|
||||
*
|
||||
* @param index The index you want to know its line
|
||||
* @return Line position of index
|
||||
*/
|
||||
int getCharLine(int index);
|
||||
|
||||
/**
|
||||
* Get the column position of index
|
||||
*
|
||||
* @param index The index you want to know its column
|
||||
* @return Column position of index
|
||||
*/
|
||||
int getCharColumn(int index);
|
||||
|
||||
/**
|
||||
* Get the CharPosition for the given index
|
||||
* You are not expected to make changes with this CharPosition
|
||||
*
|
||||
* @param index The index you want to get
|
||||
* @return The CharPosition object.
|
||||
*/
|
||||
CharPosition getCharPosition(int index);
|
||||
|
||||
/**
|
||||
* Get the CharPosition for the given (line,column)
|
||||
* You are not expected to make changes with this CharPosition
|
||||
*
|
||||
* @param line The line position you want to get
|
||||
* @param column The column position you want to get
|
||||
* @return The CharPosition object.
|
||||
*/
|
||||
CharPosition getCharPosition(int line, int column);
|
||||
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 Rosemoe
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.github.rosemoe.editor.text;
|
||||
|
||||
/**
|
||||
* A listener to know when a ContentLine object is removed from Content object
|
||||
*
|
||||
* @author Rose
|
||||
*/
|
||||
public interface LineRemoveListener {
|
||||
|
||||
/**
|
||||
* When a ContentLine is removed from Content, this method is called
|
||||
*
|
||||
* @param content Caller Content
|
||||
* @param line ContentLine object removed
|
||||
*/
|
||||
void onRemove(Content content, ContentLine line);
|
||||
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 Rosemoe
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.github.rosemoe.editor.text;
|
||||
|
||||
/**
|
||||
* Indexer without cache
|
||||
*
|
||||
* @author Rose
|
||||
*/
|
||||
public final class NoCacheIndexer extends CachedIndexer implements Indexer {
|
||||
|
||||
/**
|
||||
* Create a indexer without cache
|
||||
*
|
||||
* @param content Target content
|
||||
*/
|
||||
public NoCacheIndexer(Content content) {
|
||||
super(content);
|
||||
//Disable dynamic indexing
|
||||
if (super.getMaxCacheSize() != 0) {
|
||||
super.setMaxCacheSize(0);
|
||||
}
|
||||
if (super.isHandleEvent()) {
|
||||
super.setHandleEvent(false);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 Rosemoe
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.github.rosemoe.editor.text;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.github.rosemoe.editor.struct.BlockLine;
|
||||
|
||||
/**
|
||||
* A object provider for speed improvement
|
||||
* Now meaningless because it is not as well as it expected
|
||||
*
|
||||
* @author Rose
|
||||
*/
|
||||
public class ObjectAllocator {
|
||||
|
||||
private static final int RECYCLE_LIMIT = 1024 * 8;
|
||||
private static List<BlockLine> blockLines;
|
||||
|
||||
public static void recycleBlockLine(List<BlockLine> src) {
|
||||
if (src == null) {
|
||||
return;
|
||||
}
|
||||
if (blockLines == null) {
|
||||
blockLines = src;
|
||||
return;
|
||||
}
|
||||
int size = blockLines.size();
|
||||
int sizeAnother = src.size();
|
||||
while (sizeAnother > 0 && size < RECYCLE_LIMIT) {
|
||||
size++;
|
||||
sizeAnother--;
|
||||
blockLines.add(src.get(sizeAnother));
|
||||
}
|
||||
}
|
||||
|
||||
public static BlockLine obtainBlockLine() {
|
||||
return (blockLines == null || blockLines.isEmpty()) ? new BlockLine() : blockLines.remove(blockLines.size() - 1);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,253 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 Rosemoe
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.github.rosemoe.editor.text;
|
||||
|
||||
import com.itsaky.lsp.models.Range;
|
||||
|
||||
import io.github.rosemoe.editor.struct.BlockLine;
|
||||
import io.github.rosemoe.editor.struct.NavigationItem;
|
||||
import io.github.rosemoe.editor.struct.Span;
|
||||
import io.github.rosemoe.editor.syntax.EditorColorScheme;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* The result of analysis
|
||||
*/
|
||||
public class TextAnalyzeResult {
|
||||
|
||||
protected final List<BlockLine> mBlocks;
|
||||
protected final List<List<Span>> mSpanMap;
|
||||
|
||||
public Object mExtra;
|
||||
protected List<NavigationItem> mLabels;
|
||||
protected Span mLast;
|
||||
protected int mSuppressSwitch = Integer.MAX_VALUE;
|
||||
|
||||
private boolean determined = false;
|
||||
|
||||
private final Map<Integer, List<Range>> stringMap = new HashMap<> ();
|
||||
private final Map<Integer, List<Range>> commentMap = new HashMap<> ();
|
||||
|
||||
/**
|
||||
* Create a new result
|
||||
*/
|
||||
public TextAnalyzeResult () {
|
||||
mLast = null;
|
||||
mSpanMap = new ArrayList<> (2048);
|
||||
mBlocks = new ArrayList<> (1024);
|
||||
}
|
||||
|
||||
public Map<Integer, List<Range>> getStringMap () {
|
||||
return stringMap;
|
||||
}
|
||||
|
||||
public Map<Integer, List<Range>> getCommentMap () {
|
||||
return commentMap;
|
||||
}
|
||||
|
||||
public void addStringRange (int line, Range range) {
|
||||
List<Range> ranges = stringMap.get (line);
|
||||
if (ranges == null) {
|
||||
ranges = new ArrayList<> ();
|
||||
}
|
||||
ranges.add (range);
|
||||
stringMap.put (line, ranges);
|
||||
}
|
||||
|
||||
public void addCommentRange (int line, Range range) {
|
||||
List<Range> ranges = commentMap.get (line);
|
||||
if (ranges == null) {
|
||||
ranges = new ArrayList<> ();
|
||||
}
|
||||
ranges.add (range);
|
||||
commentMap.put (line, ranges);
|
||||
}
|
||||
|
||||
public Span addNormalSpanAt (int line, int column) {
|
||||
final var span = add (line, Span.obtain (line, column, EditorColorScheme.TEXT_NORMAL));
|
||||
span.underlineColor = 0;
|
||||
return span;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new span if required (colorId is different from last)
|
||||
*
|
||||
* @param spanLine Line
|
||||
* @param column Column
|
||||
* @param colorId Type
|
||||
* @return The span that was added
|
||||
*/
|
||||
public Span addIfNeeded (int spanLine, int column, int colorId) {
|
||||
if (mLast != null && mLast.colorId == colorId) {
|
||||
return mLast;
|
||||
}
|
||||
return add (spanLine, Span.obtain (spanLine, column, colorId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a span directly
|
||||
* Note: the line should always >= the line of span last committed
|
||||
* if two spans are on the same line, you must add them in order by their column
|
||||
*
|
||||
* @param spanLine The line position of span
|
||||
* @param span The span
|
||||
* @return The span that was added
|
||||
*/
|
||||
public Span add (int spanLine, Span span) {
|
||||
int mapLine = mSpanMap.size () - 1;
|
||||
if (spanLine == mapLine) {
|
||||
mSpanMap.get (spanLine).add (span);
|
||||
} else if (spanLine > mapLine) {
|
||||
Span extendedSpan = mLast;
|
||||
if (extendedSpan == null) {
|
||||
extendedSpan = Span.obtain (spanLine, 0, EditorColorScheme.TEXT_NORMAL);
|
||||
}
|
||||
while (mapLine < spanLine) {
|
||||
List<Span> lineSpans = new ArrayList<> ();
|
||||
lineSpans.add (extendedSpan.copy ().setColumn (0));
|
||||
mSpanMap.add (lineSpans);
|
||||
mapLine++;
|
||||
}
|
||||
List<Span> lineSpans = mSpanMap.get (spanLine);
|
||||
if (span.column == 0) {
|
||||
lineSpans.clear ();
|
||||
}
|
||||
lineSpans.add (span);
|
||||
} else {
|
||||
throw new IllegalStateException ("Invalid position");
|
||||
}
|
||||
mLast = span;
|
||||
return span;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method must be called when whole text is analyzed
|
||||
*
|
||||
* @param line The line is the line last of text
|
||||
*/
|
||||
public void determine (int line) {
|
||||
int mapLine = mSpanMap.size () - 1;
|
||||
Span extendedSpan = mLast;
|
||||
if (mLast == null) {
|
||||
extendedSpan = Span.obtain (line, 0, EditorColorScheme.TEXT_NORMAL);
|
||||
}
|
||||
while (mapLine < line) {
|
||||
List<Span> lineSpans = new ArrayList<> ();
|
||||
lineSpans.add (extendedSpan.copy ().setColumn (0));
|
||||
mSpanMap.add (lineSpans);
|
||||
mapLine++;
|
||||
}
|
||||
determined = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new BlockLine object
|
||||
* <strong>It fields maybe not initialized with zero</strong>
|
||||
*
|
||||
* @return An idle BlockLine
|
||||
*/
|
||||
public BlockLine obtainNewBlock () {
|
||||
return ObjectAllocator.obtainBlockLine ();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new code block info
|
||||
*
|
||||
* @param block Info of code block
|
||||
*/
|
||||
public void addBlockLine (BlockLine block) {
|
||||
mBlocks.add (block);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of code blocks
|
||||
*
|
||||
* @return code blocks
|
||||
*/
|
||||
public List<BlockLine> getBlocks () {
|
||||
return mBlocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the list not empty
|
||||
*/
|
||||
public void addNormalIfNull () {
|
||||
if (mSpanMap.isEmpty ()) {
|
||||
List<Span> spanList = new ArrayList<> ();
|
||||
spanList.add (Span.obtain (0, 0, EditorColorScheme.TEXT_NORMAL));
|
||||
mSpanMap.add (spanList);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get code navigation list
|
||||
*
|
||||
* @return Current navigation list
|
||||
*/
|
||||
public List<NavigationItem> getNavigation () {
|
||||
return mLabels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set code navigation list
|
||||
*
|
||||
* @param navigation New navigation list
|
||||
*/
|
||||
public void setNavigation (List<NavigationItem> navigation) {
|
||||
mLabels = navigation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns suppress switch
|
||||
*
|
||||
* @return suppress switch
|
||||
* @see TextAnalyzeResult#setSuppressSwitch(int)
|
||||
*/
|
||||
public int getSuppressSwitch () {
|
||||
return mSuppressSwitch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set suppress switch for editor
|
||||
* What is 'suppress switch' ?:
|
||||
* Suppress switch is a switch size for code block line drawing
|
||||
* and for the process to find out which code block the cursor is in.
|
||||
* Because the code blocks are not saved by the order of both start line and
|
||||
* end line,we are unable to know exactly when we should stop the process.
|
||||
* So without a suppress switch,it will cost a large of time to search code
|
||||
* blocks.So I added this switch.
|
||||
* A suppress switch is the code block count in the first layer code block
|
||||
* (as well as its sub code blocks).
|
||||
* If you are unsure,do not set it.
|
||||
* The default value if Integer.MAX_VALUE
|
||||
*
|
||||
* @param suppressSwitch Suppress switch
|
||||
*/
|
||||
public void setSuppressSwitch (int suppressSwitch) {
|
||||
mSuppressSwitch = suppressSwitch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get span map
|
||||
*/
|
||||
public List<List<Span>> getSpanMap () {
|
||||
return mSpanMap;
|
||||
}
|
||||
}
|
||||
@@ -1,274 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 Rosemoe
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.github.rosemoe.editor.text;
|
||||
|
||||
import com.itsaky.androidide.language.logs.LogLanguageImpl;
|
||||
import com.itsaky.androidide.utils.Logger;
|
||||
import com.itsaky.lsp.api.ILanguageServer;
|
||||
|
||||
import io.github.rosemoe.editor.interfaces.EditorLanguage;
|
||||
import io.github.rosemoe.editor.langs.EmptyLanguage;
|
||||
import io.github.rosemoe.editor.struct.BlockLine;
|
||||
import io.github.rosemoe.editor.struct.Span;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This is a manager of analyzing text
|
||||
*
|
||||
* @author Rose
|
||||
*/
|
||||
public class TextAnalyzer {
|
||||
|
||||
private static int sThreadId = 0;
|
||||
private final RecycleObjContainer mObjContainer = new RecycleObjContainer ();
|
||||
private final Object mLock = new Object ();
|
||||
|
||||
public long mOpStartTime;
|
||||
private TextAnalyzeResult mResult;
|
||||
private Callback mCallback;
|
||||
private AnalyzeThread mThread;
|
||||
private final EditorLanguage mLanguage;
|
||||
private final ILanguageServer mLanguageServer;
|
||||
private File file;
|
||||
|
||||
private static final Logger LOG = Logger.instance ("TextAnalyzer");
|
||||
|
||||
/**
|
||||
* Create a new manager for the given codeAnalyzer
|
||||
* @param language The language set to editor.
|
||||
* @param mLanguageServer
|
||||
* @param file
|
||||
*/
|
||||
public TextAnalyzer (EditorLanguage language, ILanguageServer mLanguageServer, File file) {
|
||||
this.mLanguageServer = mLanguageServer;
|
||||
this.file = file;
|
||||
if (language == null || language.getAnalyzer () == null) {
|
||||
throw new IllegalArgumentException ();
|
||||
}
|
||||
mResult = new TextAnalyzeResult ();
|
||||
mResult.addNormalIfNull ();
|
||||
mLanguage = language;
|
||||
}
|
||||
|
||||
public void setFile (File file) {
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
private synchronized static int nextThreadId () {
|
||||
sThreadId++;
|
||||
return sThreadId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set callback of analysis
|
||||
*
|
||||
* @param cb New callback
|
||||
*/
|
||||
public void setCallback (Callback cb) {
|
||||
mCallback = cb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the text analyzer
|
||||
*/
|
||||
public void shutdown () {
|
||||
final AnalyzeThread thread = mThread;
|
||||
if (thread != null && thread.isAlive ()) {
|
||||
thread.interrupt ();
|
||||
mThread = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from painting process to recycle outdated objects for reusing
|
||||
*/
|
||||
public void notifyRecycle () {
|
||||
mObjContainer.recycle ();
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze the given text
|
||||
*
|
||||
* @param origin The source text
|
||||
*/
|
||||
public synchronized void analyze (Content origin) {
|
||||
AnalyzeThread thread = this.mThread;
|
||||
if (thread == null || !thread.isAlive ()) {
|
||||
thread = this.mThread = new AnalyzeThread (mLanguage, mLock, origin);
|
||||
thread.setName ("TextAnalyzeDaemon-" + nextThreadId ());
|
||||
thread.setDaemon (true);
|
||||
thread.start ();
|
||||
} else {
|
||||
thread.restartWith (origin);
|
||||
synchronized (mLock) {
|
||||
mLock.notify ();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get analysis result
|
||||
*
|
||||
* @return Result of analysis
|
||||
*/
|
||||
public TextAnalyzeResult getResult () {
|
||||
return mResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for text analyzing
|
||||
*
|
||||
* @author Rose
|
||||
*/
|
||||
public interface Callback {
|
||||
|
||||
/**
|
||||
* Called when analyze result is available
|
||||
* Count of calling this method is not always equal to the count you call {@link TextAnalyzer#analyze(Content)}
|
||||
*
|
||||
* @param analyzer Host TextAnalyzer
|
||||
*/
|
||||
void onAnalyzeDone (TextAnalyzer analyzer);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Container for objects that is going to be recycled
|
||||
*
|
||||
* @author Rose
|
||||
*/
|
||||
static class RecycleObjContainer {
|
||||
|
||||
List<List<Span>> spanMap;
|
||||
|
||||
List<BlockLine> blockLines;
|
||||
|
||||
void recycle () {
|
||||
ObjectAllocator.recycleBlockLine (blockLines);
|
||||
SpanRecycler.getInstance ().recycle (spanMap);
|
||||
clear ();
|
||||
}
|
||||
|
||||
void clear () {
|
||||
spanMap = null;
|
||||
blockLines = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* AnalyzeThread to control
|
||||
*/
|
||||
public class AnalyzeThread extends Thread {
|
||||
|
||||
private final Object lock;
|
||||
private volatile boolean waiting = false;
|
||||
private Content content;
|
||||
private final EditorLanguage language;
|
||||
|
||||
/**
|
||||
* Create a new thread
|
||||
*
|
||||
* @param a The CodeAnalyzer to call
|
||||
* @param content The Content to analyze
|
||||
*/
|
||||
public AnalyzeThread (EditorLanguage lang, Object lock, Content content) {
|
||||
this.language = lang;
|
||||
this.lock = lock;
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run () {
|
||||
try {
|
||||
do {
|
||||
TextAnalyzeResult colors = new TextAnalyzeResult ();
|
||||
Delegate d = new Delegate ();
|
||||
mOpStartTime = System.currentTimeMillis ();
|
||||
do {
|
||||
waiting = false;
|
||||
language.getAnalyzer ().analyze (mLanguageServer, file, content, colors, d);
|
||||
if (waiting) {
|
||||
colors.mSpanMap.clear ();
|
||||
colors.mLast = null;
|
||||
colors.mBlocks.clear ();
|
||||
colors.mSuppressSwitch = Integer.MAX_VALUE;
|
||||
colors.mLabels = null;
|
||||
colors.mExtra = null;
|
||||
}
|
||||
} while (waiting);
|
||||
|
||||
mObjContainer.blockLines = mResult.mBlocks;
|
||||
mObjContainer.spanMap = mResult.mSpanMap;
|
||||
mResult = colors;
|
||||
colors.addNormalIfNull ();
|
||||
try {
|
||||
if (mCallback != null) {
|
||||
mCallback.onAnalyzeDone (TextAnalyzer.this);
|
||||
}
|
||||
} catch (NullPointerException e) {
|
||||
e.printStackTrace ();
|
||||
}
|
||||
|
||||
try {
|
||||
synchronized (lock) {
|
||||
lock.wait ();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
break;
|
||||
}
|
||||
} while (true);
|
||||
} catch (Exception ex) {
|
||||
if (!(language instanceof EmptyLanguage || language instanceof LogLanguageImpl)) {
|
||||
LOG.error ("An error occurred while analyzing", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* New content has been sent
|
||||
* Notify us to restart
|
||||
*
|
||||
* @param content New source
|
||||
*/
|
||||
public synchronized void restartWith (Content content) {
|
||||
waiting = true;
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
/**
|
||||
* A delegate for token stream loop
|
||||
* To make it stop in time
|
||||
*/
|
||||
public class Delegate {
|
||||
|
||||
/**
|
||||
* Whether new input is set
|
||||
* If it returns true,you should stop your tokenizing at once
|
||||
*
|
||||
* @return Whether re-analyze required
|
||||
*/
|
||||
public boolean shouldAnalyze () {
|
||||
return !waiting;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 Rosemoe
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.github.rosemoe.editor.util;
|
||||
|
||||
/**
|
||||
* Pack two int into a long
|
||||
* Also unpack it
|
||||
* This is convenient while passing data
|
||||
*
|
||||
* @author Rose
|
||||
*/
|
||||
public class IntPair {
|
||||
|
||||
/**
|
||||
* Convert an integer to a long whose binary bits are equal to the given integer
|
||||
*/
|
||||
private static long toUnsignedLong(int x) {
|
||||
return ((long) x) & 0xffffffffL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pack two int into a long
|
||||
*
|
||||
* @param first First of pair
|
||||
* @param second Second of pair
|
||||
* @return Packed value
|
||||
*/
|
||||
public static long pack(int first, int second) {
|
||||
return (toUnsignedLong(first) << 32L) | toUnsignedLong(second);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get second of pair
|
||||
*
|
||||
* @param packedValue Packed value
|
||||
* @return Second of pair
|
||||
*/
|
||||
public static int getSecond(long packedValue) {
|
||||
return (int) (packedValue & 0xFFFFFFFFL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get first of pair
|
||||
*
|
||||
* @param packedValue Packed value
|
||||
* @return First of pair
|
||||
*/
|
||||
public static int getFirst(long packedValue) {
|
||||
return (int) (packedValue >> 32L);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 Rosemoe
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.github.rosemoe.editor.util;
|
||||
|
||||
/**
|
||||
* ArrayList for primitive type long
|
||||
*/
|
||||
public class LongArrayList {
|
||||
|
||||
private long[] data;
|
||||
private int length;
|
||||
|
||||
public LongArrayList() {
|
||||
data = new long[128];
|
||||
}
|
||||
|
||||
public void add(long value) {
|
||||
data[length++] = value;
|
||||
if (data.length == length) {
|
||||
long[] newData = new long[length << 1];
|
||||
System.arraycopy(data, 0, newData, 0, length);
|
||||
data = newData;
|
||||
}
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return length;
|
||||
}
|
||||
|
||||
public long get(int index) {
|
||||
if (index > length || index < 0) {
|
||||
throw new ArrayIndexOutOfBoundsException(index);
|
||||
}
|
||||
return data[index];
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
length = 0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 Rosemoe
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.github.rosemoe.editor.util;
|
||||
|
||||
/**
|
||||
* Compatible implementation of Objects
|
||||
*
|
||||
* @author Rose
|
||||
*/
|
||||
public class Objects {
|
||||
|
||||
/**
|
||||
* If the value is null, throw a {@link NullPointerException}
|
||||
* Otherwise, return the value
|
||||
*/
|
||||
public static <T> T requireNonNull(T value) {
|
||||
if (value == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 Rosemoe
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.github.rosemoe.editor.widget;
|
||||
|
||||
import android.graphics.Paint;
|
||||
|
||||
import io.github.rosemoe.editor.text.Content;
|
||||
import io.github.rosemoe.editor.text.FontCache;
|
||||
|
||||
/**
|
||||
* Base layout implementation of {@link Layout}
|
||||
* This class has basic methods for its subclasses to measure texts
|
||||
*
|
||||
* @author Rose
|
||||
*/
|
||||
abstract class AbstractLayout implements Layout {
|
||||
|
||||
protected CodeEditor editor;
|
||||
protected Content text;
|
||||
protected Paint shadowPaint;
|
||||
protected FontCache fontCache;
|
||||
|
||||
public AbstractLayout(CodeEditor editor, Content text) {
|
||||
this.editor = editor;
|
||||
this.text = text;
|
||||
shadowPaint = new Paint(editor.getTextPaint());
|
||||
fontCache = new FontCache();
|
||||
}
|
||||
|
||||
protected float measureText(CharSequence text, int start, int end) {
|
||||
int tabCount = 0;
|
||||
end = Math.min(text.length(), end);
|
||||
for (int i = start; i < end; i++) {
|
||||
if (text.charAt(i) == '\t') {
|
||||
tabCount++;
|
||||
}
|
||||
}
|
||||
float extraWidth = fontCache.measureChar(' ', shadowPaint) * editor.getTabWidth() - fontCache.measureChar('\t', shadowPaint);
|
||||
return fontCache.measureText(text, start, end, shadowPaint) + tabCount * extraWidth;
|
||||
}
|
||||
|
||||
protected float[] orderedFindCharIndex(float targetOffset, CharSequence str, int index, int end) {
|
||||
float width = 0f;
|
||||
while (index < end && width < targetOffset) {
|
||||
float single = fontCache.measureChar(str.charAt(index), shadowPaint);
|
||||
if (str.charAt(index) == '\t') {
|
||||
single = editor.getTabWidth() * fontCache.measureChar(' ', shadowPaint);
|
||||
}
|
||||
width += single;
|
||||
index++;
|
||||
}
|
||||
return new float[]{index, width};
|
||||
}
|
||||
|
||||
protected float[] orderedFindCharIndex(float targetOffset, CharSequence str) {
|
||||
float width = 0f;
|
||||
int index = 0;
|
||||
int length = str.length();
|
||||
while (index < length && width < targetOffset) {
|
||||
float single = fontCache.measureChar(str.charAt(index), shadowPaint);
|
||||
if (str.charAt(index) == '\t') {
|
||||
single = editor.getTabWidth() * fontCache.measureChar(' ', shadowPaint);
|
||||
}
|
||||
width += single;
|
||||
index++;
|
||||
}
|
||||
return new float[]{index, width};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyLayout() {
|
||||
editor = null;
|
||||
text = null;
|
||||
shadowPaint = null;
|
||||
fontCache = null;
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 Rosemoe
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.github.rosemoe.editor.widget;
|
||||
|
||||
/**
|
||||
* This class is used to control cursor visibility
|
||||
*
|
||||
* @author Rose
|
||||
*/
|
||||
final class CursorBlink implements Runnable {
|
||||
|
||||
final CodeEditor editor;
|
||||
long lastSelectionModificationTime = 0;
|
||||
int period;
|
||||
boolean visibility;
|
||||
boolean valid;
|
||||
private float[] buffer;
|
||||
|
||||
CursorBlink(CodeEditor editor, int period) {
|
||||
visibility = true;
|
||||
this.editor = editor;
|
||||
this.period = period;
|
||||
}
|
||||
|
||||
void setPeriod(int period) {
|
||||
this.period = period;
|
||||
if (period <= 0) {
|
||||
visibility = true;
|
||||
valid = false;
|
||||
} else {
|
||||
valid = true;
|
||||
}
|
||||
}
|
||||
|
||||
void onSelectionChanged() {
|
||||
lastSelectionModificationTime = System.currentTimeMillis();
|
||||
visibility = true;
|
||||
}
|
||||
|
||||
boolean isSelectionVisible() {
|
||||
buffer = editor.mLayout.getCharLayoutOffset(editor.getCursor().getLeftLine(), editor.getCursor().getLeftColumn(), buffer);
|
||||
return (buffer[0] >= editor.getOffsetY() && buffer[0] - editor.getRowHeight() <= editor.getOffsetY() + editor.getHeight()
|
||||
&& buffer[1] >= editor.getOffsetX() && buffer[1] - 100f/* larger than a single character */ <= editor.getOffsetX() + editor.getWidth());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (valid && period > 0) {
|
||||
if (System.currentTimeMillis() - lastSelectionModificationTime >= period * 2) {
|
||||
visibility = !visibility;
|
||||
if (!editor.getCursor().isSelected() && isSelectionVisible()) {
|
||||
editor.invalidate();
|
||||
}
|
||||
}
|
||||
editor.postDelayed(this, period);
|
||||
} else {
|
||||
visibility = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 Rosemoe
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.github.rosemoe.editor.widget;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.TypedValue;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import com.itsaky.androidide.R;
|
||||
import com.itsaky.lsp.models.CompletionItem;
|
||||
|
||||
/**
|
||||
* Adapter to display results
|
||||
*
|
||||
* @author Rose
|
||||
*/
|
||||
@SuppressWarnings("CanBeFinal")
|
||||
class DefaultCompletionItemAdapter extends EditorCompletionAdapter {
|
||||
|
||||
@Override
|
||||
public int getItemHeight() {
|
||||
return (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 30, Resources.getSystem().getDisplayMetrics());
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int pos, View view, ViewGroup parent, boolean isCurrentCursorPosition) {
|
||||
if (view == null) {
|
||||
view = LayoutInflater.from(getContext()).inflate(R.layout.default_completion_result_item, parent, false);
|
||||
}
|
||||
final CompletionItem item = getItem(pos);
|
||||
final String label = item.getLabel();
|
||||
final String desc = item.getDetail();
|
||||
final Drawable icon = ContextCompat.getDrawable(parent.getContext(), R.mipmap.box_red);
|
||||
|
||||
TextView tv = view.findViewById(R.id.result_item_label);
|
||||
tv.setText(label);
|
||||
tv = view.findViewById(R.id.result_item_desc);
|
||||
tv.setText(desc);
|
||||
view.setTag(pos);
|
||||
|
||||
if (isCurrentCursorPosition) {
|
||||
view.setBackgroundColor(0xffdddddd);
|
||||
} else {
|
||||
view.setBackgroundColor(0xffffffff);
|
||||
}
|
||||
|
||||
ImageView iv = view.findViewById(R.id.result_item_image);
|
||||
iv.setImageDrawable(icon);
|
||||
return view;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,361 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 Rosemoe
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.github.rosemoe.editor.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.GradientDrawable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
import com.itsaky.androidide.databinding.LayoutCompletionWindowBinding;
|
||||
import com.itsaky.androidide.utils.JavaCharacter;
|
||||
import com.itsaky.androidide.utils.Logger;
|
||||
import com.itsaky.lsp.models.CompletionItem;
|
||||
import com.itsaky.lsp.models.Position;
|
||||
import com.itsaky.lsp.models.Range;
|
||||
import com.itsaky.lsp.models.TextEdit;
|
||||
|
||||
import io.github.rosemoe.editor.interfaces.AutoCompleteProvider;
|
||||
import io.github.rosemoe.editor.syntax.EditorColorScheme;
|
||||
import io.github.rosemoe.editor.text.CharPosition;
|
||||
import io.github.rosemoe.editor.text.Content;
|
||||
import io.github.rosemoe.editor.text.TextAnalyzeResult;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Auto complete window for editing code quicker
|
||||
*
|
||||
* @author Rose
|
||||
*/
|
||||
public class EditorAutoCompleteWindow extends EditorBasePopupWindow {
|
||||
|
||||
private final static String TIP = "Refreshing...";
|
||||
private final CodeEditor mEditor;
|
||||
private final GradientDrawable mBg;
|
||||
protected boolean mCancelShowUp = false;
|
||||
private int mCurrent = 0;
|
||||
private String mLastPrefix;
|
||||
private AutoCompleteProvider mProvider;
|
||||
private boolean mLoading;
|
||||
private int mMaxHeight;
|
||||
private EditorCompletionAdapter mAdapter;
|
||||
private final LayoutCompletionWindowBinding mBinding;
|
||||
|
||||
/**
|
||||
* Create a panel instance for the given editor
|
||||
*
|
||||
* @param editor Target editor
|
||||
*/
|
||||
public EditorAutoCompleteWindow(CodeEditor editor) {
|
||||
super(editor);
|
||||
mEditor = editor;
|
||||
mAdapter = new DefaultCompletionItemAdapter();
|
||||
mBinding = LayoutCompletionWindowBinding.inflate(LayoutInflater.from(mEditor.getContext()));
|
||||
mBinding.tip.setText(TIP);
|
||||
setContentView(mBinding.getRoot());
|
||||
GradientDrawable gd = new GradientDrawable();
|
||||
gd.setCornerRadii(new float[]{6, 6, 0, 0, 6, 6, 6, 6}); // (top-left, top-right, bottom-right, bottom-left) x 2
|
||||
mBinding.getRoot().setBackground(gd);
|
||||
mBg = gd;
|
||||
applyColorScheme();
|
||||
mBinding.list.setDividerHeight(0);
|
||||
setLoading(true);
|
||||
mBinding.list.setOnItemClickListener((parent, view, position, id) -> {
|
||||
try {
|
||||
select(position);
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(mEditor.getContext(), e.toString(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void setAdapter(EditorCompletionAdapter adapter) {
|
||||
mAdapter = adapter;
|
||||
if (adapter == null) {
|
||||
mAdapter = new DefaultCompletionItemAdapter();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void show() {
|
||||
if (mCancelShowUp) {
|
||||
return;
|
||||
}
|
||||
|
||||
super.show();
|
||||
}
|
||||
|
||||
public Context getContext() {
|
||||
return mEditor.getContext();
|
||||
}
|
||||
|
||||
public int getCurrentPosition() {
|
||||
return mCurrent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a auto completion items provider
|
||||
*
|
||||
* @param provider New provider.can not be null
|
||||
*/
|
||||
public void setProvider(AutoCompleteProvider provider) {
|
||||
mProvider = provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply colors for self
|
||||
*/
|
||||
public void applyColorScheme() {
|
||||
EditorColorScheme colors = mEditor.getColorScheme();
|
||||
mBg.setStroke(1, colors.getColor(EditorColorScheme.AUTO_COMP_PANEL_CORNER));
|
||||
mBg.setColor(colors.getColor(EditorColorScheme.AUTO_COMP_PANEL_BG));
|
||||
}
|
||||
|
||||
/**
|
||||
* Change layout to loading/idle
|
||||
*
|
||||
* @param state Whether loading
|
||||
*/
|
||||
public void setLoading(boolean state) {
|
||||
mLoading = state;
|
||||
if (state) {
|
||||
mEditor.postDelayed(() -> {
|
||||
if (mLoading) {
|
||||
mBinding.tip.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}, 300);
|
||||
} else {
|
||||
mBinding.tip.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move selection down
|
||||
*/
|
||||
public void moveDown() {
|
||||
if (mCurrent + 1 >= mBinding.list.getAdapter().getCount()) {
|
||||
return;
|
||||
}
|
||||
mCurrent++;
|
||||
((EditorCompletionAdapter) mBinding.list.getAdapter()).notifyDataSetChanged();
|
||||
ensurePosition();
|
||||
}
|
||||
|
||||
/**
|
||||
* Move selection up
|
||||
*/
|
||||
public void moveUp() {
|
||||
if (mCurrent - 1 < 0) {
|
||||
return;
|
||||
}
|
||||
mCurrent--;
|
||||
((EditorCompletionAdapter) mBinding.list.getAdapter()).notifyDataSetChanged();
|
||||
ensurePosition();
|
||||
}
|
||||
|
||||
/**
|
||||
* Make current selection visible
|
||||
*/
|
||||
private void ensurePosition() {
|
||||
mBinding.list.setSelection(mCurrent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select current position
|
||||
*/
|
||||
public void select() {
|
||||
select(mCurrent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the given position
|
||||
*
|
||||
* @param pos Index of auto complete item
|
||||
*/
|
||||
public void select(int pos) {
|
||||
final CompletionItem item = ((EditorCompletionAdapter) mBinding.list.getAdapter()).getItem(pos);
|
||||
final Range range = getIdentifierRange(mEditor.getCursor().getRight(), mEditor.getText());
|
||||
|
||||
String text = item.getInsertText() == null ? item.getLabel() : item.getInsertText();
|
||||
final boolean shiftCursor = item.getInsertText() != null && text.contains("$0");
|
||||
|
||||
mEditor.getText().delete(
|
||||
range.getStart().getLine(),
|
||||
range.getStart().getColumn (),
|
||||
range.getEnd().getLine(),
|
||||
range.getEnd().getColumn ()
|
||||
);
|
||||
if(!text.contains("\n")) {
|
||||
mEditor.getCursor().onCommitText(text);
|
||||
} else {
|
||||
final String[] lines = text.split("\n");
|
||||
mEditor.getCursor().onCommitText(lines[0]);
|
||||
for(int i=1;i<lines.length;i++) {
|
||||
mEditor.getCursor().onCommitText("\n" + lines[i].trim());
|
||||
}
|
||||
}
|
||||
|
||||
if(shiftCursor) {
|
||||
final int line = mEditor.getCursor().getLeftLine();
|
||||
final String lineText = mEditor.getText().getLineString(line);
|
||||
final int column = lineText.lastIndexOf("$0");
|
||||
if(column != -1){
|
||||
mEditor.setSelection(line, column);
|
||||
mEditor.getText().delete(line, column, line, column + 2);
|
||||
}
|
||||
}
|
||||
|
||||
final List<TextEdit> edits = item.getAdditionalTextEdits();
|
||||
if(edits != null && edits.size() > 0) {
|
||||
for(int i=0;i<edits.size();i++) {
|
||||
final TextEdit edit = edits.get(i);
|
||||
if(edit == null) {
|
||||
continue;
|
||||
}
|
||||
final Position start = edit.getRange().getStart();
|
||||
final Position end = edit.getRange().getEnd();
|
||||
if(start.equals(end)) {
|
||||
mEditor.getText().insert(start.getLine(), start.getColumn (), edit.getNewText());
|
||||
} else {
|
||||
mEditor.getText().replace(start.getLine(),
|
||||
start.getColumn (),
|
||||
end.getLine(),
|
||||
end.getColumn (),
|
||||
edit.getNewText());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(item.getCommand() != null) {
|
||||
var cmd = item.getCommand();
|
||||
if("editor.action.triggerParameterHints".equals(cmd.getCommand())) {
|
||||
|
||||
// Trigger signature help request if included in CompletionItem
|
||||
// TODO Don't rely on CompletionItem for requesting signature help
|
||||
|
||||
// If the insert text contains '(', automatically trigger this action
|
||||
mEditor.signatureHelp("(");
|
||||
}
|
||||
}
|
||||
|
||||
mEditor.postHideCompletionWindow();
|
||||
}
|
||||
|
||||
private Range getIdentifierRange(int end, Content content) {
|
||||
int start = end;
|
||||
while(start > 0 && JavaCharacter.isJavaIdentifierPart(content.charAt(start - 1))) {
|
||||
start--;
|
||||
}
|
||||
|
||||
CharPosition startPos = content.getIndexer().getCharPosition(start);
|
||||
CharPosition endPos = content.getIndexer().getCharPosition(end);
|
||||
return new Range(new Position(startPos.line, startPos.column), new Position(endPos.line, endPos.column));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get prefix set
|
||||
*
|
||||
* @return The previous prefix
|
||||
*/
|
||||
public String getPrefix() {
|
||||
return mLastPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set prefix for auto complete analysis
|
||||
*
|
||||
* @param prefix The user's input code's prefix
|
||||
*/
|
||||
public void setPrefix(String prefix) {
|
||||
if (mCancelShowUp) {
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
mLastPrefix = prefix;
|
||||
new MatchThread(prefix).start();
|
||||
}
|
||||
|
||||
public void setMaxHeight(int height) {
|
||||
mMaxHeight = height;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display result of analysis
|
||||
*
|
||||
* @param results Items of analysis
|
||||
*/
|
||||
public void displayResults (final List<CompletionItem> results) {
|
||||
mEditor.post(() -> {
|
||||
setLoading(false);
|
||||
mAdapter.clear();
|
||||
mAdapter.attachAttributes(this, results);
|
||||
if (results == null || results.isEmpty()) {
|
||||
if (isShowing ()) {
|
||||
hide();
|
||||
}
|
||||
return;
|
||||
}
|
||||
mBinding.list.setAdapter(mAdapter);
|
||||
mCurrent = 0;
|
||||
float newHeight = mEditor.getDpUnit() * mAdapter.getItemHeight() * results.size();
|
||||
|
||||
show ();
|
||||
update(getWidth(), (int) Math.min(newHeight, mMaxHeight));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Analysis thread
|
||||
*
|
||||
* @author Rose
|
||||
*/
|
||||
private class MatchThread extends Thread {
|
||||
private final String mPrefix;
|
||||
private final String mFileUri;
|
||||
private final Content mContent;
|
||||
private final boolean mInner;
|
||||
private final TextAnalyzeResult mColors;
|
||||
private final int mIndex;
|
||||
private final int mLine;
|
||||
private final int mColumn;
|
||||
private final AutoCompleteProvider mLocalProvider = mProvider;
|
||||
|
||||
public MatchThread (String prefix) {
|
||||
mPrefix = prefix;
|
||||
mFileUri = mEditor.getFile() != null ? mEditor.getFile().toURI().toString() : null;
|
||||
mContent = mEditor.getText();
|
||||
mColors = mEditor.getTextAnalyzeResult();
|
||||
mIndex = mEditor.getCursor().getLeft();
|
||||
mLine = mEditor.getCursor().getLeftLine();
|
||||
mColumn = mEditor.getCursor().getLeftColumn();
|
||||
mInner = (!mEditor.isHighlightCurrentBlock()) || (mEditor.getBlockIndex() != -1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
displayResults(mLocalProvider.getAutoCompleteItems(mContent, mFileUri, mPrefix, mInner, mColors, mIndex, mLine, mColumn));
|
||||
} catch (Exception e) {
|
||||
LOG.error ("Error computing completion", e);
|
||||
displayResults(new ArrayList<> ());
|
||||
}
|
||||
}
|
||||
|
||||
private final Logger LOG = Logger.instance ("CompletionMatchThread");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 Rosemoe
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.github.rosemoe.editor.widget;
|
||||
|
||||
import android.view.Gravity;
|
||||
import android.widget.PopupWindow;
|
||||
import com.itsaky.androidide.R;
|
||||
|
||||
/**
|
||||
* Editor base panel class
|
||||
*
|
||||
* @author Rose
|
||||
*/
|
||||
class EditorBasePopupWindow extends PopupWindow {
|
||||
private CodeEditor mEditor;
|
||||
protected int[] mLocation;
|
||||
protected int mTop;
|
||||
protected int mLeft;
|
||||
|
||||
/**
|
||||
* Create a panel for editor
|
||||
*
|
||||
* @param editor Target editor
|
||||
*/
|
||||
public EditorBasePopupWindow(CodeEditor editor) {
|
||||
if (editor == null) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
mLocation = new int[2];
|
||||
mEditor = editor;
|
||||
super.setTouchable(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the left position on the editor rect
|
||||
*
|
||||
* @param x X on editor
|
||||
*/
|
||||
public void setExtendedX(float x) {
|
||||
mLeft = (int) x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the top position on the editor rect
|
||||
*
|
||||
* @param y Y on editor
|
||||
*/
|
||||
public void setExtendedY(float y) {
|
||||
mTop = (int) y;
|
||||
}
|
||||
|
||||
public void updatePosition() {
|
||||
int width = mEditor.getWidth();
|
||||
if (mLeft > width - getWidth()) {
|
||||
mLeft = width - getWidth();
|
||||
}
|
||||
int height = mEditor.getHeight();
|
||||
if (mTop > height - getHeight()) {
|
||||
mTop = height - getHeight();
|
||||
}
|
||||
if (mTop < 0) {
|
||||
mTop = 0;
|
||||
}
|
||||
if (mLeft < 0) {
|
||||
mLeft = 0;
|
||||
}
|
||||
mEditor.getLocationInWindow(mLocation);
|
||||
if (isShowing()) {
|
||||
update(mLocation[0] + mLeft, mLocation[1] + mTop, getWidth(), getHeight());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the panel or update its position(If already shown)
|
||||
*/
|
||||
public void show() {
|
||||
int width = mEditor.getWidth();
|
||||
if (mLeft > width - getWidth()) {
|
||||
mLeft = width - getWidth();
|
||||
}
|
||||
int height = mEditor.getHeight();
|
||||
if (mTop > height - getHeight()) {
|
||||
mTop = height - getHeight();
|
||||
}
|
||||
if (mTop < 0) {
|
||||
mTop = 0;
|
||||
}
|
||||
if (mLeft < 0) {
|
||||
mLeft = 0;
|
||||
}
|
||||
mEditor.getLocationInWindow(mLocation);
|
||||
if (isShowing()) {
|
||||
update(mLocation[0] + mLeft, mLocation[1] + mTop, getWidth(), getHeight());
|
||||
return;
|
||||
}
|
||||
setAnimationStyle(R.style.PopupAnimation);
|
||||
super.showAtLocation(mEditor,
|
||||
Gravity.START | Gravity.TOP,
|
||||
mLocation[0] + mLeft, mLocation[1] + mTop);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the panel (If shown)
|
||||
*/
|
||||
public void hide() {
|
||||
if (isShowing()) {
|
||||
super.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,260 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 Rosemoe
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.github.rosemoe.editor.widget;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.Gravity;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.appcompat.view.menu.MenuBuilder;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.blankj.utilcode.util.SizeUtils;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.itsaky.androidide.R;
|
||||
import com.itsaky.androidide.app.StudioApp;
|
||||
import com.itsaky.androidide.utils.DialogUtils;
|
||||
import com.itsaky.lsp.models.CodeActionItem;
|
||||
import com.itsaky.toaster.Toaster;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.github.rosemoe.editor.util.IntPair;
|
||||
|
||||
/**
|
||||
* Action Mode style text action panel for editor
|
||||
*
|
||||
* @author Rose
|
||||
*/
|
||||
class EditorTextActionModeStarter implements CodeEditor.EditorTextActionPresenter {
|
||||
|
||||
private final CodeEditor mEditor;
|
||||
private ActionMode mActionMode;
|
||||
|
||||
EditorTextActionModeStarter(CodeEditor editor) {
|
||||
mEditor = editor;
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
@Override
|
||||
public void onBeginTextSelect() {
|
||||
if (mActionMode != null) {
|
||||
return;
|
||||
}
|
||||
mActionMode = ((AppCompatActivity) mEditor.getContext()).startSupportActionMode(new ActionMode.Callback() {
|
||||
|
||||
private List<CodeActionItem> fixes;
|
||||
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
|
||||
mEditor.mStartedActionMode = CodeEditor.ACTION_MODE_SELECT_TEXT;
|
||||
if (menu instanceof MenuBuilder) {
|
||||
MenuBuilder builder = (MenuBuilder) menu;
|
||||
builder.setOptionalIconsVisible(true);
|
||||
}
|
||||
actionMode.setTitle(android.R.string.selectTextMode);
|
||||
TypedArray array = mEditor.getContext().getTheme().obtainStyledAttributes(new int[]{
|
||||
android.R.attr.actionModeSelectAllDrawable,
|
||||
android.R.attr.actionModeCutDrawable,
|
||||
android.R.attr.actionModeCopyDrawable,
|
||||
android.R.attr.actionModePasteDrawable,
|
||||
});
|
||||
|
||||
menu.add (0, 8, 0, R.string.action_expand_selection)
|
||||
.setShowAsActionFlags (1)
|
||||
.setIcon (createDrawable (R.drawable.ic_expand_selection));
|
||||
|
||||
menu.add(0, 0, 0, mEditor.getContext().getString(android.R.string.selectAll))
|
||||
.setShowAsActionFlags(1)
|
||||
.setIcon(array.getDrawable(0));
|
||||
|
||||
if (mEditor.isEditable()) {
|
||||
menu.add(0, 1, 0, mEditor.getContext().getString(android.R.string.cut))
|
||||
.setShowAsActionFlags(1)
|
||||
.setIcon(array.getDrawable(1));
|
||||
}
|
||||
|
||||
menu.add(0, 2, 0, mEditor.getContext().getString(android.R.string.copy))
|
||||
.setShowAsActionFlags(1)
|
||||
.setIcon(array.getDrawable(2));
|
||||
|
||||
if (mEditor.isEditable()) {
|
||||
menu.add(0, 3, 0, mEditor.getContext().getString(android.R.string.paste))
|
||||
.setShowAsActionFlags(1)
|
||||
.setIcon(array.getDrawable(3));
|
||||
}
|
||||
|
||||
menu.add(0, 4, 0, mEditor.getContext().getString(com.itsaky.androidide.R.string.menu_navigate_definition))
|
||||
.setShowAsActionFlags(1)
|
||||
.setIcon(createDrawable(com.itsaky.androidide.R.drawable.ic_goto_definition));
|
||||
|
||||
menu.add(0, 5, 0, mEditor.getContext().getString(com.itsaky.androidide.R.string.menu_navigate_references))
|
||||
.setShowAsActionFlags(1)
|
||||
.setIcon(createDrawable(com.itsaky.androidide.R.drawable.ic_find_references));
|
||||
|
||||
menu.add(0, 6, 0, mEditor.getContext().getString(com.itsaky.androidide.R.string.menu_comment_line))
|
||||
.setShowAsActionFlags(1)
|
||||
.setIcon(createDrawable(com.itsaky.androidide.R.drawable.ic_comment_line));
|
||||
|
||||
menu.add(0, 7, 0, mEditor.getContext().getString(com.itsaky.androidide.R.string.menu_uncomment_line))
|
||||
.setShowAsActionFlags(1)
|
||||
.setIcon(createDrawable(com.itsaky.androidide.R.drawable.ic_uncomment_line));
|
||||
|
||||
array.recycle();
|
||||
return true;
|
||||
}
|
||||
|
||||
private Drawable createDrawable(int icon) {
|
||||
final Drawable d = ContextCompat.getDrawable(mEditor.getContext(), icon);
|
||||
if (d != null) {
|
||||
d.setColorFilter(ContextCompat.getColor(mEditor.getContext(), com.itsaky.androidide.R.color.secondaryColor), PorterDuff.Mode.SRC_ATOP);
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
|
||||
if (mEditor.getFile() == null) return false;
|
||||
|
||||
final String name = mEditor.getFile().getName();
|
||||
final boolean isJava = name.endsWith(".java") || name.endsWith(".gradle");
|
||||
final boolean isXml = name.endsWith(".xml");
|
||||
final MenuItem def = menu.findItem(4);
|
||||
final MenuItem ref = menu.findItem(5);
|
||||
final MenuItem comment = menu.findItem(6);
|
||||
final MenuItem uncomment = menu.findItem(7);
|
||||
|
||||
comment.setEnabled(isJava || isXml).getIcon().setAlpha(isJava || isXml ? 255 : 76);
|
||||
uncomment.setEnabled(isJava || isXml).getIcon().setAlpha(isJava || isXml ? 255 : 76);
|
||||
|
||||
// These menu items may, or may not be added
|
||||
// So we need to check if its null or not
|
||||
// before we perform any further actions
|
||||
if (def != null) {
|
||||
def.getIcon().setAlpha(isJava ? 255 : 76);
|
||||
def.setVisible(isJava);
|
||||
}
|
||||
|
||||
if (ref != null) {
|
||||
ref.getIcon().setAlpha(isJava ? 255 : 76);
|
||||
ref.setVisible(isJava);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
|
||||
switch (menuItem.getItemId()) {
|
||||
case 0:
|
||||
mEditor.selectAll();
|
||||
break;
|
||||
case 1:
|
||||
mEditor.cutText();
|
||||
break;
|
||||
case 2:
|
||||
mEditor.copyText();
|
||||
break;
|
||||
case 3:
|
||||
mEditor.pasteText();
|
||||
break;
|
||||
case 4:
|
||||
mEditor.findDefinition();
|
||||
break;
|
||||
case 5:
|
||||
mEditor.findReferences();
|
||||
break;
|
||||
case 6:
|
||||
mEditor.commentLine();
|
||||
break;
|
||||
case 7:
|
||||
mEditor.uncommentLine();
|
||||
break;
|
||||
case 8 :
|
||||
mEditor.expandSelection ();
|
||||
break;
|
||||
}
|
||||
|
||||
final var id = menuItem.getItemId ();
|
||||
if (id == 0 || id == 8) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return onExit ();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode actionMode) {
|
||||
mEditor.mStartedActionMode = CodeEditor.ACTION_MODE_NONE;
|
||||
mActionMode = null;
|
||||
mEditor.setSelection(mEditor.getCursor().getLeftLine(), mEditor.getCursor().getLeftColumn());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpdate() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpdate(int updateReason) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectedTextClicked(MotionEvent event) {
|
||||
long packed = mEditor.getPointPositionOnScreen(event.getX(), event.getY());
|
||||
int line = IntPair.getFirst(packed);
|
||||
int column = IntPair.getSecond(packed);
|
||||
mEditor.setSelection(line, column);
|
||||
mEditor.hideAutoCompleteWindow();
|
||||
mEditor.hideDiagnosticWindow();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextSelectionEnd() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onExit() {
|
||||
if (mActionMode != null) {
|
||||
mActionMode.finish();
|
||||
mActionMode = null;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldShowCursor() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 Rosemoe
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.github.rosemoe.editor.widget;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.drawable.GradientDrawable;
|
||||
import android.util.TypedValue;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
|
||||
import com.itsaky.androidide.R;
|
||||
|
||||
/**
|
||||
* This will show when selecting text
|
||||
*
|
||||
* @author Rose
|
||||
*/
|
||||
class EditorTextActionWindow extends EditorBasePopupWindow implements View.OnClickListener, CodeEditor.EditorTextActionPresenter {
|
||||
private final CodeEditor mEditor;
|
||||
private final Button mPasteBtn;
|
||||
private final Button mCopyBtn;
|
||||
private final Button mCutBtn;
|
||||
private final View mRootView;
|
||||
private int maxWidth;
|
||||
|
||||
/**
|
||||
* Create a panel for the given editor
|
||||
*
|
||||
* @param editor Target editor
|
||||
*/
|
||||
public EditorTextActionWindow(CodeEditor editor) {
|
||||
super(editor);
|
||||
mEditor = editor;
|
||||
// Since popup window does provide decor view, we have to pass null to this method
|
||||
@SuppressLint("InflateParams")
|
||||
View root = LayoutInflater.from(editor.getContext()).inflate(R.layout.text_compose_panel, null);
|
||||
Button selectAll = root.findViewById(R.id.panel_btn_select_all);
|
||||
Button cut = root.findViewById(R.id.panel_btn_cut);
|
||||
Button copy = root.findViewById(R.id.panel_btn_copy);
|
||||
mPasteBtn = root.findViewById(R.id.panel_btn_paste);
|
||||
mCopyBtn = copy;
|
||||
mCutBtn = cut;
|
||||
selectAll.setOnClickListener(this);
|
||||
cut.setOnClickListener(this);
|
||||
copy.setOnClickListener(this);
|
||||
mPasteBtn.setOnClickListener(this);
|
||||
GradientDrawable gd = new GradientDrawable();
|
||||
gd.setCornerRadius(5);
|
||||
gd.setColor(0xffffffff);
|
||||
root.setBackground(gd);
|
||||
setContentView(root);
|
||||
mRootView = root;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBeginTextSelect() {
|
||||
float dpUnit = mEditor.getDpUnit();
|
||||
setHeight((int) (dpUnit * 60));
|
||||
maxWidth = (int) (dpUnit * 230);
|
||||
setWidth(maxWidth);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onExit() {
|
||||
boolean result = isShowing();
|
||||
hide();
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpdate() {
|
||||
hide();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpdate(int updateReason) {
|
||||
hide();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectedTextClicked(MotionEvent event) {
|
||||
EditorTextActionWindow panel = this;
|
||||
if (panel.isShowing()) {
|
||||
panel.hide();
|
||||
} else {
|
||||
int first = mEditor.getFirstVisibleRow();
|
||||
int last = mEditor.getLastVisibleRow();
|
||||
int left = mEditor.getCursor().getLeftLine();
|
||||
int right = mEditor.getCursor().getRightLine();
|
||||
int toLineBottom;
|
||||
if (right <= first) {
|
||||
toLineBottom = first;
|
||||
} else if (right > last) {
|
||||
if (left <= first) {
|
||||
toLineBottom = (first + last) / 2;
|
||||
} else if (left >= last) {
|
||||
toLineBottom = last - 2;
|
||||
} else {
|
||||
if (left + 3 >= last) {
|
||||
toLineBottom = left - 2;
|
||||
} else {
|
||||
toLineBottom = left + 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (left <= first) {
|
||||
if (right + 3 >= last) {
|
||||
toLineBottom = right - 2;
|
||||
} else {
|
||||
toLineBottom = right + 1;
|
||||
}
|
||||
} else {
|
||||
if (left + 5 >= right) {
|
||||
toLineBottom = right + 1;
|
||||
} else {
|
||||
toLineBottom = (left + right) / 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
toLineBottom = Math.max(0, toLineBottom);
|
||||
int panelY = mEditor.getRowBottom(toLineBottom) - mEditor.getOffsetY();
|
||||
float handleLeftX = mEditor.getOffset(left, mEditor.getCursor().getLeftColumn());
|
||||
float handleRightX = mEditor.getOffset(right, mEditor.getCursor().getRightColumn());
|
||||
int panelX = (int) ((handleLeftX + handleRightX) / 2f);
|
||||
panel.setExtendedX(panelX);
|
||||
panel.setExtendedY(panelY);
|
||||
panel.show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextSelectionEnd() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldShowCursor() {
|
||||
return !isShowing();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the state of paste button
|
||||
*/
|
||||
private void updateBtnState() {
|
||||
mPasteBtn.setEnabled(mEditor.hasClip());
|
||||
mCopyBtn.setVisibility(mEditor.getCursor().isSelected() ? View.VISIBLE : View.GONE);
|
||||
mCutBtn.setVisibility(mEditor.getCursor().isSelected() ? View.VISIBLE : View.GONE);
|
||||
mRootView.measure(View.MeasureSpec.makeMeasureSpec(1000000, View.MeasureSpec.AT_MOST), View.MeasureSpec.makeMeasureSpec(100000, View.MeasureSpec.AT_MOST));
|
||||
setWidth(Math.min(mRootView.getMeasuredWidth(), maxWidth));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void show() {
|
||||
updateBtnState();
|
||||
setElevation(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 4, Resources.getSystem().getDisplayMetrics()));
|
||||
super.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View p1) {
|
||||
int id = p1.getId();
|
||||
if (id == R.id.panel_btn_select_all) {
|
||||
mEditor.selectAll();
|
||||
} else if (id == R.id.panel_btn_cut) {
|
||||
mEditor.copyText();
|
||||
if (mEditor.getCursor().isSelected()) {
|
||||
mEditor.getCursor().onDeleteKeyPressed();
|
||||
}
|
||||
} else if (id == R.id.panel_btn_paste) {
|
||||
mEditor.pasteText();
|
||||
mEditor.setSelection(mEditor.getCursor().getRightLine(), mEditor.getCursor().getRightColumn());
|
||||
} else if (id == R.id.panel_btn_copy) {
|
||||
mEditor.copyText();
|
||||
mEditor.setSelection(mEditor.getCursor().getRightLine(), mEditor.getCursor().getRightColumn());
|
||||
}
|
||||
hide();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 Rosemoe
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.github.rosemoe.editor.widget;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.view.KeyEvent;
|
||||
|
||||
/**
|
||||
* Handles key events such SHIFT
|
||||
* @author Rosemoe
|
||||
*/
|
||||
public class KeyMetaStates extends android.text.method.MetaKeyKeyListener {
|
||||
|
||||
private CodeEditor editor;
|
||||
/**
|
||||
* Dummy text used for Android original APIs
|
||||
*/
|
||||
private Editable dest = Editable.Factory.getInstance().newEditable("");
|
||||
|
||||
public KeyMetaStates(CodeEditor editor) {
|
||||
this.editor = editor;
|
||||
}
|
||||
|
||||
public void onKeyDown(KeyEvent event) {
|
||||
super.onKeyDown(editor, dest, event.getKeyCode(), event);
|
||||
}
|
||||
|
||||
public void onKeyUp(KeyEvent event) {
|
||||
super.onKeyUp(editor, dest, event.getKeyCode(), event);
|
||||
}
|
||||
|
||||
public boolean isShiftPressed() {
|
||||
return getMetaState(dest, META_SHIFT_ON) != 0;
|
||||
}
|
||||
|
||||
public void adjust() {
|
||||
adjustMetaAfterKeypress(dest);
|
||||
}
|
||||
|
||||
public void clearMetaStates(int states) {
|
||||
clearMetaKeyState(editor, dest, states);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 Rosemoe
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.github.rosemoe.editor.widget;
|
||||
|
||||
/**
|
||||
* This class represents a 'row' in editor
|
||||
* Editor uses this to draw rows
|
||||
*
|
||||
* @author Rose
|
||||
*/
|
||||
class Row {
|
||||
|
||||
/**
|
||||
* The index in lines
|
||||
* But not row index
|
||||
*/
|
||||
public int lineIndex;
|
||||
|
||||
/**
|
||||
* Whether this row is a start of a line
|
||||
* Editor will draw line number to left of this row to indicate this
|
||||
*/
|
||||
public boolean isLeadingRow;
|
||||
|
||||
/**
|
||||
* Start index in target line
|
||||
*/
|
||||
public int startColumn;
|
||||
|
||||
/**
|
||||
* End index in target line
|
||||
*/
|
||||
public int endColumn;
|
||||
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 Rosemoe
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.github.rosemoe.editor.widget;
|
||||
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
/**
|
||||
* Row iterator.
|
||||
* This iterator is able to return a series of Row objects linearly
|
||||
* Editor uses this to get information of rows and paint them accordingly
|
||||
*
|
||||
* @author Rose
|
||||
*/
|
||||
interface RowIterator {
|
||||
|
||||
/**
|
||||
* Return next Row object
|
||||
* <p>
|
||||
* The result should not be stored, because implementing classes will always return the same
|
||||
* object due to performance
|
||||
*
|
||||
* @return Row object contains the information about a row
|
||||
* @throws NoSuchElementException If no more row available
|
||||
*/
|
||||
Row next();
|
||||
|
||||
/**
|
||||
* Whether there is more Row object
|
||||
*
|
||||
* @return Whether more row available
|
||||
*/
|
||||
boolean hasNext();
|
||||
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package io.github.rosemoe.editor.widget;
|
||||
|
||||
import com.itsaky.androidide.utils.Symbols;
|
||||
import io.github.rosemoe.editor.text.Cursor;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A channel to insert symbols in {@link CodeEditor}
|
||||
* @author Rosemoe
|
||||
*/
|
||||
public class SymbolChannel {
|
||||
|
||||
private CodeEditor mEditor;
|
||||
|
||||
private final List<Character> pairs;
|
||||
|
||||
protected SymbolChannel(CodeEditor editor) {
|
||||
mEditor = editor;
|
||||
pairs = new ArrayList<>();
|
||||
pairs.add('}');
|
||||
pairs.add(')');
|
||||
pairs.add(']');
|
||||
pairs.add('"');
|
||||
pairs.add('\'');
|
||||
pairs.add('>');
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the given text in the editor.
|
||||
* <p>
|
||||
* This method allows you to insert texts externally to the content of editor.
|
||||
* The content of {@param symbolText} is not checked to be exactly characters of symbols.
|
||||
*
|
||||
* @param symbolText Text to insert, usually a text of symbols
|
||||
* @param selectionOffset New selection position relative to the start of text to insert.
|
||||
* Ranging from 0 to symbolText.length()
|
||||
*/
|
||||
public void insertSymbol(String text, int selectionOffset) {
|
||||
if (selectionOffset < 0 || selectionOffset > text.length()) {
|
||||
return;
|
||||
}
|
||||
Cursor cur = mEditor.getText().getCursor();
|
||||
if (cur.isSelected()) {
|
||||
cur.onDeleteKeyPressed();
|
||||
mEditor.notifyExternalCursorChange();
|
||||
}
|
||||
|
||||
if(cur.getLeftColumn() < mEditor.getText().getColumnCount(cur.getLeftLine())
|
||||
&& text.length() == 1
|
||||
&& text.charAt(0) == mEditor.getText().charAt(cur.getLeftLine(), cur.getLeftColumn())
|
||||
&& pairs.contains(text.charAt(0))) {
|
||||
mEditor.moveSelectionRight();
|
||||
} else {
|
||||
cur.onCommitText(text);
|
||||
if (selectionOffset != text.length()) {
|
||||
mEditor.setSelection(cur.getRightLine(), cur.getRightColumn() - (text.length() - selectionOffset));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 Rosemoe
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.github.rosemoe.editor.widget;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Define symbol pairs to complete them automatically when the user
|
||||
* enters the first character of pair.
|
||||
*
|
||||
* @author Rosemoe
|
||||
*/
|
||||
public class SymbolPairMatch {
|
||||
|
||||
private Map<Character, Replacement> pairMaps = new HashMap<>();
|
||||
|
||||
private SymbolPairMatch parent;
|
||||
|
||||
public SymbolPairMatch() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public SymbolPairMatch(SymbolPairMatch parent) {
|
||||
setParent(parent);
|
||||
}
|
||||
|
||||
protected void setParent(SymbolPairMatch parent) {
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Put a pair of symbol completion
|
||||
* When the user types the {@param firstCharacter}, it will be replaced by {@param replacement}
|
||||
* Replacement maybe null to disable completion for this character.
|
||||
*
|
||||
* @see Replacement
|
||||
*/
|
||||
public void putPair(char firstCharacter, Replacement replacement) {
|
||||
pairMaps.put(firstCharacter, replacement);
|
||||
}
|
||||
|
||||
public final Replacement getCompletion(char character) {
|
||||
Replacement result = parent != null ? parent.getCompletion(character) : null;
|
||||
if (result == null) {
|
||||
result = pairMaps.get(character);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public void removeAllRules() {
|
||||
pairMaps.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines a replacement of input
|
||||
*/
|
||||
public static class Replacement {
|
||||
|
||||
/**
|
||||
* Defines that this character does not have to be replaced
|
||||
*/
|
||||
public final static Replacement NO_REPLACEMENT = new Replacement("", 0);
|
||||
|
||||
public final String text;
|
||||
|
||||
public final int selection;
|
||||
|
||||
/**
|
||||
* The entered character will be replaced to {@param text} and
|
||||
* the new cursor position will be {@param selection}
|
||||
* The value of {@param selection} maybe 0 to {@param text}.length()
|
||||
*/
|
||||
public Replacement(String text, int selection) {
|
||||
this.selection = selection;
|
||||
this.text = text;
|
||||
if (selection < 0 || selection > text.length()) {
|
||||
throw new IllegalArgumentException("invalid selection value");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public final static class DefaultSymbolPairs extends SymbolPairMatch {
|
||||
public DefaultSymbolPairs() {
|
||||
super.putPair('{', new Replacement("{}", 1));
|
||||
super.putPair('(', new Replacement("()", 1));
|
||||
super.putPair('[', new Replacement("[]", 1));
|
||||
super.putPair('"', new Replacement("\"\"", 1));
|
||||
super.putPair('\'', new Replacement("''", 1));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 Rosemoe
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.github.rosemoe.editor.widget;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.drawable.GradientDrawable;
|
||||
import android.os.Build;
|
||||
import android.util.TypedValue;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
import com.google.android.material.button.MaterialButton;
|
||||
import com.itsaky.androidide.R;
|
||||
|
||||
public class TextActionPopupWindow extends TextComposeBasePopup implements View.OnClickListener, CodeEditor.EditorTextActionPresenter {
|
||||
private final CodeEditor mEditor;
|
||||
private final MaterialButton mPasteBtn;
|
||||
private final MaterialButton mSelectAll;
|
||||
private final LinearLayout mContainer;
|
||||
|
||||
private float mDpUnit = 0f;
|
||||
|
||||
/**
|
||||
* Create a panel for editor
|
||||
*
|
||||
* @param editor Target editor
|
||||
*/
|
||||
public TextActionPopupWindow(CodeEditor editor) {
|
||||
super(editor);
|
||||
mEditor = editor;
|
||||
mDpUnit = mEditor.getDpUnit();
|
||||
int popupHeightInDp = 60;
|
||||
popHeightPx = (int) (popupHeightInDp * mDpUnit);
|
||||
|
||||
// Since popup window does provide decor view, we have to pass null to this method
|
||||
@SuppressLint("InflateParams")
|
||||
View root = LayoutInflater.from(editor.getContext()).inflate(R.layout.text_compose_popup_window, null);
|
||||
mSelectAll = root.findViewById(R.id.tcpw_material_button_select_all);
|
||||
mContainer = root.findViewById(R.id.text_compose_panel);
|
||||
MaterialButton cut = root.findViewById(R.id.tcpw_material_button_cut);
|
||||
MaterialButton copy = root.findViewById(R.id.tcpw_material_button_copy);
|
||||
mPasteBtn = root.findViewById(R.id.tcpw_material_button_paste);
|
||||
mSelectAll.setOnClickListener(this);
|
||||
cut.setOnClickListener(this);
|
||||
copy.setOnClickListener(this);
|
||||
mPasteBtn.setOnClickListener(this);
|
||||
GradientDrawable gd = new GradientDrawable();
|
||||
gd.setCornerRadius(mDpUnit * 8);
|
||||
gd.setStroke(1, 0xff808080);
|
||||
gd.setColor(0xffffffff);
|
||||
root.setBackground(gd);
|
||||
setContentView(root);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void show() {
|
||||
setElevation(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2f, Resources.getSystem().getDisplayMetrics()));
|
||||
super.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
int id = view.getId();
|
||||
if (id == R.id.tcpw_material_button_select_all) {
|
||||
mEditor.selectAll();
|
||||
} else if (id == R.id.tcpw_material_button_cut) {
|
||||
mEditor.copyText();
|
||||
if (mEditor.getCursor().isSelected()) {
|
||||
mEditor.getCursor().onDeleteKeyPressed();
|
||||
}
|
||||
} else if (id == R.id.tcpw_material_button_paste) {
|
||||
mEditor.pasteText();
|
||||
mEditor.setSelection(mEditor.getCursor().getRightLine(), mEditor.getCursor().getRightColumn());
|
||||
} else if (id == R.id.tcpw_material_button_copy) {
|
||||
mEditor.copyText();
|
||||
mEditor.setSelection(mEditor.getCursor().getRightLine(), mEditor.getCursor().getRightColumn());
|
||||
}
|
||||
hide(DISMISS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectedTextClicked(MotionEvent event) {
|
||||
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@Override
|
||||
public void onUpdate() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpdate(int updateReason) {
|
||||
hide(updateReason);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBeginTextSelect() {
|
||||
setHeight((int) (LinearLayout.LayoutParams.WRAP_CONTENT));
|
||||
setWidth((int) (LinearLayout.LayoutParams.WRAP_CONTENT));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onExit() {
|
||||
boolean result = isShowing();
|
||||
hide(DISMISS);
|
||||
if (mEditor.getCursor().isSelected()) {
|
||||
mEditor.setSelection(mEditor.getCursor().getLeftLine(), mEditor.getCursor().getLeftColumn());
|
||||
}
|
||||
return result && !isShowing();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldShowCursor() {
|
||||
// this will show handler along with popup
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextSelectionEnd() {
|
||||
TextActionPopupWindow panel = this;
|
||||
if (panel.isShowing()) {
|
||||
panel.hide(DISMISS);
|
||||
} else {
|
||||
int first = mEditor.getFirstVisibleRow();
|
||||
int last = mEditor.getLastVisibleRow();
|
||||
int left = mEditor.getCursor().getLeftLine();
|
||||
int right = mEditor.getCursor().getRightLine();
|
||||
int toLineBottom;
|
||||
if (right <= first) {
|
||||
toLineBottom = first;
|
||||
} else if (right > last) {
|
||||
if (left <= first) {
|
||||
toLineBottom = (first + last) / 2;
|
||||
} else if (left >= last) {
|
||||
toLineBottom = last - 2;
|
||||
} else {
|
||||
if (left + 3 >= last) {
|
||||
toLineBottom = left - 2;
|
||||
} else {
|
||||
toLineBottom = left + 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (left <= first) {
|
||||
if (right + 3 >= last) {
|
||||
toLineBottom = right - 2;
|
||||
} else {
|
||||
toLineBottom = right + 1;
|
||||
}
|
||||
} else {
|
||||
if (left + 5 >= right) {
|
||||
toLineBottom = right + 1;
|
||||
} else {
|
||||
toLineBottom = (left + right) / 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
toLineBottom = Math.max(0, toLineBottom);
|
||||
int panelY = mEditor.getRowBottom(toLineBottom) - mEditor.getOffsetY();
|
||||
float handleLeftX = mEditor.getOffset(left, mEditor.getCursor().getLeftColumn());
|
||||
float handleRightX = mEditor.getOffset(right, mEditor.getCursor().getRightColumn());
|
||||
int panelX = (int) ((handleLeftX + handleRightX) / 2f);
|
||||
panel.setExtendedX(mDpUnit * 28);
|
||||
panel.setExtendedY(panelY);
|
||||
panel.show();
|
||||
mContainer.requestFocus();
|
||||
//mSelectAll.clearFocus();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 Rosemoe
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.github.rosemoe.editor.widget;
|
||||
|
||||
import android.graphics.RectF;
|
||||
import android.view.Gravity;
|
||||
import android.widget.PopupWindow;
|
||||
|
||||
/**
|
||||
* Editor base panel class
|
||||
*
|
||||
* @author Rose
|
||||
*/
|
||||
class TextComposeBasePopup extends PopupWindow {
|
||||
public static int DISMISS = 0;
|
||||
public static int DRAG = 1;
|
||||
public static int SCROLL = 2;
|
||||
private final float textSizePx;
|
||||
private CodeEditor mEditor;
|
||||
private int[] mLocation;
|
||||
private int mTop;
|
||||
private int mMaximumTop;
|
||||
private int mLeft;
|
||||
protected int popHeightPx;
|
||||
private int hideType = -1;
|
||||
|
||||
/**
|
||||
* Create a panel for editor
|
||||
*
|
||||
* @param editor Target editor
|
||||
*/
|
||||
public TextComposeBasePopup(CodeEditor editor) {
|
||||
if (editor == null) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
mLocation = new int[2];
|
||||
mEditor = editor;
|
||||
super.setTouchable(true);
|
||||
textSizePx = mEditor.getTextSizePx();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the left position on the editor rect
|
||||
*
|
||||
* @param x X on editor
|
||||
*/
|
||||
public void setExtendedX(float x) {
|
||||
mLeft = (int) x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the top position on the editor rect
|
||||
*
|
||||
* @param y Y on editor
|
||||
*/
|
||||
public void setExtendedY(float y) {
|
||||
mMaximumTop = (int) y;
|
||||
}
|
||||
|
||||
private final RectF selectionRect = new RectF();
|
||||
|
||||
|
||||
/**
|
||||
* Show the panel or update its position(If already shown)
|
||||
*/
|
||||
public void show() {
|
||||
int width = mEditor.getWidth();
|
||||
RectF leftHandleRect = mEditor.getLeftHandleRect();
|
||||
RectF rightHandleRect = mEditor.getRightHandleRect();
|
||||
|
||||
// when right handle goes below visible area, it rect becomes empty. so this feature (or bug) used to calculate popup location
|
||||
// if we can not use this,
|
||||
// alternative method can be implemented using mMaximumTop
|
||||
// @TODO implement a proper way to calculate popup position
|
||||
if (rightHandleRect.isEmpty()) {
|
||||
rightHandleRect.top = mMaximumTop;
|
||||
rightHandleRect.left = width;
|
||||
rightHandleRect.bottom = mMaximumTop;
|
||||
rightHandleRect.right = width;
|
||||
}
|
||||
|
||||
float handleHeight = leftHandleRect.height();
|
||||
selectionRect.top = Math.min(leftHandleRect.top, rightHandleRect.top);
|
||||
selectionRect.bottom = Math.max(leftHandleRect.bottom, rightHandleRect.bottom);
|
||||
selectionRect.left = Math.min(leftHandleRect.left, rightHandleRect.left);
|
||||
selectionRect.right = Math.max(leftHandleRect.right, rightHandleRect.right);
|
||||
|
||||
// prevent drawing popup over the keyboard
|
||||
/*if (selectionRect.bottom > mMaximumTop - popHeightPx) {
|
||||
selectionRect.bottom -= popHeightPx;
|
||||
}*/
|
||||
|
||||
if (mLeft > width - getWidth()) {
|
||||
mLeft = width - getWidth();
|
||||
}
|
||||
int height = mEditor.getHeight();
|
||||
if (mTop > height - getHeight()) {
|
||||
mTop = height - getHeight();
|
||||
}
|
||||
if (mTop < 0) {
|
||||
mTop = 0;
|
||||
}
|
||||
if (mLeft < 0) {
|
||||
mLeft = 0;
|
||||
}
|
||||
mEditor.getLocationInWindow(mLocation);
|
||||
boolean topCovered = mLocation[1] > selectionRect.top - textSizePx - popHeightPx - handleHeight;
|
||||
|
||||
if (topCovered) {
|
||||
mTop = (int) (selectionRect.bottom + (handleHeight));
|
||||
} else {
|
||||
mTop = (int) (selectionRect.top - textSizePx - popHeightPx - handleHeight);
|
||||
}
|
||||
if (isShowing()) {
|
||||
update(mLocation[0] + mLeft, mLocation[1] + mTop, getWidth(), getHeight());
|
||||
return;
|
||||
}
|
||||
super.showAtLocation(mEditor,
|
||||
Gravity.START | Gravity.TOP,
|
||||
mLocation[0] + mLeft, mLocation[1] + mTop);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the panel (If shown)
|
||||
*/
|
||||
public void hide(int type) {
|
||||
if (isShowing()) {
|
||||
hideType = type;
|
||||
if ((hideType == DRAG || hideType == SCROLL) && mEditor.getEventHandler() != null) {
|
||||
mEditor.getEventHandler().notifyGestureInteractionEnd(type);
|
||||
}
|
||||
super.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.drawerlayout.widget.DrawerLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="match_parent"
|
||||
android:id="@+id/editor_drawerLayout"
|
||||
@@ -69,22 +67,6 @@
|
||||
<include
|
||||
android:id="@+id/bottom_sheet"
|
||||
layout="@layout/layout_editor_bottom_sheet"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/symbolText"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="wrap_content"
|
||||
android:textColor="@color/primaryTextColor"
|
||||
android:textSize="14sp"
|
||||
android:singleLine="true"
|
||||
android:background="@color/content_background"
|
||||
android:clickable="false"
|
||||
android:focusable="false"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="4dp"
|
||||
tools:ignore="MissingConstraints"/>
|
||||
|
||||
<include
|
||||
android:id="@+id/diagnosticInfo"
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
~ along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<io.github.rosemoe.editor.widget.CodeEditor
|
||||
<com.itsaky.androidide.views.editor.IDEEditor
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/editor"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.itsaky.androidide.views.Editor
|
||||
<com.itsaky.androidide.views.editor.IDEEditor
|
||||
android:id="@+id/editor"
|
||||
android:layout_height="0dp"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.itsaky.androidide.utils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.charset.CharacterCodingException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.CharsetEncoder;
|
||||
import java.nio.charset.CoderResult;
|
||||
import java.nio.charset.CodingErrorAction;
|
||||
import java.nio.charset.UnsupportedCharsetException;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Implements an {@link InputStream} to read from String, StringBuffer, StringBuilder or CharBuffer.
|
||||
* <p>
|
||||
* <strong>Note:</strong> Supports {@link #mark(int)} and {@link #reset()}.
|
||||
* </p>
|
||||
*
|
||||
* @since 2.2
|
||||
*/
|
||||
public class CharSequenceInputStream extends InputStream {
|
||||
|
||||
private static final int BUFFER_SIZE = 2048;
|
||||
|
||||
private static final int NO_MARK = -1;
|
||||
|
||||
private final CharsetEncoder charsetEncoder;
|
||||
private final CharBuffer cBuf;
|
||||
private final ByteBuffer bBuf;
|
||||
|
||||
private int cBufMark; // position in cBuf
|
||||
private int bBufMark; // position in bBuf
|
||||
|
||||
/**
|
||||
* Constructs a new instance with a buffer size of 2048.
|
||||
*
|
||||
* @param cs the input character sequence.
|
||||
* @param charset the character set name to use.
|
||||
* @throws IllegalArgumentException if the buffer is not large enough to hold a complete character.
|
||||
*/
|
||||
public CharSequenceInputStream(final CharSequence cs, final Charset charset) {
|
||||
this(cs, charset, BUFFER_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*
|
||||
* @param cs the input character sequence.
|
||||
* @param charset the character set name to use, null maps to the default Charset.
|
||||
* @param bufferSize the buffer size to use.
|
||||
* @throws IllegalArgumentException if the buffer is not large enough to hold a complete character.
|
||||
*/
|
||||
public CharSequenceInputStream(final CharSequence cs, final Charset charset, final int bufferSize) {
|
||||
// @formatter:off
|
||||
this.charsetEncoder = toCharset(charset).newEncoder()
|
||||
.onMalformedInput(CodingErrorAction.REPLACE)
|
||||
.onUnmappableCharacter(CodingErrorAction.REPLACE);
|
||||
// @formatter:on
|
||||
// Ensure that buffer is long enough to hold a complete character
|
||||
this.bBuf = ByteBuffer.allocate(ReaderInputStream.checkMinBufferSize(charsetEncoder, bufferSize));
|
||||
this.bBuf.flip();
|
||||
this.cBuf = CharBuffer.wrap(cs);
|
||||
this.cBufMark = NO_MARK;
|
||||
this.bBufMark = NO_MARK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new instance with a buffer size of 2048.
|
||||
*
|
||||
* @param cs the input character sequence.
|
||||
* @param charset the character set name to use.
|
||||
* @throws IllegalArgumentException if the buffer is not large enough to hold a complete character.
|
||||
*/
|
||||
public CharSequenceInputStream(final CharSequence cs, final String charset) {
|
||||
this(cs, charset, BUFFER_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*
|
||||
* @param cs the input character sequence.
|
||||
* @param charset the character set name to use, null maps to the default Charset.
|
||||
* @param bufferSize the buffer size to use.
|
||||
* @throws IllegalArgumentException if the buffer is not large enough to hold a complete character.
|
||||
*/
|
||||
public CharSequenceInputStream(final CharSequence cs, final String charset, final int bufferSize) {
|
||||
this(cs, toCharset(charset), bufferSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an estimate of the number of bytes remaining in the byte stream.
|
||||
* @return the count of bytes that can be read without blocking (or returning EOF).
|
||||
*
|
||||
* @throws IOException if an error occurs (probably not possible).
|
||||
*/
|
||||
@Override
|
||||
public int available() throws IOException {
|
||||
// The cached entries are in bbuf; since encoding always creates at least one byte
|
||||
// per character, we can add the two to get a better estimate (e.g. if bbuf is empty)
|
||||
// Note that the previous implementation (2.4) could return zero even though there were
|
||||
// encoded bytes still available.
|
||||
return this.bBuf.remaining() + this.cBuf.remaining();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
// noop
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills the byte output buffer from the input char buffer.
|
||||
*
|
||||
* @throws CharacterCodingException
|
||||
* an error encoding data.
|
||||
*/
|
||||
private void fillBuffer() throws CharacterCodingException {
|
||||
this.bBuf.compact();
|
||||
final CoderResult result = this.charsetEncoder.encode(this.cBuf, this.bBuf, true);
|
||||
if (result.isError()) {
|
||||
result.throwException();
|
||||
}
|
||||
this.bBuf.flip();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the CharsetEncoder.
|
||||
*
|
||||
* @return the CharsetEncoder.
|
||||
*/
|
||||
CharsetEncoder getCharsetEncoder() {
|
||||
return charsetEncoder;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
* @param readlimit max read limit (ignored).
|
||||
*/
|
||||
@Override
|
||||
public synchronized void mark(final int readlimit) {
|
||||
this.cBufMark = this.cBuf.position();
|
||||
this.bBufMark = this.bBuf.position();
|
||||
this.cBuf.mark();
|
||||
this.bBuf.mark();
|
||||
// It would be nice to be able to use mark & reset on the cbuf and bbuf;
|
||||
// however the bbuf is re-used so that won't work
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean markSupported() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
for (;;) {
|
||||
if (this.bBuf.hasRemaining()) {
|
||||
return this.bBuf.get() & 0xFF;
|
||||
}
|
||||
fillBuffer();
|
||||
if (!this.bBuf.hasRemaining() && !this.cBuf.hasRemaining()) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(final byte[] b) throws IOException {
|
||||
return read(b, 0, b.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(final byte[] array, int off, int len) throws IOException {
|
||||
Objects.requireNonNull(array, "array");
|
||||
if (len < 0 || (off + len) > array.length) {
|
||||
throw new IndexOutOfBoundsException("Array Size=" + array.length + ", offset=" + off + ", length=" + len);
|
||||
}
|
||||
if (len == 0) {
|
||||
return 0; // must return 0 for zero length read
|
||||
}
|
||||
if (!this.bBuf.hasRemaining() && !this.cBuf.hasRemaining()) {
|
||||
return -1;
|
||||
}
|
||||
int bytesRead = 0;
|
||||
while (len > 0) {
|
||||
if (this.bBuf.hasRemaining()) {
|
||||
final int chunk = Math.min(this.bBuf.remaining(), len);
|
||||
this.bBuf.get(array, off, chunk);
|
||||
off += chunk;
|
||||
len -= chunk;
|
||||
bytesRead += chunk;
|
||||
} else {
|
||||
fillBuffer();
|
||||
if (!this.bBuf.hasRemaining() && !this.cBuf.hasRemaining()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return bytesRead == 0 && !this.cBuf.hasRemaining() ? -1 : bytesRead;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void reset() throws IOException {
|
||||
//
|
||||
// This is not the most efficient implementation, as it re-encodes from the beginning.
|
||||
//
|
||||
// Since the bbuf is re-used, in general it's necessary to re-encode the data.
|
||||
//
|
||||
// It should be possible to apply some optimisations however:
|
||||
// + use mark/reset on the cbuf and bbuf. This would only work if the buffer had not been (re)filled since
|
||||
// the mark. The code would have to catch InvalidMarkException - does not seem possible to check if mark is
|
||||
// valid otherwise. + Try saving the state of the cbuf before each fillBuffer; it might be possible to
|
||||
// restart from there.
|
||||
//
|
||||
if (this.cBufMark != NO_MARK) {
|
||||
// if cbuf is at 0, we have not started reading anything, so skip re-encoding
|
||||
if (this.cBuf.position() != 0) {
|
||||
this.charsetEncoder.reset();
|
||||
this.cBuf.rewind();
|
||||
this.bBuf.rewind();
|
||||
this.bBuf.limit(0); // rewind does not clear the buffer
|
||||
while(this.cBuf.position() < this.cBufMark) {
|
||||
this.bBuf.rewind(); // empty the buffer (we only refill when empty during normal processing)
|
||||
this.bBuf.limit(0);
|
||||
fillBuffer();
|
||||
}
|
||||
}
|
||||
if (this.cBuf.position() != this.cBufMark) {
|
||||
throw new IllegalStateException("Unexpected CharBuffer position: actual=" + cBuf.position() + " " +
|
||||
"expected=" + this.cBufMark);
|
||||
}
|
||||
this.bBuf.position(this.bBufMark);
|
||||
this.cBufMark = NO_MARK;
|
||||
this.bBufMark = NO_MARK;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long n) throws IOException {
|
||||
//
|
||||
// This could be made more efficient by using position to skip within the current buffer.
|
||||
//
|
||||
long skipped = 0;
|
||||
while (n > 0 && available() > 0) {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
this.read();
|
||||
n--;
|
||||
skipped++;
|
||||
}
|
||||
return skipped;
|
||||
}
|
||||
|
||||
static Charset toCharset(final Charset charset) {
|
||||
return charset == null ? Charset.defaultCharset() : charset;
|
||||
}
|
||||
|
||||
static Charset toCharset(final String charsetName) throws UnsupportedCharsetException {
|
||||
return charsetName == null ? Charset.defaultCharset() : Charset.forName(charsetName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.itsaky.androidide.utils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.Reader;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.CharsetEncoder;
|
||||
import java.nio.charset.CoderResult;
|
||||
import java.nio.charset.CodingErrorAction;
|
||||
import java.nio.charset.UnsupportedCharsetException;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* {@link InputStream} implementation that reads a character stream from a {@link Reader} and transforms it to a byte
|
||||
* stream using a specified charset encoding. The stream is transformed using a {@link CharsetEncoder} object,
|
||||
* guaranteeing that all charset encodings supported by the JRE are handled correctly. In particular for charsets such
|
||||
* as UTF-16, the implementation ensures that one and only one byte order marker is produced.
|
||||
* <p>
|
||||
* Since in general it is not possible to predict the number of characters to be read from the {@link Reader} to satisfy
|
||||
* a read request on the {@link ReaderInputStream}, all reads from the {@link Reader} are buffered. There is therefore
|
||||
* no well defined correlation between the current position of the {@link Reader} and that of the
|
||||
* {@link ReaderInputStream}. This also implies that in general there is no need to wrap the underlying {@link Reader}
|
||||
* in a {@link java.io.BufferedReader}.
|
||||
* </p>
|
||||
* <p>
|
||||
* {@link ReaderInputStream} implements the inverse transformation of {@link java.io.InputStreamReader}; in the
|
||||
* following example, reading from {@code in2} would return the same byte sequence as reading from {@code in} (provided
|
||||
* that the initial byte sequence is legal with respect to the charset encoding):
|
||||
* </p>
|
||||
*
|
||||
* <pre>
|
||||
* InputStream inputStream = ...
|
||||
* Charset cs = ...
|
||||
* InputStreamReader reader = new InputStreamReader(inputStream, cs);
|
||||
* ReaderInputStream in2 = new ReaderInputStream(reader, cs);
|
||||
* </pre>
|
||||
* <p>
|
||||
* {@link ReaderInputStream} implements the same transformation as {@link java.io.OutputStreamWriter}, except that the
|
||||
* control flow is reversed: both classes transform a character stream into a byte stream, but
|
||||
* {@link java.io.OutputStreamWriter} pushes data to the underlying stream, while {@link ReaderInputStream} pulls it
|
||||
* from the underlying stream.
|
||||
* </p>
|
||||
* <p>
|
||||
* Note that while there are use cases where there is no alternative to using this class, very often the need to use
|
||||
* this class is an indication of a flaw in the design of the code. This class is typically used in situations where an
|
||||
* existing API only accepts an {@link InputStream}, but where the most natural way to produce the data is as a
|
||||
* character stream, i.e. by providing a {@link Reader} instance. An example of a situation where this problem may
|
||||
* appear is when implementing the {@code javax.activation.DataSource} interface from the Java Activation Framework.
|
||||
* </p>
|
||||
* <p>
|
||||
* Given the fact that the {@link Reader} class doesn't provide any way to predict whether the next read operation will
|
||||
* block or not, it is not possible to provide a meaningful implementation of the {@link InputStream#available()}
|
||||
* method. A call to this method will always return 0. Also, this class doesn't support {@link InputStream#mark(int)}.
|
||||
* </p>
|
||||
* <p>
|
||||
* Instances of {@link ReaderInputStream} are not thread safe.
|
||||
* </p>
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
public class ReaderInputStream extends InputStream {
|
||||
private static final int DEFAULT_BUFFER_SIZE = 1024;
|
||||
|
||||
static int checkMinBufferSize(final CharsetEncoder charsetEncoder, final int bufferSize) {
|
||||
final float minRequired = minBufferSize(charsetEncoder);
|
||||
if (bufferSize < minRequired) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Buffer size %,d must be at least %s for a CharsetEncoder %s.", bufferSize, minRequired, charsetEncoder.charset().displayName()));
|
||||
}
|
||||
return bufferSize;
|
||||
}
|
||||
|
||||
static float minBufferSize(final CharsetEncoder charsetEncoder) {
|
||||
return charsetEncoder.maxBytesPerChar() * 2;
|
||||
}
|
||||
|
||||
private final Reader reader;
|
||||
|
||||
private final CharsetEncoder charsetEncoder;
|
||||
|
||||
/**
|
||||
* CharBuffer used as input for the decoder. It should be reasonably large as we read data from the underlying Reader
|
||||
* into this buffer.
|
||||
*/
|
||||
private final CharBuffer encoderIn;
|
||||
/**
|
||||
* ByteBuffer used as output for the decoder. This buffer can be small as it is only used to transfer data from the
|
||||
* decoder to the buffer provided by the caller.
|
||||
*/
|
||||
private final ByteBuffer encoderOut;
|
||||
|
||||
private CoderResult lastCoderResult;
|
||||
|
||||
private boolean endOfInput;
|
||||
|
||||
/**
|
||||
* Constructs a new {@link ReaderInputStream} that uses the default character encoding with a default input buffer size
|
||||
* of {@value #DEFAULT_BUFFER_SIZE} characters.
|
||||
*
|
||||
* @param reader the target {@link Reader}
|
||||
* @deprecated 2.5 use {@link #ReaderInputStream(Reader, Charset)} instead
|
||||
*/
|
||||
@Deprecated
|
||||
public ReaderInputStream(final Reader reader) {
|
||||
this(reader, Charset.defaultCharset());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@link ReaderInputStream} with a default input buffer size of {@value #DEFAULT_BUFFER_SIZE}
|
||||
* characters.
|
||||
*
|
||||
* @param reader the target {@link Reader}
|
||||
* @param charset the charset encoding
|
||||
*/
|
||||
public ReaderInputStream(final Reader reader, final Charset charset) {
|
||||
this(reader, charset, DEFAULT_BUFFER_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@link ReaderInputStream}.
|
||||
*
|
||||
* @param reader the target {@link Reader}.
|
||||
* @param charset the charset encoding.
|
||||
* @param bufferSize the size of the input buffer in number of characters.
|
||||
*/
|
||||
public ReaderInputStream(final Reader reader, final Charset charset, final int bufferSize) {
|
||||
// @formatter:off
|
||||
this(reader,
|
||||
toCharset(charset).newEncoder()
|
||||
.onMalformedInput(CodingErrorAction.REPLACE)
|
||||
.onUnmappableCharacter(CodingErrorAction.REPLACE),
|
||||
bufferSize);
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@link ReaderInputStream}.
|
||||
*
|
||||
* @param reader the target {@link Reader}
|
||||
* @param charsetEncoder the charset encoder
|
||||
* @since 2.1
|
||||
*/
|
||||
public ReaderInputStream(final Reader reader, final CharsetEncoder charsetEncoder) {
|
||||
this(reader, charsetEncoder, DEFAULT_BUFFER_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@link ReaderInputStream}.
|
||||
*
|
||||
* @param reader the target {@link Reader}
|
||||
* @param charsetEncoder the charset encoder, null defauls to the default Charset encoder.
|
||||
* @param bufferSize the size of the input buffer in number of characters
|
||||
* @since 2.1
|
||||
*/
|
||||
public ReaderInputStream(final Reader reader, final CharsetEncoder charsetEncoder, final int bufferSize) {
|
||||
this.reader = reader;
|
||||
this.charsetEncoder = toCharsetEncoder(charsetEncoder);
|
||||
this.encoderIn = CharBuffer.allocate(checkMinBufferSize(this.charsetEncoder, bufferSize));
|
||||
this.encoderIn.flip();
|
||||
this.encoderOut = ByteBuffer.allocate(128);
|
||||
this.encoderOut.flip();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@link ReaderInputStream} with a default input buffer size of {@value #DEFAULT_BUFFER_SIZE}
|
||||
* characters.
|
||||
*
|
||||
* @param reader the target {@link Reader}
|
||||
* @param charsetName the name of the charset encoding
|
||||
*/
|
||||
public ReaderInputStream(final Reader reader, final String charsetName) {
|
||||
this(reader, charsetName, DEFAULT_BUFFER_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@link ReaderInputStream}.
|
||||
*
|
||||
* @param reader the target {@link Reader}
|
||||
* @param charsetName the name of the charset encoding, null maps to the default Charset.
|
||||
* @param bufferSize the size of the input buffer in number of characters
|
||||
*/
|
||||
public ReaderInputStream(final Reader reader, final String charsetName, final int bufferSize) {
|
||||
this(reader, toCharset(charsetName), bufferSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the stream. This method will cause the underlying {@link Reader} to be closed.
|
||||
*
|
||||
* @throws IOException if an I/O error occurs.
|
||||
*/
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
reader.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills the internal char buffer from the reader.
|
||||
*
|
||||
* @throws IOException If an I/O error occurs
|
||||
*/
|
||||
private void fillBuffer() throws IOException {
|
||||
if (!endOfInput && (lastCoderResult == null || lastCoderResult.isUnderflow())) {
|
||||
encoderIn.compact();
|
||||
final int position = encoderIn.position();
|
||||
// We don't use Reader#read(CharBuffer) here because it is more efficient
|
||||
// to write directly to the underlying char array (the default implementation
|
||||
// copies data to a temporary char array).
|
||||
final int c = reader.read(encoderIn.array(), position, encoderIn.remaining());
|
||||
if (c == -1) {
|
||||
endOfInput = true;
|
||||
} else {
|
||||
encoderIn.position(position + c);
|
||||
}
|
||||
encoderIn.flip();
|
||||
}
|
||||
encoderOut.compact();
|
||||
lastCoderResult = charsetEncoder.encode(encoderIn, encoderOut, endOfInput);
|
||||
if (endOfInput) {
|
||||
lastCoderResult = charsetEncoder.flush(encoderOut);
|
||||
}
|
||||
if (lastCoderResult.isError()) {
|
||||
lastCoderResult.throwException();
|
||||
}
|
||||
encoderOut.flip();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the CharsetEncoder.
|
||||
*
|
||||
* @return the CharsetEncoder.
|
||||
*/
|
||||
CharsetEncoder getCharsetEncoder() {
|
||||
return charsetEncoder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a single byte.
|
||||
*
|
||||
* @return either the byte read or {@code -1} if the end of the stream has been reached
|
||||
* @throws IOException if an I/O error occurs.
|
||||
*/
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
for (;;) {
|
||||
if (encoderOut.hasRemaining()) {
|
||||
return encoderOut.get() & 0xFF;
|
||||
}
|
||||
fillBuffer();
|
||||
if (endOfInput && !encoderOut.hasRemaining()) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the specified number of bytes into an array.
|
||||
*
|
||||
* @param b the byte array to read into
|
||||
* @return the number of bytes read or {@code -1} if the end of the stream has been reached
|
||||
* @throws IOException if an I/O error occurs.
|
||||
*/
|
||||
@Override
|
||||
public int read(final byte[] b) throws IOException {
|
||||
return read(b, 0, b.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the specified number of bytes into an array.
|
||||
*
|
||||
* @param array the byte array to read into
|
||||
* @param off the offset to start reading bytes into
|
||||
* @param len the number of bytes to read
|
||||
* @return the number of bytes read or {@code -1} if the end of the stream has been reached
|
||||
* @throws IOException if an I/O error occurs.
|
||||
*/
|
||||
@Override
|
||||
public int read(final byte[] array, int off, int len) throws IOException {
|
||||
Objects.requireNonNull(array, "array");
|
||||
if (len < 0 || off < 0 || (off + len) > array.length) {
|
||||
throw new IndexOutOfBoundsException("Array size=" + array.length + ", offset=" + off + ", length=" + len);
|
||||
}
|
||||
int read = 0;
|
||||
if (len == 0) {
|
||||
return 0; // Always return 0 if len == 0
|
||||
}
|
||||
while (len > 0) {
|
||||
if (encoderOut.hasRemaining()) { // Data from the last read not fully copied
|
||||
final int c = Math.min(encoderOut.remaining(), len);
|
||||
encoderOut.get(array, off, c);
|
||||
off += c;
|
||||
len -= c;
|
||||
read += c;
|
||||
} else if (endOfInput) { // Already reach EOF in the last read
|
||||
break;
|
||||
} else { // Read again
|
||||
fillBuffer();
|
||||
}
|
||||
}
|
||||
return read == 0 && endOfInput ? -1 : read;
|
||||
}
|
||||
|
||||
static Charset toCharset(final Charset charset) {
|
||||
return charset == null ? Charset.defaultCharset() : charset;
|
||||
}
|
||||
|
||||
static Charset toCharset(final String charsetName) throws UnsupportedCharsetException {
|
||||
return charsetName == null ? Charset.defaultCharset() : Charset.forName(charsetName);
|
||||
}
|
||||
|
||||
static CharsetEncoder toCharsetEncoder(CharsetEncoder charsetEncoder) {
|
||||
return charsetEncoder != null ? charsetEncoder : Charset.defaultCharset().newEncoder();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id 'com.android.library'
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion rootProject.ext.compileSdk
|
||||
buildToolsVersion rootProject.ext.buildTools
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion rootProject.ext.minSdk
|
||||
targetSdkVersion rootProject.ext.targetSdk
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility rootProject.ext.javaSourceVersion
|
||||
targetCompatibility rootProject.ext.javaTargetVersion
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation androidx.annotation
|
||||
}
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package io.github.rosemoe.sora;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ExampleInstrumentedTest {
|
||||
@Test
|
||||
public void useAppContext () {
|
||||
// Context of the app under test.
|
||||
Context appContext = InstrumentationRegistry.getInstrumentation ().getTargetContext ();
|
||||
assertEquals ("io.github.rosemoe.sora.test", appContext.getPackageName ());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
~ sora-editor - the awesome code editor for Android
|
||||
~ https://github.com/Rosemoe/sora-editor
|
||||
~ Copyright (C) 2020-2022 Rosemoe
|
||||
~
|
||||
~ This library is free software; you can redistribute it and/or
|
||||
~ modify it under the terms of the GNU Lesser General Public
|
||||
~ License as published by the Free Software Foundation; either
|
||||
~ version 2.1 of the License, or (at your option) any later version.
|
||||
~
|
||||
~ This library is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
~ Lesser General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU Lesser General Public
|
||||
~ License along with this library; if not, write to the Free Software
|
||||
~ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
|
||||
~ USA
|
||||
~
|
||||
~ Please contact Rosemoe by email 2073412493@qq.com if you need
|
||||
~ additional information or have any questions
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~-->
|
||||
|
||||
<manifest package="io.github.rosemoe.sora">
|
||||
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* sora-editor - the awesome code editor for Android
|
||||
* https://github.com/Rosemoe/sora-editor
|
||||
* Copyright (C) 2020-2022 Rosemoe
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
|
||||
* USA
|
||||
*
|
||||
* Please contact Rosemoe by email 2073412493@qq.com if you need
|
||||
* additional information or have any questions
|
||||
*/
|
||||
package io.github.rosemoe.sora.annotations;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* This annotation marks those fields, methods and constructors experimentally created.
|
||||
* <p>
|
||||
* Methods, fields and constructors with this annotation is very subject to keep or delete.
|
||||
* For that reason, they are not stable for production use.
|
||||
*/
|
||||
@Target({ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
public @interface Experimental {
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* sora-editor - the awesome code editor for Android
|
||||
* https://github.com/Rosemoe/sora-editor
|
||||
* Copyright (C) 2020-2022 Rosemoe
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
|
||||
* USA
|
||||
*
|
||||
* Please contact Rosemoe by email 2073412493@qq.com if you need
|
||||
* additional information or have any questions
|
||||
*/
|
||||
package io.github.rosemoe.sora.annotations;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Marks that this member is internally used and that it is not recommended using this
|
||||
* member at your side.
|
||||
*/
|
||||
@Retention(RetentionPolicy.CLASS)
|
||||
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.TYPE})
|
||||
public @interface UnsupportedUserUsage {
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* sora-editor - the awesome code editor for Android
|
||||
* https://github.com/Rosemoe/sora-editor
|
||||
* Copyright (C) 2020-2022 Rosemoe
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
|
||||
* USA
|
||||
*
|
||||
* Please contact Rosemoe by email 2073412493@qq.com if you need
|
||||
* additional information or have any questions
|
||||
*/
|
||||
package io.github.rosemoe.sora.data;
|
||||
|
||||
/**
|
||||
* Model for code navigation
|
||||
*
|
||||
* @author Rose
|
||||
*/
|
||||
@SuppressWarnings("CanBeFinal")
|
||||
public class NavigationItem {
|
||||
|
||||
/**
|
||||
* The line position
|
||||
*/
|
||||
public int line;
|
||||
|
||||
/**
|
||||
* The description
|
||||
*/
|
||||
public String label;
|
||||
|
||||
/**
|
||||
* Create a new navigation
|
||||
*
|
||||
* @param line The line position
|
||||
* @param label The description
|
||||
*/
|
||||
public NavigationItem(int line, String label) {
|
||||
this.line = line;
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* sora-editor - the awesome code editor for Android
|
||||
* https://github.com/Rosemoe/sora-editor
|
||||
* Copyright (C) 2020-2022 Rosemoe
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
|
||||
* USA
|
||||
*
|
||||
* Please contact Rosemoe by email 2073412493@qq.com if you need
|
||||
* additional information or have any questions
|
||||
*/
|
||||
package io.github.rosemoe.sora.data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import io.github.rosemoe.sora.lang.styling.CodeBlock;
|
||||
|
||||
/**
|
||||
* An object provider for speed improvement
|
||||
* Now meaningless because it is not as well as it expected
|
||||
*
|
||||
* @author Rose
|
||||
*/
|
||||
public class ObjectAllocator {
|
||||
|
||||
private static final int RECYCLE_LIMIT = 1024 * 8;
|
||||
private static List<CodeBlock> codeBlocks;
|
||||
private static List<CodeBlock> tempArray;
|
||||
|
||||
public static void recycleBlockLines(List<CodeBlock> src) {
|
||||
if (src == null) {
|
||||
return;
|
||||
}
|
||||
if (codeBlocks == null) {
|
||||
codeBlocks = src;
|
||||
return;
|
||||
}
|
||||
int size = codeBlocks.size();
|
||||
int sizeAnother = src.size();
|
||||
while (sizeAnother > 0 && size < RECYCLE_LIMIT) {
|
||||
size++;
|
||||
sizeAnother--;
|
||||
var obj = src.get(sizeAnother);
|
||||
obj.clear();
|
||||
codeBlocks.add(obj);
|
||||
}
|
||||
src.clear();
|
||||
synchronized (ObjectAllocator.class) {
|
||||
tempArray = src;
|
||||
}
|
||||
}
|
||||
|
||||
public static List<CodeBlock> obtainList() {
|
||||
List<CodeBlock> temp = null;
|
||||
synchronized (ObjectAllocator.class) {
|
||||
temp = tempArray;
|
||||
tempArray = null;
|
||||
}
|
||||
if (temp == null) {
|
||||
temp = new ArrayList<>(128);
|
||||
}
|
||||
return temp;
|
||||
}
|
||||
|
||||
public static CodeBlock obtainBlockLine() {
|
||||
return (codeBlocks == null || codeBlocks.isEmpty()) ? new CodeBlock() : codeBlocks.remove(codeBlocks.size() - 1);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* sora-editor - the awesome code editor for Android
|
||||
* https://github.com/Rosemoe/sora-editor
|
||||
* Copyright (C) 2020-2022 Rosemoe
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
|
||||
* USA
|
||||
*
|
||||
* Please contact Rosemoe by email 2073412493@qq.com if you need
|
||||
* additional information or have any questions
|
||||
*/
|
||||
package io.github.rosemoe.sora.event;
|
||||
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import io.github.rosemoe.sora.text.CharPosition;
|
||||
import io.github.rosemoe.sora.widget.CodeEditor;
|
||||
|
||||
/**
|
||||
* Report a single click
|
||||
*/
|
||||
public class ClickEvent extends Event {
|
||||
|
||||
private final CharPosition pos;
|
||||
private final MotionEvent event;
|
||||
|
||||
public ClickEvent(@NonNull CodeEditor editor, CharPosition position, MotionEvent event) {
|
||||
super(editor);
|
||||
this.pos = position;
|
||||
this.event = event;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canIntercept() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public int getLine() {
|
||||
return pos.line;
|
||||
}
|
||||
|
||||
public int getColumn() {
|
||||
return pos.column;
|
||||
}
|
||||
|
||||
public int getIndex() {
|
||||
return pos.index;
|
||||
}
|
||||
|
||||
public float getX() {
|
||||
return event.getX();
|
||||
}
|
||||
|
||||
public float getY() {
|
||||
return event.getY();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get original event object from Android framework
|
||||
*/
|
||||
public MotionEvent getCausingEvent() {
|
||||
return event;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* sora-editor - the awesome code editor for Android
|
||||
* https://github.com/Rosemoe/sora-editor
|
||||
* Copyright (C) 2020-2022 Rosemoe
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
|
||||
* USA
|
||||
*
|
||||
* Please contact Rosemoe by email 2073412493@qq.com if you need
|
||||
* additional information or have any questions
|
||||
*/
|
||||
package io.github.rosemoe.sora.event;
|
||||
|
||||
import io.github.rosemoe.sora.text.CharPosition;
|
||||
import io.github.rosemoe.sora.widget.CodeEditor;
|
||||
|
||||
/**
|
||||
* This event happens when {@link CodeEditor#setText(CharSequence)} is called or
|
||||
* user edited the displaying content.
|
||||
*
|
||||
* Note that you should not update the content at this time. Otherwise, there might be some
|
||||
* exceptions causing the editor framework to crash. If you do need to update the content, you should
|
||||
* post your actions to the main thread so that the user's modification will be successful.
|
||||
*/
|
||||
public class ContentChangeEvent extends Event {
|
||||
|
||||
/**
|
||||
* Notify that {@link CodeEditor#setText(CharSequence)} is called
|
||||
*/
|
||||
public final static int ACTION_SET_NEW_TEXT = 1;
|
||||
|
||||
/**
|
||||
* Notify that user inserted some texts to the content
|
||||
*/
|
||||
public final static int ACTION_INSERT = 2;
|
||||
|
||||
/**
|
||||
* Notify that user deleted some texts in the content
|
||||
*/
|
||||
public final static int ACTION_DELETE = 3;
|
||||
|
||||
private final int mAction;
|
||||
private final CharPosition mStart;
|
||||
private final CharPosition mEnd;
|
||||
private final CharSequence mTextChanged;
|
||||
|
||||
public ContentChangeEvent(CodeEditor editor, int action, CharPosition changeStart, CharPosition changeEnd, CharSequence textChanged) {
|
||||
super(editor);
|
||||
mAction = action;
|
||||
mStart = changeStart;
|
||||
mEnd = changeEnd;
|
||||
mTextChanged = textChanged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get action code of the event.
|
||||
* @see #ACTION_SET_NEW_TEXT
|
||||
* @see #ACTION_INSERT
|
||||
* @see #ACTION_DELETE
|
||||
*/
|
||||
public int getAction() {
|
||||
return mAction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the CharPosition indicating the start of changed region.
|
||||
*
|
||||
* Note that you can not modify the values in the returned instance.
|
||||
*/
|
||||
public CharPosition getChangeStart() {
|
||||
return mStart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the CharPosition indicating the end of changed region.
|
||||
*
|
||||
* Note that you can not modify the values in the returned instance.
|
||||
*/
|
||||
public CharPosition getChangeEnd() {
|
||||
return mEnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the changed text in this modification.
|
||||
* If action is {@link #ACTION_SET_NEW_TEXT}, Content instance is returned.
|
||||
* If action is {@link #ACTION_INSERT}, inserted text is the result.
|
||||
* If action is {@link #ACTION_DELETE}, deleted text is the result.
|
||||
*/
|
||||
public CharSequence getChangedText() {
|
||||
return mTextChanged;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* sora-editor - the awesome code editor for Android
|
||||
* https://github.com/Rosemoe/sora-editor
|
||||
* Copyright (C) 2020-2022 Rosemoe
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
|
||||
* USA
|
||||
*
|
||||
* Please contact Rosemoe by email 2073412493@qq.com if you need
|
||||
* additional information or have any questions
|
||||
*/
|
||||
package io.github.rosemoe.sora.event;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import io.github.rosemoe.sora.widget.CodeEditor;
|
||||
|
||||
/**
|
||||
* An Event object describes an event of editor.
|
||||
* It includes several attributes such as time and the editor object.
|
||||
* Subclasses of Event will define their own fields or methods.
|
||||
*
|
||||
* @author Rosemoe
|
||||
*/
|
||||
public abstract class Event {
|
||||
|
||||
private final long mEventTime;
|
||||
private final CodeEditor mEditor;
|
||||
private boolean mIntercepted;
|
||||
|
||||
public Event(@NonNull CodeEditor editor) {
|
||||
this(editor, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
public Event(@NonNull CodeEditor editor, long eventTime) {
|
||||
mEditor = Objects.requireNonNull(editor);
|
||||
mEventTime = eventTime;
|
||||
mIntercepted = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get event time
|
||||
*/
|
||||
public long getEventTime() {
|
||||
return mEventTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the editor
|
||||
*/
|
||||
@NonNull
|
||||
public CodeEditor getEditor() {
|
||||
return mEditor;
|
||||
}
|
||||
|
||||
/*
|
||||
* Check whether this event can be intercepted (so that the event is not sent to other
|
||||
* receivers after being intercepted)
|
||||
*/
|
||||
public boolean canIntercept() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercept the event.
|
||||
*
|
||||
* Make sure {@link #canIntercept()} returns true. Otherwise, an {@link UnsupportedOperationException}
|
||||
* will be thrown.
|
||||
*/
|
||||
public void intercept() {
|
||||
if (!canIntercept()) {
|
||||
throw new UnsupportedOperationException("intercept() not supported");
|
||||
}
|
||||
mIntercepted = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether this event is intercepted
|
||||
*/
|
||||
public boolean isIntercepted() {
|
||||
return mIntercepted;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
* sora-editor - the awesome code editor for Android
|
||||
* https://github.com/Rosemoe/sora-editor
|
||||
* Copyright (C) 2020-2022 Rosemoe
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
|
||||
* USA
|
||||
*
|
||||
* Please contact Rosemoe by email 2073412493@qq.com if you need
|
||||
* additional information or have any questions
|
||||
*/
|
||||
package io.github.rosemoe.sora.event;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.locks.ReadWriteLock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
/**
|
||||
* This class manages event dispatching in editor.
|
||||
* Users can either register their event receivers here or dispatch event to
|
||||
* the receivers in this manager.
|
||||
*
|
||||
* There may be several EventManagers in one editor instance. For example, each plugin
|
||||
* will have it own EventManager and the editor also has a root event manager for external
|
||||
* listeners.
|
||||
*
|
||||
* Note that the event type must be exact. That's to say, you need to use a terminal class instead
|
||||
* of using its parent classes. For instance, if you register a receiver with the event type {@link Event},
|
||||
* no event will be sent to your receiver.
|
||||
*
|
||||
* @author Rosemoe
|
||||
*/
|
||||
public class EventManager {
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
private final Map<Class<?>, Receivers> receivers;
|
||||
private final ReadWriteLock lock;
|
||||
|
||||
public EventManager() {
|
||||
receivers = new HashMap<>();
|
||||
lock = new ReentrantReadWriteLock();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get receivers container of a given event type safely
|
||||
*/
|
||||
@NonNull
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T extends Event> Receivers<T> getReceivers(@NonNull Class<T> type) {
|
||||
lock.readLock().lock();
|
||||
Receivers<T> result;
|
||||
try {
|
||||
result = receivers.get(type);
|
||||
} finally {
|
||||
lock.readLock().unlock();
|
||||
}
|
||||
if (result == null) {
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
result = receivers.get(type);
|
||||
if (result == null) {
|
||||
result = new Receivers<>();
|
||||
receivers.put(type, result);
|
||||
}
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a receiver of the given event.
|
||||
*
|
||||
* @param eventType Event type to be received
|
||||
* @param receiver Receiver of event
|
||||
* @param <T> Event type
|
||||
*/
|
||||
public <T extends Event> void subscribeEvent(Class<T> eventType, EventReceiver<T> receiver) {
|
||||
var receivers = getReceivers(eventType);
|
||||
receivers.lock.writeLock().lock();
|
||||
try {
|
||||
var list = receivers.receivers;
|
||||
if (list.contains(receiver)) {
|
||||
throw new IllegalArgumentException("the receiver is already registered for this type");
|
||||
}
|
||||
list.add(receiver);
|
||||
} finally {
|
||||
receivers.lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the given event to its receivers registered in this manager.
|
||||
* @param event Event to dispatch
|
||||
* @param <T> Event type
|
||||
* @return Whether the event's intercept flag is set
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends Event> boolean dispatchEvent(T event) {
|
||||
// Safe cast
|
||||
var receivers = getReceivers((Class<T>)event.getClass());
|
||||
receivers.lock.readLock().lock();
|
||||
EventReceiver<T>[] receiverArr;
|
||||
int count;
|
||||
try {
|
||||
count = receivers.receivers.size();
|
||||
receiverArr = obtainBuffer(count);
|
||||
receivers.receivers.toArray(receiverArr);
|
||||
} finally {
|
||||
receivers.lock.readLock().unlock();
|
||||
}
|
||||
List<EventReceiver<T>> unsubscribedReceivers = null;
|
||||
try {
|
||||
Unsubscribe unsubscribe = new Unsubscribe();
|
||||
for (int i = 0;i < count && !event.isIntercepted();i++) {
|
||||
var receiver = receiverArr[i];
|
||||
receiver.onReceive(event, unsubscribe);
|
||||
if (unsubscribe.isUnsubscribed()) {
|
||||
if (unsubscribedReceivers == null) {
|
||||
unsubscribedReceivers = new LinkedList<>();
|
||||
}
|
||||
unsubscribedReceivers.add(receiver);
|
||||
}
|
||||
unsubscribe.reset();
|
||||
}
|
||||
} finally {
|
||||
if (unsubscribedReceivers != null) {
|
||||
receivers.lock.writeLock().lock();
|
||||
try {
|
||||
receivers.receivers.removeAll(unsubscribedReceivers);
|
||||
} finally {
|
||||
receivers.lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
recycleBuffer(receiverArr);
|
||||
}
|
||||
return event.isIntercepted();
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal class for saving receivers of each type
|
||||
* @param <T> Event type
|
||||
*/
|
||||
private static class Receivers<T extends Event> {
|
||||
|
||||
ReadWriteLock lock = new ReentrantReadWriteLock();
|
||||
|
||||
List<EventReceiver<T>> receivers = new ArrayList<>();
|
||||
|
||||
}
|
||||
|
||||
private final EventReceiver<?>[][] caches = new EventReceiver[10][];
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <V extends Event> EventReceiver<V>[] obtainBuffer(int size) {
|
||||
EventReceiver<V>[] res = null;
|
||||
synchronized (this) {
|
||||
for (int i = 0;i < caches.length;i++) {
|
||||
if (caches[i] != null && caches[i].length >= size) {
|
||||
res = (EventReceiver<V>[]) caches[i];
|
||||
caches[i] = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (res == null) {
|
||||
res = new EventReceiver[size];
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
private synchronized void recycleBuffer(EventReceiver<?>[] array) {
|
||||
if (array == null) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0;i < caches.length;i++) {
|
||||
if (caches[i] == null) {
|
||||
Arrays.fill(array, null);
|
||||
caches[i] = array;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* sora-editor - the awesome code editor for Android
|
||||
* https://github.com/Rosemoe/sora-editor
|
||||
* Copyright (C) 2020-2022 Rosemoe
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
|
||||
* USA
|
||||
*
|
||||
* Please contact Rosemoe by email 2073412493@qq.com if you need
|
||||
* additional information or have any questions
|
||||
*/
|
||||
package io.github.rosemoe.sora.event;
|
||||
|
||||
public interface EventReceiver<T extends Event> {
|
||||
|
||||
void onReceive(T event, Unsubscribe unsubscribe);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* sora-editor - the awesome code editor for Android
|
||||
* https://github.com/Rosemoe/sora-editor
|
||||
* Copyright (C) 2020-2022 Rosemoe
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
|
||||
* USA
|
||||
*
|
||||
* Please contact Rosemoe by email 2073412493@qq.com if you need
|
||||
* additional information or have any questions
|
||||
*/
|
||||
package io.github.rosemoe.sora.event;
|
||||
|
||||
import io.github.rosemoe.sora.widget.CodeEditor;
|
||||
|
||||
/**
|
||||
* Notifies a selection handle's touch state has changed
|
||||
*/
|
||||
public class HandleStateChangeEvent extends Event {
|
||||
|
||||
public final static int HANDLE_TYPE_INSERT = 0;
|
||||
public final static int HANDLE_TYPE_LEFT = 1;
|
||||
public final static int HANDLE_TYPE_RIGHT = 2;
|
||||
private final int mWhich;
|
||||
private final boolean isHeld;
|
||||
|
||||
public HandleStateChangeEvent(CodeEditor editor, int which, boolean heldState) {
|
||||
super(editor);
|
||||
mWhich = which;
|
||||
isHeld = heldState;
|
||||
}
|
||||
|
||||
public int getHandleType() {
|
||||
return mWhich;
|
||||
}
|
||||
|
||||
public boolean isHeld() {
|
||||
return isHeld;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* sora-editor - the awesome code editor for Android
|
||||
* https://github.com/Rosemoe/sora-editor
|
||||
* Copyright (C) 2020-2022 Rosemoe
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
|
||||
* USA
|
||||
*
|
||||
* Please contact Rosemoe by email 2073412493@qq.com if you need
|
||||
* additional information or have any questions
|
||||
*/
|
||||
package io.github.rosemoe.sora.event;
|
||||
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import io.github.rosemoe.sora.text.CharPosition;
|
||||
import io.github.rosemoe.sora.widget.CodeEditor;
|
||||
|
||||
/**
|
||||
* Long press event.
|
||||
*
|
||||
* This event can be intercepted so that the editor framework will do nothing (such as selecting a word). You can take over the
|
||||
* procedure. Note that after intercepting an event, it will not be sent to other listeners, either.
|
||||
*
|
||||
* @author Rosemoe
|
||||
*/
|
||||
public class LongPressEvent extends Event {
|
||||
|
||||
private final CharPosition pos;
|
||||
private final MotionEvent event;
|
||||
|
||||
public LongPressEvent(@NonNull CodeEditor editor, CharPosition position, MotionEvent event) {
|
||||
super(editor);
|
||||
this.pos = position;
|
||||
this.event = event;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canIntercept() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public int getLine() {
|
||||
return pos.line;
|
||||
}
|
||||
|
||||
public int getColumn() {
|
||||
return pos.column;
|
||||
}
|
||||
|
||||
public int getIndex() {
|
||||
return pos.index;
|
||||
}
|
||||
|
||||
public float getX() {
|
||||
return event.getX();
|
||||
}
|
||||
|
||||
public float getY() {
|
||||
return event.getY();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get original event object from Android framework
|
||||
*/
|
||||
public MotionEvent getCausingEvent() {
|
||||
return event;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* sora-editor - the awesome code editor for Android
|
||||
* https://github.com/Rosemoe/sora-editor
|
||||
* Copyright (C) 2020-2022 Rosemoe
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
|
||||
* USA
|
||||
*
|
||||
* Please contact Rosemoe by email 2073412493@qq.com if you need
|
||||
* additional information or have any questions
|
||||
*/
|
||||
package io.github.rosemoe.sora.event;
|
||||
|
||||
import io.github.rosemoe.sora.widget.CodeEditor;
|
||||
|
||||
/**
|
||||
* Reports a scroll in editor.
|
||||
* The scrolling action can either have run or be running when this event is generated and sent.
|
||||
*
|
||||
* The returned x,y positions are usually positive when over-scrolling is disabled. They represent
|
||||
* the left-top position's pixel in editor.
|
||||
*/
|
||||
public class ScrollEvent extends Event {
|
||||
|
||||
/**
|
||||
* Caused by thumb's exact movements
|
||||
*/
|
||||
public final static int CAUSE_USER_DRAG = 1;
|
||||
/**
|
||||
* Caused by fling after user's movements
|
||||
*/
|
||||
public final static int CAUSE_USER_FLING = 2;
|
||||
/**
|
||||
* Caused by calling {@link CodeEditor#ensurePositionVisible(int, int)}.
|
||||
* This can happen when this method is manually called or either the user edits the text
|
||||
*/
|
||||
public final static int CAUSE_MAKE_POSITION_VISIBLE = 3;
|
||||
/**
|
||||
* Caused by the user's thumb reaching the edge of editor viewport, which causes the editor to
|
||||
* scroll to move the selection to text currently outside the viewport.
|
||||
*/
|
||||
public final static int CAUSE_TEXT_SELECTING = 4;
|
||||
|
||||
public final static int CAUSE_SCALE_TEXT = 5;
|
||||
|
||||
private final int mStartX;
|
||||
private final int mStartY;
|
||||
private final int mEndX;
|
||||
private final int mEndY;
|
||||
private final int mCause;
|
||||
|
||||
public ScrollEvent(CodeEditor editor, int startX, int startY, int endX, int endY, int cause) {
|
||||
super(editor);
|
||||
mStartX = startX;
|
||||
mStartY = startY;
|
||||
mEndX = endX;
|
||||
mEndY = endY;
|
||||
mCause = cause;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the start x
|
||||
*/
|
||||
public int getStartX() {
|
||||
return mStartX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the start y
|
||||
*/
|
||||
public int getStartY() {
|
||||
return mStartY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get end x
|
||||
*/
|
||||
public int getEndX() {
|
||||
return mEndX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get end y
|
||||
*/
|
||||
public int getEndY() {
|
||||
return mEndY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cause of the scroll
|
||||
*/
|
||||
public int getCause() {
|
||||
return mCause;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* sora-editor - the awesome code editor for Android
|
||||
* https://github.com/Rosemoe/sora-editor
|
||||
* Copyright (C) 2020-2022 Rosemoe
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
|
||||
* USA
|
||||
*
|
||||
* Please contact Rosemoe by email 2073412493@qq.com if you need
|
||||
* additional information or have any questions
|
||||
*/
|
||||
package io.github.rosemoe.sora.event;
|
||||
|
||||
import io.github.rosemoe.sora.text.CharPosition;
|
||||
import io.github.rosemoe.sora.widget.CodeEditor;
|
||||
|
||||
/**
|
||||
* This event happens when text is edited by the user, or the user click the view to change the
|
||||
* position of selection. Even when the actual values of CharPosition are not changed, you may receive the event.
|
||||
*
|
||||
* Note that you should not change returned CharPosition objects because they are shared in an event
|
||||
* dispatch.
|
||||
*/
|
||||
public class SelectionChangeEvent extends Event {
|
||||
|
||||
private final CharPosition left;
|
||||
private final CharPosition right;
|
||||
private final int cause;
|
||||
/**
|
||||
* Unknown cause
|
||||
*/
|
||||
public final static int CAUSE_UNKNOWN = 0;
|
||||
|
||||
/**
|
||||
* Selection change caused by text modifications
|
||||
*/
|
||||
public final static int CAUSE_TEXT_MODIFICATION = 1;
|
||||
|
||||
/**
|
||||
* Set selection by handle
|
||||
*/
|
||||
public final static int CAUSE_SELECTION_HANDLE = 2;
|
||||
|
||||
/**
|
||||
* Set selection by single tap
|
||||
*/
|
||||
public final static int CAUSE_TAP = 3;
|
||||
|
||||
/**
|
||||
* Set selection because of {@link android.view.inputmethod.InputConnection#setSelection(int, int)}
|
||||
*/
|
||||
public final static int CAUSE_IME = 4;
|
||||
|
||||
/**
|
||||
* Long press
|
||||
*/
|
||||
public final static int CAUSE_LONG_PRESS = 5;
|
||||
|
||||
public SelectionChangeEvent(CodeEditor editor, int cause) {
|
||||
super(editor);
|
||||
var cursor = editor.getText().getCursor();
|
||||
left = cursor.left();
|
||||
right = cursor.right();
|
||||
this.cause = cause;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cause of the change
|
||||
* @see #CAUSE_UNKNOWN
|
||||
* @see #CAUSE_TEXT_MODIFICATION
|
||||
* @see #CAUSE_SELECTION_HANDLE
|
||||
* @see #CAUSE_TAP
|
||||
*/
|
||||
public int getCause() {
|
||||
return cause;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the left selection's position
|
||||
*/
|
||||
public CharPosition getLeft() {
|
||||
return left;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the right selection's position
|
||||
*/
|
||||
public CharPosition getRight() {
|
||||
return right;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether text is selected
|
||||
*/
|
||||
public boolean isSelected() {
|
||||
return left.index != right.index;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* sora-editor - the awesome code editor for Android
|
||||
* https://github.com/Rosemoe/sora-editor
|
||||
* Copyright (C) 2020-2022 Rosemoe
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
|
||||
* USA
|
||||
*
|
||||
* Please contact Rosemoe by email 2073412493@qq.com if you need
|
||||
* additional information or have any questions
|
||||
*/
|
||||
package io.github.rosemoe.sora.event;
|
||||
|
||||
/**
|
||||
* Instance for unsubscribing for a receiver.
|
||||
*
|
||||
* Note that this instance can be reused during an event dispatch, so
|
||||
* it is not a valid behavior to save the instance in event receivers.
|
||||
* Always use the one given by {@link EventReceiver#onReceive(Event, Unsubscribe)}.
|
||||
*/
|
||||
public class Unsubscribe {
|
||||
|
||||
private boolean unsubscribeFlag = false;
|
||||
|
||||
/**
|
||||
* Unsubscribe the event. And current receiver will not get event again.
|
||||
* References to the receiver are also removed.
|
||||
*/
|
||||
public void unsubscribe() {
|
||||
unsubscribeFlag = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether unsubscribe flag is set
|
||||
*/
|
||||
public boolean isUnsubscribed() {
|
||||
return unsubscribeFlag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the flag
|
||||
*/
|
||||
public void reset() {
|
||||
unsubscribeFlag = false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* sora-editor - the awesome code editor for Android
|
||||
* https://github.com/Rosemoe/sora-editor
|
||||
* Copyright (C) 2020-2022 Rosemoe
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
|
||||
* USA
|
||||
*
|
||||
* Please contact Rosemoe by email 2073412493@qq.com if you need
|
||||
* additional information or have any questions
|
||||
*/
|
||||
package io.github.rosemoe.sora.graphics;
|
||||
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
|
||||
public class BufferedDrawPoints {
|
||||
|
||||
private int pointCount;
|
||||
private float[] points;
|
||||
|
||||
public BufferedDrawPoints() {
|
||||
points = new float[128];
|
||||
}
|
||||
|
||||
public void drawPoint(float cx, float cy) {
|
||||
// Check buffer size and grow
|
||||
if (points.length < (pointCount + 1) * 2) {
|
||||
float[] newBuffer = new float[points.length << 1];
|
||||
System.arraycopy(points, 0, newBuffer, 0, pointCount * 2);
|
||||
points = newBuffer;
|
||||
}
|
||||
points[pointCount * 2] = cx;
|
||||
points[pointCount * 2 + 1] = cy;
|
||||
pointCount++;
|
||||
}
|
||||
|
||||
public void commitPoints(Canvas canvas, Paint paint) {
|
||||
if (pointCount == 0) {
|
||||
return;
|
||||
}
|
||||
canvas.drawPoints(points, 0, pointCount * 2, paint);
|
||||
pointCount = 0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
/*
|
||||
* sora-editor - the awesome code editor for Android
|
||||
* https://github.com/Rosemoe/sora-editor
|
||||
* Copyright (C) 2020-2022 Rosemoe
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
|
||||
* USA
|
||||
*
|
||||
* Please contact Rosemoe by email 2073412493@qq.com if you need
|
||||
* additional information or have any questions
|
||||
*/
|
||||
package io.github.rosemoe.sora.graphics;
|
||||
|
||||
import static io.github.rosemoe.sora.lang.styling.TextStyle.isBold;
|
||||
import static io.github.rosemoe.sora.lang.styling.TextStyle.isItalics;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.github.rosemoe.sora.lang.styling.Span;
|
||||
import io.github.rosemoe.sora.text.ContentLine;
|
||||
|
||||
public class GraphicTextRow {
|
||||
|
||||
private final static float SKEW_X = -0.2f;
|
||||
private final Paint mPaint = new Paint();
|
||||
private ContentLine mText;
|
||||
private int mStart;
|
||||
private int mEnd;
|
||||
private int mTabWidth;
|
||||
private List<Span> mSpans;
|
||||
private final float[] mBuffer;
|
||||
|
||||
private final static GraphicTextRow[] sCached = new GraphicTextRow[5];
|
||||
|
||||
private GraphicTextRow() {
|
||||
mBuffer = new float[2];
|
||||
}
|
||||
|
||||
public static GraphicTextRow obtain() {
|
||||
GraphicTextRow st;
|
||||
synchronized (sCached) {
|
||||
for (int i = sCached.length; --i >= 0; ) {
|
||||
if (sCached[i] != null) {
|
||||
st = sCached[i];
|
||||
sCached[i] = null;
|
||||
return st;
|
||||
}
|
||||
}
|
||||
}
|
||||
st = new GraphicTextRow();
|
||||
return st;
|
||||
}
|
||||
|
||||
public static void recycle(GraphicTextRow st) {
|
||||
st.mText = null;
|
||||
st.mSpans = null;
|
||||
st.mStart = st.mEnd = st.mTabWidth = 0;
|
||||
synchronized (sCached) {
|
||||
for (int i = 0; i < sCached.length; ++i) {
|
||||
if (sCached[i] == null) {
|
||||
sCached[i] = st;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void set(ContentLine line, int start, int end, int tabWidth, List<Span> spans, Paint paint) {
|
||||
if (mPaint.getTextSize() != paint.getTextSize())
|
||||
mPaint.setTextSizeWrapped(paint.getTextSize());
|
||||
var typeface = paint.getTypeface();
|
||||
if (mPaint.getTypeface() != typeface) {
|
||||
mPaint.setTypefaceWrapped(typeface);
|
||||
}
|
||||
if (paint.getFontFeatureSettings() != null)
|
||||
mPaint.setFontFeatureSettingsWrapped(paint.getFontFeatureSettings());
|
||||
mText = line;
|
||||
mTabWidth = tabWidth;
|
||||
mStart = start;
|
||||
mEnd = end;
|
||||
mSpans = spans;
|
||||
}
|
||||
|
||||
public void buildMeasureCache() {
|
||||
if (mText.widthCache == null || mText.widthCache.length < mEnd) {
|
||||
mText.widthCache = new float[Math.max(128, mText.length())];
|
||||
}
|
||||
measureTextInternal(mStart, mEnd, mText.widthCache);
|
||||
}
|
||||
|
||||
/**
|
||||
* From {@code start} to measure characters, until measured width add next char's width is bigger
|
||||
* than {@code advance}.
|
||||
*
|
||||
* Note that the result array should not be stored.
|
||||
*
|
||||
* @return Element 0 is offset, Element 1 is measured width
|
||||
*/
|
||||
public float[] findOffsetByAdvance(int start, float advance) {
|
||||
if (mText.widthCache != null) {
|
||||
float w = 0f;
|
||||
var cache = mText.widthCache;
|
||||
for (int i = start; i < mEnd;i++) {
|
||||
if (w > advance) {
|
||||
mBuffer[0] = Math.max(start, i - 1);
|
||||
mBuffer[1] = i > start ? w - cache[i - 1] : w;
|
||||
return mBuffer;
|
||||
} else {
|
||||
w += cache[i];
|
||||
}
|
||||
}
|
||||
mBuffer[0] = mEnd;
|
||||
mBuffer[1] = w;
|
||||
return mBuffer;
|
||||
}
|
||||
int regionStart = start;
|
||||
int index = 0;
|
||||
// Skip leading spans
|
||||
while (index < mSpans.size() && mSpans.get(index).column < regionStart) {
|
||||
index++;
|
||||
}
|
||||
float currentPosition = 0f;
|
||||
// Find in each region
|
||||
var lastStyle = 0L;
|
||||
var chars = mText.value;
|
||||
float tabAdvance = mPaint.getSpaceWidth() * mTabWidth;
|
||||
int offset = start;
|
||||
while (index <= mSpans.size() && currentPosition < advance) {
|
||||
var regionEnd = index < mSpans.size() ? mSpans.get(index).column : mEnd;
|
||||
regionEnd = Math.min(mEnd, regionEnd);
|
||||
int styleSpanIndex = Math.max(0, index - 1);
|
||||
var style = mSpans.get(styleSpanIndex).getStyleBits();
|
||||
if (style != lastStyle) {
|
||||
if (isBold(style) != isBold(lastStyle)) {
|
||||
mPaint.setFakeBoldText(isBold(style));
|
||||
}
|
||||
if (isItalics(style) != isItalics(lastStyle)) {
|
||||
mPaint.setTextSkewX(isItalics(style) ? SKEW_X : 0f);
|
||||
}
|
||||
lastStyle = style;
|
||||
}
|
||||
|
||||
// Find in sub-region
|
||||
int res = -1;
|
||||
{
|
||||
int lastStart = regionStart;
|
||||
for (int i = regionStart; i < regionEnd; i++) {
|
||||
if (chars[i] == '\t') {
|
||||
// Here is a tab
|
||||
// Try to find advance
|
||||
if (lastStart != i) {
|
||||
int idx = mPaint.findOffsetByRunAdvance(mText, lastStart, i, advance - currentPosition);
|
||||
currentPosition += mPaint.measureTextRunAdvance(chars, lastStart, idx, regionStart, regionEnd);
|
||||
if (idx < i) {
|
||||
res = idx;
|
||||
break;
|
||||
} else {
|
||||
if (currentPosition + tabAdvance > advance) {
|
||||
res = i;
|
||||
break;
|
||||
} else {
|
||||
currentPosition += tabAdvance;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (currentPosition + tabAdvance > advance) {
|
||||
res = i;
|
||||
break;
|
||||
} else {
|
||||
currentPosition += tabAdvance;
|
||||
}
|
||||
}
|
||||
lastStart = i + 1;
|
||||
}
|
||||
}
|
||||
if (res == -1) {
|
||||
int idx = mPaint.findOffsetByRunAdvance(mText, lastStart, regionEnd, advance - currentPosition);
|
||||
currentPosition += measureText(lastStart, idx);
|
||||
res = idx;
|
||||
}
|
||||
}
|
||||
|
||||
offset = res;
|
||||
if (res < regionEnd) {
|
||||
break;
|
||||
}
|
||||
|
||||
index ++;
|
||||
regionStart = regionEnd;
|
||||
if (regionEnd == mEnd) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (lastStyle != 0L) {
|
||||
mPaint.setFakeBoldText(false);
|
||||
mPaint.setTextSkewX(0f);
|
||||
}
|
||||
if (currentPosition > advance && offset > start) {
|
||||
offset--;
|
||||
currentPosition -= measureText(offset, offset + 1);
|
||||
}
|
||||
mBuffer[0] = offset;
|
||||
mBuffer[1] = currentPosition;
|
||||
return mBuffer;
|
||||
}
|
||||
|
||||
public float measureText(int start, int end) {
|
||||
if (start == end) {
|
||||
return 0f;
|
||||
}
|
||||
if (mText.widthCache != null) {
|
||||
float width = 0f;
|
||||
var cache = mText.widthCache;
|
||||
for (int i = start;i < end;i++) {
|
||||
width += cache[i];
|
||||
}
|
||||
return width;
|
||||
}
|
||||
return measureTextInternal(start, end, null);
|
||||
}
|
||||
|
||||
private float measureTextInternal(int start, int end, float[] widths) {
|
||||
start = Math.max(start, mStart);
|
||||
end = Math.min(end, mEnd);
|
||||
if (mSpans.size() == 0) {
|
||||
throw new IllegalArgumentException("At least one span is needed");
|
||||
}
|
||||
int regionStart = start;
|
||||
int index = 0;
|
||||
// Skip leading spans
|
||||
while (index < mSpans.size() && mSpans.get(index).column < regionStart) {
|
||||
index++;
|
||||
}
|
||||
float width = 0f;
|
||||
// Measure for each region
|
||||
var lastStyle = 0L;
|
||||
while (index <= mSpans.size()) {
|
||||
var regionEnd = index < mSpans.size() ? mSpans.get(index).column : mEnd;
|
||||
regionEnd = Math.min(end, regionEnd);
|
||||
int styleSpanIndex = Math.max(0, index - 1);
|
||||
var style = mSpans.get(styleSpanIndex).getStyleBits();
|
||||
if (style != lastStyle) {
|
||||
if (isBold(style) != isBold(lastStyle)) {
|
||||
mPaint.setFakeBoldText(isBold(style));
|
||||
}
|
||||
if (isItalics(style) != isItalics(lastStyle)) {
|
||||
mPaint.setTextSkewX(isItalics(style) ? SKEW_X : 0f);
|
||||
}
|
||||
lastStyle = style;
|
||||
}
|
||||
width += measureTextInner(regionStart, regionEnd, widths);
|
||||
index++;
|
||||
regionStart = regionEnd;
|
||||
if (regionEnd == end) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (lastStyle != 0L) {
|
||||
mPaint.setFakeBoldText(false);
|
||||
mPaint.setTextSkewX(0f);
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
private float measureTextInner(int start, int end, float[] widths) {
|
||||
if (start == end) {
|
||||
return 0f;
|
||||
}
|
||||
// Can be called directly
|
||||
float width = mPaint.getTextRunAdvances(mText.value, start, end - start, start, end - start, false, widths, widths == null ? 0 : start);
|
||||
float tabWidth = mPaint.getSpaceWidth() * mTabWidth;
|
||||
int tabCount = 0;
|
||||
for (int i = start; i < end; i++) {
|
||||
if (mText.charAt(i) == '\t') {
|
||||
tabCount++;
|
||||
if (widths != null) {
|
||||
widths[i] = tabWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
float extraWidth = tabCount == 0 ? 0 : tabWidth - mPaint.measureText("\t");
|
||||
return width + extraWidth * tabCount;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* sora-editor - the awesome code editor for Android
|
||||
* https://github.com/Rosemoe/sora-editor
|
||||
* Copyright (C) 2020-2022 Rosemoe
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
|
||||
* USA
|
||||
*
|
||||
* Please contact Rosemoe by email 2073412493@qq.com if you need
|
||||
* additional information or have any questions
|
||||
*/
|
||||
package io.github.rosemoe.sora.graphics;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.graphics.Typeface;
|
||||
import android.os.Build;
|
||||
|
||||
import io.github.rosemoe.sora.text.ContentLine;
|
||||
|
||||
public class Paint extends android.graphics.Paint {
|
||||
|
||||
private float spaceWidth;
|
||||
|
||||
public Paint() {
|
||||
super();
|
||||
spaceWidth = measureText(" ");
|
||||
}
|
||||
|
||||
public float getSpaceWidth() {
|
||||
return spaceWidth;
|
||||
}
|
||||
|
||||
public void setTypefaceWrapped(Typeface typeface) {
|
||||
super.setTypeface(typeface);
|
||||
spaceWidth = measureText(" ");
|
||||
}
|
||||
|
||||
public void setTextSizeWrapped(float textSize) {
|
||||
super.setTextSize(textSize);
|
||||
spaceWidth = measureText(" ");
|
||||
}
|
||||
|
||||
public void setFontFeatureSettingsWrapped(String settings) {
|
||||
super.setFontFeatureSettings(settings);
|
||||
spaceWidth = measureText(" ");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(android.graphics.Paint src) {
|
||||
super.set(src);
|
||||
spaceWidth = measureText(" ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the advance of text with the context positions related to shaping the characters
|
||||
*/
|
||||
@SuppressLint("NewApi")
|
||||
public float measureTextRunAdvance(char[] text, int start, int end, int contextStart, int contextEnd) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
return getRunAdvance(text, start, end, contextStart, contextEnd, false, end);
|
||||
} else {
|
||||
// Hidden, but we can call it directly on Android 21 - 22
|
||||
return getTextRunAdvances(text, start, end - start, contextStart, contextEnd - contextStart, false, null, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find offset for a certain advance returned by {@link #measureTextRunAdvance(char[], int, int, int, int)}
|
||||
*/
|
||||
public int findOffsetByRunAdvance(ContentLine text, int start, int end, float advance) {
|
||||
if (text.widthCache != null) {
|
||||
var cache = text.widthCache;
|
||||
var offset = start;
|
||||
var currAdvance = 0f;
|
||||
for (;offset < end && currAdvance < advance;offset++) {
|
||||
currAdvance += cache[offset];
|
||||
}
|
||||
if (currAdvance > advance) {
|
||||
offset--;
|
||||
}
|
||||
return Math.max(offset, start);
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
return getOffsetForAdvance(text, start, end, start, end, false, advance);
|
||||
} else {
|
||||
return start + breakText(text.value, start, end - start, advance, null);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* sora-editor - the awesome code editor for Android
|
||||
* https://github.com/Rosemoe/sora-editor
|
||||
* Copyright (C) 2020-2022 Rosemoe
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
|
||||
* USA
|
||||
*
|
||||
* Please contact Rosemoe by email 2073412493@qq.com if you need
|
||||
* additional information or have any questions
|
||||
*/
|
||||
package io.github.rosemoe.sora.interfaces;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
|
||||
import io.github.rosemoe.sora.annotations.Experimental;
|
||||
import io.github.rosemoe.sora.widget.schemes.EditorColorScheme;
|
||||
|
||||
/**
|
||||
* External renderer for spans.
|
||||
* <p>
|
||||
* Users can attach this renderer to any span so that you can draw
|
||||
* extra content inside the rectangle on canvas of the span.
|
||||
* <p>
|
||||
* However, any implementation of this class should not create many new objects
|
||||
* in its methods but initialize your required resources when the renderer is
|
||||
* created.
|
||||
* <p>
|
||||
* Meanwhile, the renderer should be consistent with editor's current
|
||||
* color scheme. Use colors defined in your color schemes if possible.
|
||||
* <p>
|
||||
* Also try to create universal renderers for a certain effect.
|
||||
* <p>
|
||||
* Note: the Paint object should not be modified or used to draw objects on the canvas
|
||||
* It is only provided for measuring. Use another Paint created by yourself instead.
|
||||
*
|
||||
* @author Rosemoe
|
||||
*/
|
||||
@Experimental
|
||||
public interface ExternalRenderer {
|
||||
|
||||
boolean requirePreDraw();
|
||||
|
||||
boolean requirePostDraw();
|
||||
|
||||
/**
|
||||
* Called when the editor draws the given region.
|
||||
* @param canvas The canvas to draw
|
||||
* @param paint Paint for measuring
|
||||
* @param colorScheme Current color scheme
|
||||
* @param preOrPost True for preDraw, False for postDraw
|
||||
*/
|
||||
void draw(Canvas canvas, Paint paint, EditorColorScheme colorScheme, boolean preOrPost);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* sora-editor - the awesome code editor for Android
|
||||
* https://github.com/Rosemoe/sora-editor
|
||||
* Copyright (C) 2020-2022 Rosemoe
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
|
||||
* USA
|
||||
*
|
||||
* Please contact Rosemoe by email 2073412493@qq.com if you need
|
||||
* additional information or have any questions
|
||||
*/
|
||||
package io.github.rosemoe.sora.lang;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import io.github.rosemoe.sora.lang.analysis.AnalyzeManager;
|
||||
import io.github.rosemoe.sora.lang.analysis.StyleReceiver;
|
||||
import io.github.rosemoe.sora.lang.completion.CompletionPublisher;
|
||||
import io.github.rosemoe.sora.lang.smartEnter.NewlineHandler;
|
||||
import io.github.rosemoe.sora.text.CharPosition;
|
||||
import io.github.rosemoe.sora.text.ContentReference;
|
||||
import io.github.rosemoe.sora.widget.SymbolPairMatch;
|
||||
|
||||
/**
|
||||
* Empty language
|
||||
*
|
||||
* @author Rosemoe
|
||||
*/
|
||||
public class EmptyLanguage implements Language {
|
||||
|
||||
@Override
|
||||
public CharSequence format(CharSequence text) {
|
||||
return text;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SymbolPairMatch getSymbolPairs() {
|
||||
return new SymbolPairMatch.DefaultSymbolPairs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requireAutoComplete(@NonNull ContentReference content, @NonNull CharPosition position, @NonNull CompletionPublisher publisher, @NonNull Bundle extraArguments) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInterruptionLevel() {
|
||||
return INTERRUPTION_LEVEL_STRONG;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NewlineHandler[] getNewlineHandlers() {
|
||||
return new NewlineHandler[0];
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public AnalyzeManager getAnalyzeManager() {
|
||||
return EmptyAnalyzeManager.INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIndentAdvance(@NonNull ContentReference content, int line, int column) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useTab() {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static class EmptyAnalyzeManager implements AnalyzeManager {
|
||||
|
||||
public final static EmptyAnalyzeManager INSTANCE = new EmptyAnalyzeManager();
|
||||
|
||||
@Override
|
||||
public void setReceiver(@Nullable StyleReceiver receiver) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset(@NonNull ContentReference content, @NonNull Bundle extraArguments) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insert(CharPosition start, CharPosition end, CharSequence insertedContent) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(CharPosition start, CharPosition end, CharSequence deletedContent) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rerun() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user