/Brackeys-IDE

👨‍💻 Brackeys IDE is a fast and free multi-language code editor for Android.

Primary LanguageJavaApache License 2.0Apache-2.0

Brackeys IDE

Brackeys IDE is a fast and free multi-language code editor for Android.

Android CI License


Table of Contents

EditorKit

  1. Gradle Dependency
  2. The Basics
  3. More Options
    1. Config
    2. Text Scroller
  4. Code Suggestions
  5. Undo Redo
  6. Navigation
    1. Text Navigation
    2. Find Replace
    3. Shortcuts
  7. Theming

Languages

  1. Gradle Dependency
  2. Custom Language
    1. LanguageParser
    2. SuggestionProvider
    3. LanguageStyler

EditorKit

The editorkit module provides code editor without any support for programming languages.

jCenter

Gradle Dependency

Add this to your module's build.gradle file:

dependencies {

    implementation 'com.brackeys.ui:editorkit:1.0.0'
}

The editorkit module does not provide support for syntax highlighting, you need to add specific language dependency. You can see list of available languages here.


The Basics

First, you need to add TextProcessor in your layout:

<com.brackeys.ui.editorkit.widget.TextProcessor
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="top|start"
    android:id="@+id/editor" />

Second, you need to provide a Language object to support syntax highlighting by using following code:

val editor = findViewById<TextProcessor>(R.id.editor)

editor.language = JavaScriptLanguage() // or any other language you want

Third, you need to call setTextContent to set the text. Avoid using the default setText method.

editor.setTextContent("your code here")

Also you might want to use setTextContent(PrecomputedTextCompat) if you're working with large text files.

Finally, after you set the text you need to clear undo/redo history because you don't want to keep the change history of other files.

import com.brackeys.ui.editorkit.utils.UndoStack

editor.undoStack = UndoStack()
editor.redoStack = UndoStack()

Now you can begin using the code editor.


More Options

Config

You can change the default code editor's behavior by passing the Config object to it:

editor.config = Config(
    fontSize = 14f, // text size, including the line numbers
    fontType = Typeface.MONOSPACE, // typeface, including the line numbers

    wordWrap = true, // whether the word wrap enabled
    codeCompletion = true, // whether the code suggestions will shown
    pinchZoom = true, // whether the zoom gesture enabled
    highlightCurrentLine = true, // whether the current line will be highlighted
    highlightDelimiters = true, // highlight open/closed brackets beside the cursor

    softKeyboard = false, // whether the fullscreen editing keyboard will shown

    autoIndentation = true, // whether the auto indentation enabled
    autoCloseBrackets = true, // automatically close open parenthesis/bracket/brace
    autoCloseQuotes = true, // automatically close single/double quote when typing
    useSpacesInsteadOfTabs = true, // insert spaces instead of tabs when using auto-indentation
    tabWidth = 4 // the tab width, works together with `useSpacesInsteadOfTabs`
)

Text Scroller

To attach the fast scroller you need to add TextScroller in your layout:

<com.brackeys.ui.editorkit.widget.TextScroller
    android:layout_width="30dp"
    android:layout_height="match_parent"
    android:id="@+id/scroller"
    app:thumbNormal="@drawable/fastscroll_normal"
    app:thumbDragging="@drawable/fastscroll_pressed"
    app:thumbTint="@color/blue"/>

Now you need to pass a reference to a view inside link method:

val editor = findViewById<TextProcessor>(R.id.editor)
val scroller = findViewById<TextScroller>(R.id.scroller)

scroller.link(editor)

Code Suggestions

When you working with a code editor you want to see the list of code suggestion. (Note that you have to provide a Language object before start using it.)

First, you need to create a layout file that will represent the suggestion item inside dropdown menu:

<!-- item_suggestion.xml -->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:singleLine="true"
    android:padding="6dp"
    android:textSize="12sp"
    android:typeface="monospace"
    android:id="@+id/title" />

Second, you need to create custom SuggestionAdapter:

class AutoCompleteAdapter(context: Context) : SuggestionAdapter(context, R.layout.item_suggestion) {

    override fun createViewHolder(parent: ViewGroup): SuggestionViewHolder {
        val inflater = LayoutInflater.from(parent.context)
        val view = inflater.inflate(R.layout.item_suggestion, parent, false)
        return AutoCompleteViewHolder(view)
    }
    
    class AutoCompleteViewHolder(itemView: View) : SuggestionViewHolder(itemView) {
    
        private val title: TextView = itemView.findViewById(R.id.title)
        
        override fun bind(suggestion: Suggestion?, query: String) {
            title.text = suggestion?.text
        }
    }
}

...and pass it to your code editor:

editor.suggestionAdapter = AutoCompleteAdapter(this)

You can enable/disable suggestions dynamically by changing the codeCompletion parameter in editor's Config.


Undo Redo

The TextProcessor supports undo/redo operations, but remember that you should always check the ability to undo/redo before calling actual methods:

// Undo
if (editor.canUndo()) {
    editor.undo()
}

// Redo
if (editor.canRedo()) {
    editor.redo()
}

