Updated editor

This commit is contained in:
Akash Yadav
2022-02-02 09:32:54 +05:30
parent e2d5afbf7f
commit 6dd41eaf06
238 changed files with 22221 additions and 12245 deletions
+17
View File
@@ -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" />
+1
View File
@@ -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));
@@ -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 -19
View File
@@ -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();
}
}
+1
View File
@@ -0,0 +1 @@
/build
+45
View File
@@ -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
}
View File
+21
View File
@@ -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 ());
}
}
+28
View File
@@ -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