Also you may have a use case when you want to update undo/redo buttons visibility or other UI after the text replacements is done, this can be achieved by adding OnUndoRedoChangedListener:

editor.onUndoRedoChangedListener = object : OnUndoRedoChangedListener {
    override fun onUndoRedoChanged() {
        val canUndo = editor.canUndo()
        val canRedo = editor.canRedo()
        
        // ...
    }
}

Navigation

Text Navigation

You can use these helper methods to navigate in text:

editor.moveCaretToStartOfLine()
editor.moveCaretToEndOfLine()
editor.moveCaretToPrevWord()
editor.moveCaretToNextWord()

...or use «Go to Line» feature to place the caret at the specific line:

import com.brackeys.ui.editorkit.exception.LineException

try {
    editor.gotoLine(lineNumber)
} catch (e: LineException) {
    Toast.makeText(this, "Line does not exists", Toast.LENGTH_SHORT).show()
}

Find Replace

Before using find method you have to provide a search string and FindParams object:

import com.brackeys.ui.editorkit.model.FindParams

val findParams = FindParams(
    regex = false, // whether the regex will be used
    matchCase = true, // case sensitive
    wordsOnly = true // words only?
)

editor.find("function", findParams)

After that, the TextProcessor will highlight all find results. You can also navigate between find results using findNext() and findPrevious() methods. To clear find spans use clearFindResultSpans() method.

If you want to replace selected find result you can use replaceFindResult(replaceText) method or replaceAllFindResults(replaceText) to replace all.

Shortcuts

If you're using bluetooth keyboard you probably want to use keyboard shortcuts to write your code faster. To support the keyboard shortcuts you need to add ShortcutListener:

editor.shortcutListener = object : ShortcutListener {
    override fun onShortcut(shortcut: Shortcut): Boolean {
        val (ctrl, shift, alt, keyCode) = shortcut
        return when {
            ctrl && keyCode == KeyEvent.KEYCODE_DPAD_LEFT -> editor.moveCaretToStartOfLine()
            ctrl && keyCode == KeyEvent.KEYCODE_DPAD_RIGHT -> editor.moveCaretToEndOfLine()
            alt && keyCode == KeyEvent.KEYCODE_DPAD_LEFT -> editor.moveCaretToPrevWord()
            alt && keyCode == KeyEvent.KEYCODE_DPAD_RIGHT -> editor.moveCaretToNextWord()
            // ...
            else -> false
        }
    }
}

The onShortcut method will be invoked only if at least one of following keys is pressed: CTRL, Shift, Alt.
You might already notice that you have to return a Boolean value as the result of onShortcut method. Return true if the listener has consumed the shortcut event, false otherwise.


Theming

The EditorTheme class provides some predefined color schemes:

editor.colorScheme = EditorTheme.DARCULA // default

// or you can use one of these:
EditorTheme.MONOKAI
EditorTheme.OBSIDIAN
EditorTheme.LADIES_NIGHT
EditorTheme.TOMORROW_NIGHT
EditorTheme.VISUAL_STUDIO_2013

You can also create custom theme by changing the ColorScheme properties:

editor.colorScheme = ColorScheme(
    textColor = Color.parseColor("#C8C8C8"),
    backgroundColor = Color.parseColor("#232323"),
    gutterColor = Color.parseColor("#2C2C2C"),
    gutterDividerColor = Color.parseColor("#555555"),
    gutterCurrentLineNumberColor = Color.parseColor("#FFFFFF"),
    gutterTextColor = Color.parseColor("#C6C8C6"),
    selectedLineColor = Color.parseColor("#141414"),
    selectionColor = Color.parseColor("#454464"),
    suggestionQueryColor = Color.parseColor("#4F98F7"),
    findResultBackgroundColor = Color.parseColor("#1C3D6B"),
    delimiterBackgroundColor = Color.parseColor("#616161"),
    syntaxScheme = SyntaxScheme(
        numberColor = Color.parseColor("#BACDAB"),
        operatorColor = Color.parseColor("#DCDCDC"),
        keywordColor = Color.parseColor("#669BD1"),
        typeColor = Color.parseColor("#669BD1"),
        langConstColor = Color.parseColor("#669BD1"),
        preprocessorColor = Color.parseColor("#C49594"),
        methodColor = Color.parseColor("#71C6B1"),
        stringColor = Color.parseColor("#CE9F89"),
        commentColor = Color.parseColor("#6BA455"),
        tagColor = Color.parseColor("#DCDCDC"),
        tagNameColor = Color.parseColor("#669BD1"),
        attrNameColor = Color.parseColor("#C8C8C8"),
        attrValueColor = Color.parseColor("#CE9F89"),
        entityRefColor = Color.parseColor("#BACDAB")
    )
)

Languages

The language modules provides support for programming languages. This includes syntax highlighting, code suggestions and source code parser. (Note that source code parser currently works only in language-javascript module, but it will be implemented for more languages soon)

jCenter

Gradle Dependency

Select your language and add it's dependency to your module's build.gradle file:

dependencies {

    implementation 'com.brackeys.ui:language-actionscript:1.0.0'
    implementation 'com.brackeys.ui:language-base:1.0.0' // for custom language
    implementation 'com.brackeys.ui:language-c:1.0.0'
    implementation 'com.brackeys.ui:language-cpp:1.0.0'
    implementation 'com.brackeys.ui:language-csharp:1.0.0'
    implementation 'com.brackeys.ui:language-html:1.0.0'
    implementation 'com.brackeys.ui:language-java:1.0.0'
    implementation 'com.brackeys.ui:language-javascript:1.0.0'
    implementation 'com.brackeys.ui:language-json:1.0.0'
    implementation 'com.brackeys.ui:language-kotlin:1.0.0'
    implementation 'com.brackeys.ui:language-lisp:1.0.0'
    implementation 'com.brackeys.ui:language-lua:1.0.0'
    implementation 'com.brackeys.ui:language-markdown:1.0.0'
    implementation 'com.brackeys.ui:language-plaintext:1.0.0'
    implementation 'com.brackeys.ui:language-python:1.0.0'
    implementation 'com.brackeys.ui:language-sql:1.0.0'
    implementation 'com.brackeys.ui:language-visualbasic:1.0.0'
    implementation 'com.brackeys.ui:language-xml:1.0.0'
}

Custom Language

First, add this to your module's build.gradle file:

dependencies {

    implementation 'com.brackeys.ui:language-base:1.0.0'
}

Second, implement the Language interface:

import com.brackeys.ui.language.base.Language
import com.brackeys.ui.language.base.parser.LanguageParser
import com.brackeys.ui.language.base.provider.SuggestionProvider
import com.brackeys.ui.language.base.styler.LanguageStyler

class CustomLanguage : Language {

    override fun getName(): String {
        return "custom language"
    }

    override fun getParser(): LanguageParser {
        return CustomParser()
    }

    override fun getProvider(): SuggestionProvider {
        return CustomProvider()
    }

    override fun getStyler(): LanguageStyler {
        return CustomStyler()
    }
}

Every language consist of 3 key components:

  1. LanguageParser is responsible for analyzing the source code. The code editor does not use this component directly.
  2. SuggestionProvider is responsible for collecting the names of functions, fields, and keywords within your file scope. The code editor use this component to display the list of code suggestions.
  3. LanguageStyler is responsible for syntax highlighting. The code editor use this component to display all spans on the screen.

LanguageParser

LanguageParser is an interface which detects syntax errors so you can display them in the TextProcessor later.

To create a custom parser you need to implement execute method that will return a ParseResult model as the result.
If ParseResult contains an exception it means that the source code can't compile and contains syntax errors. You should highlight an error line by calling editor.setErrorLine(lineNumber) method.
Remember, that you shouldn't use this method on the main thread.

class CustomParser : LanguageParser {

    override fun execute(name: String, source: String): ParseResult {
        // TODO Implement parser
        val lineNumber = 0
        val columnNumber = 0
        val parseException = ParseException("describe exception here", lineNumber, columnNumber)
        return ParseResult(parseException)
    }
}

SuggestionProvider

SuggestionProvider is an interface which provides code suggestions to display them in the TextProcessor.

After calling setTextContent the code editor will call processLine for each line to find all code suggestions. Also this method will be called every time text changes for the specific line, so you can keep your suggestion list up to date.

class CustomProvider : SuggestionProvider {

    // You can use WordsManager
    // if you don't want to write the language-specific implementation
    private val wordsManager = WordsManager()

    override fun getAll(): Set<Suggestion> {
        return wordsManager.getWords()
    }

    override fun processLine(lineNumber: Int, text: String) {
        wordsManager.processLine(lineNumber, text)
    }

    override fun deleteLine(lineNumber: Int) {
        wordsManager.deleteLine(lineNumber)
    }

    override fun clearLines() {
        wordsManager.clearLines()
    }
}

LanguageStyler

LanguageStyler is an interface which provides syntax highlight spans to display them in the TextProcessor.

The execute method will be executed on the main thread. That means the UI blocks during the execution and no interaction is possible for this period. The code editor never use this method directly. The enqueue method it's just asynchronous version of execute that will be called every time the text changes.

You can use regex or lexer in the execute method to match all the spans in the text. Remember: the more spans you add, the more time it takes to render on the main thread.

class CustomStyler : LanguageStyler {

    private var task: StylingTask? = null

    override fun execute(sourceCode: String, syntaxScheme: SyntaxScheme): List<SyntaxHighlightSpan> {
        val syntaxHighlightSpans = mutableListOf<SyntaxHighlightSpan>()
        
        // TODO Implement syntax highlighting
        
        return syntaxHighlightSpans
    }

    // StylingResult it's just a callback (List<SyntaxHighlightSpan>) -> Unit
    override fun enqueue(sourceCode: String, syntaxScheme: SyntaxScheme, stylingResult: StylingResult) {
        task?.cancelTask()
        task = StylingTask(
            doAsync = { execute(sourceCode, syntaxScheme) },
            onSuccess = stylingResult
        )
        task?.executeTask()
    }

    override fun cancel() {
        task?.cancelTask()
        task = null
    }
}