diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..eddb7ad
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,46 @@
+apply plugin: 'com.android.application'
+
+apply plugin: 'kotlin-android'
+
+apply plugin: 'kotlin-android-extensions'
+
+android {
+ compileSdkVersion rootProject.ext.compileSdkVersion
+ buildToolsVersion rootProject.ext.buildToolsVersion
+
+ defaultConfig {
+ applicationId "de.markusressel.kodeeditor"
+ minSdkVersion rootProject.ext.minSdkVersion
+ targetSdkVersion rootProject.ext.targetSdkVersion
+ versionCode rootProject.ext.versionCode
+ versionName rootProject.ext.versionName
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+
+ multiDexEnabled true
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ implementation project(':library')
+
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+
+ implementation "com.android.support:appcompat-v7:$supportLibVersion"
+
+ testImplementation 'junit:junit:4.12'
+ androidTestImplementation 'com.android.support.test:runner:1.0.2'
+ androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
+}
+
+configurations.all {
+ resolutionStrategy.force "com.android.support:support-v4:$supportLibVersion"
+ resolutionStrategy.force "com.android.support:appcompat-v7:$supportLibVersion"
+ resolutionStrategy.force "com.android.support:design:$supportLibVersion"
+}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..f1b4245
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -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
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..3544dbc
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/de/markusressel/kodeeditor/MainActivity.kt b/app/src/main/java/de/markusressel/kodeeditor/MainActivity.kt
new file mode 100644
index 0000000..4f03ace
--- /dev/null
+++ b/app/src/main/java/de/markusressel/kodeeditor/MainActivity.kt
@@ -0,0 +1,18 @@
+package de.markusressel.kodeeditor
+
+import android.os.Bundle
+import android.support.v7.app.AppCompatActivity
+import kotlinx.android.synthetic.main.activity_main.*
+
+class MainActivity : AppCompatActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super
+ .onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+
+ codeEditorView
+ .setText("# Topic 1\n" + "\n" + "This is a *simple* demo text written in **Markdown**")
+
+ }
+}
diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..c7bd21d
--- /dev/null
+++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..d5fccc5
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..a849a26
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..a2f5908
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..1b52399
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..ff10afd
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..115a4c7
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..dcd3cd8
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..459ca60
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..8ca12fe
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..8e19b41
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..b824ebd
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..4c19a13
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..3ab3e9c
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #3F51B5
+ #303F9F
+ #FF4081
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..a96ba16
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ KodeEditor
+
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..5885930
--- /dev/null
+++ b/app/src/main/res/values/styles.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..eb31ba9
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,54 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ ext.kotlin_version = '1.2.61'
+
+ ext {
+ gradle_plugin_version = '3.1.3'
+
+ minSdkVersion = 16
+ versionName = "0.0.1"
+ versionCode = 1
+
+ compileSdkVersion = 27
+ targetSdkVersion = 27
+ buildToolsVersion = "27.0.3"
+
+ // DEPENDENCIES
+
+ androidKtxVersion = "0.3"
+ supportLibVersion = "27.1.1"
+
+ javaxAnnotationVersion = "10.0-b28"
+ aboutlibrariesVersion = "6.0.9"
+ materialdrawerVersion = "6.0.8"
+ iconicsVersion = "3.0.4"
+
+ rxKotlinVersion = "2.3.0"
+ rxBindingVersion = "2.1.1"
+ rxLifecycleVersion = "2.2.1"
+
+ timberKtVersion = "1.5.1"
+ }
+
+
+ repositories {
+ google()
+ jcenter()
+ }
+ dependencies {
+ classpath "com.android.tools.build:gradle:$gradle_plugin_version"
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..743d692
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,13 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..6e2838d
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Sat Aug 25 17:52:39 CEST 2018
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
diff --git a/library/build.gradle b/library/build.gradle
new file mode 100644
index 0000000..5572a35
--- /dev/null
+++ b/library/build.gradle
@@ -0,0 +1,62 @@
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ compileSdkVersion rootProject.ext.compileSdkVersion
+ buildToolsVersion rootProject.ext.buildToolsVersion
+
+ defaultConfig {
+ minSdkVersion rootProject.ext.minSdkVersion
+ targetSdkVersion rootProject.ext.targetSdkVersion
+ versionCode rootProject.ext.versionCode
+ versionName rootProject.ext.versionName
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+// api 'org.jetbrains.kotlinx:kotlinx-coroutines-android:0.25.0'
+
+ implementation "com.android.support:appcompat-v7:$supportLibVersion"
+
+ // RxJava (RxKotlin actually)
+ implementation "io.reactivex.rxjava2:rxjava:2.2.0"
+ implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
+ implementation "io.reactivex.rxjava2:rxkotlin:${rootProject.ext.rxKotlinVersion}"
+
+ // RxBinding
+ // RxJava binding APIs for Android UI widgets from the platform and support libraries.
+ implementation "com.jakewharton.rxbinding2:rxbinding:${rootProject.ext.rxBindingVersion}"
+ implementation "com.jakewharton.rxbinding2:rxbinding-support-v4:${rootProject.ext.rxBindingVersion}"
+ implementation "com.jakewharton.rxbinding2:rxbinding-design:${rootProject.ext.rxBindingVersion}"
+
+ // RxLifecycle to prevent memory leaks
+ def rxLifecycleVersion = rootProject.ext.rxLifecycleVersion
+ implementation "com.trello.rxlifecycle2:rxlifecycle:$rxLifecycleVersion"
+ implementation "com.trello.rxlifecycle2:rxlifecycle-kotlin:$rxLifecycleVersion"
+ implementation "com.trello.rxlifecycle2:rxlifecycle-android:$rxLifecycleVersion"
+ implementation "com.trello.rxlifecycle2:rxlifecycle-android-lifecycle-kotlin:$rxLifecycleVersion"
+
+ // Zoom Layout Container
+ api 'com.otaliastudios:zoomlayout:1.3.0'
+
+ testImplementation 'junit:junit:4.12'
+ androidTestImplementation 'com.android.support.test:runner:1.0.2'
+ androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
+}
+repositories {
+ mavenCentral()
+}
+kotlin {
+ experimental {
+ coroutines "enable"
+ }
+}
diff --git a/library/proguard-rules.pro b/library/proguard-rules.pro
new file mode 100644
index 0000000..f1b4245
--- /dev/null
+++ b/library/proguard-rules.pro
@@ -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
diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..692fb76
--- /dev/null
+++ b/library/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
diff --git a/library/src/main/java/de/markusressel/kodeeditor/library/syntaxhighlighter/SyntaxHighlighter.kt b/library/src/main/java/de/markusressel/kodeeditor/library/syntaxhighlighter/SyntaxHighlighter.kt
new file mode 100644
index 0000000..7a1c19a
--- /dev/null
+++ b/library/src/main/java/de/markusressel/kodeeditor/library/syntaxhighlighter/SyntaxHighlighter.kt
@@ -0,0 +1,95 @@
+package de.markusressel.kodeeditor.library.syntaxhighlighter
+
+import android.text.Editable
+import android.text.Spannable
+import android.text.style.CharacterStyle
+import de.markusressel.kodeeditor.library.syntaxhighlighter.colorscheme.SyntaxColorScheme
+
+/**
+ * Interface for a SyntaxHighlighter
+ */
+interface SyntaxHighlighter {
+
+ val appliedStyles: MutableSet
+
+ /**
+ * The currently active color scheme
+ */
+ var colorScheme: SyntaxColorScheme
+
+ /**
+ * Get a set of rules for this highlighter
+ */
+ fun getRules(): Set
+
+ /**
+ * Get the default color scheme to use for this highlighter
+ */
+ fun getDefaultColorScheme(): SyntaxColorScheme
+
+ /**
+ * Highlight the given text
+ */
+ fun highlight(editable: Editable) {
+ // cleanup previously applied styles
+ // clear(editable)
+ clearAppliedStyles(editable)
+
+ // reapply
+ getRules()
+ .forEach {
+ val sectionType = it
+ .getSectionType()
+ it
+ .findMatches(editable)
+ .forEach {
+ val start = it
+ .range
+ .start
+ val end = it.range.endInclusive + 1
+
+ // needs to be called for each result
+ // so multiple spans are created and applied
+ val styles = colorScheme
+ .getStyles(sectionType)
+
+ highlight(editable, start, end, styles)
+ }
+ }
+ }
+
+ /**
+ * Apply a set of styles to a specific part of an editable
+ *
+ * @param editable the editable to highlight
+ * @param start the starting position
+ * @param end the end position (inclusive)
+ * @param styles the styles to apply
+ */
+ private fun highlight(editable: Editable, start: Int, end: Int, styles: Set<() -> CharacterStyle>) {
+ styles
+ .forEach {
+ val style = it()
+ editable
+ .setSpan(style, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
+
+ // remember which styles were applied
+ appliedStyles
+ .add(style)
+ }
+ }
+
+ /**
+ * Clear any modifications the syntax highlighter may have made to a given editable
+ */
+ fun clearAppliedStyles(editable: Editable) {
+ appliedStyles
+ .forEach {
+ editable
+ .removeSpan(it)
+ }
+ appliedStyles
+ .clear()
+ }
+
+}
\ No newline at end of file
diff --git a/library/src/main/java/de/markusressel/kodeeditor/library/syntaxhighlighter/SyntaxHighlighterBase.kt b/library/src/main/java/de/markusressel/kodeeditor/library/syntaxhighlighter/SyntaxHighlighterBase.kt
new file mode 100644
index 0000000..9bf82cd
--- /dev/null
+++ b/library/src/main/java/de/markusressel/kodeeditor/library/syntaxhighlighter/SyntaxHighlighterBase.kt
@@ -0,0 +1,12 @@
+package de.markusressel.kodeeditor.library.syntaxhighlighter
+
+import android.text.style.CharacterStyle
+import de.markusressel.kodeeditor.library.syntaxhighlighter.colorscheme.SyntaxColorScheme
+
+abstract class SyntaxHighlighterBase : SyntaxHighlighter {
+
+ override val appliedStyles: MutableSet = mutableSetOf()
+
+ override var colorScheme: SyntaxColorScheme = getDefaultColorScheme()
+
+}
\ No newline at end of file
diff --git a/library/src/main/java/de/markusressel/kodeeditor/library/syntaxhighlighter/SyntaxHighlighterRule.kt b/library/src/main/java/de/markusressel/kodeeditor/library/syntaxhighlighter/SyntaxHighlighterRule.kt
new file mode 100644
index 0000000..e149613
--- /dev/null
+++ b/library/src/main/java/de/markusressel/kodeeditor/library/syntaxhighlighter/SyntaxHighlighterRule.kt
@@ -0,0 +1,18 @@
+package de.markusressel.kodeeditor.library.syntaxhighlighter
+
+import android.text.Editable
+import de.markusressel.kodeeditor.library.syntaxhighlighter.colorscheme.SectionTypeEnum
+
+interface SyntaxHighlighterRule {
+
+ /**
+ * Get the type of section this rule is meant for
+ */
+ fun getSectionType(): SectionTypeEnum
+
+ /**
+ * Find segments in the editable that are affected by this rule
+ */
+ fun findMatches(editable: Editable): Sequence
+
+}
\ No newline at end of file
diff --git a/library/src/main/java/de/markusressel/kodeeditor/library/syntaxhighlighter/colorscheme/SectionTypeEnum.kt b/library/src/main/java/de/markusressel/kodeeditor/library/syntaxhighlighter/colorscheme/SectionTypeEnum.kt
new file mode 100644
index 0000000..bae7479
--- /dev/null
+++ b/library/src/main/java/de/markusressel/kodeeditor/library/syntaxhighlighter/colorscheme/SectionTypeEnum.kt
@@ -0,0 +1,8 @@
+package de.markusressel.kodeeditor.library.syntaxhighlighter.colorscheme
+
+/**
+ * Text section types that may occur and can be styled individually
+ */
+enum class SectionTypeEnum {
+ BoldText, Heading, ItalicText, Link, SourceCode, StrikedText
+}
\ No newline at end of file
diff --git a/library/src/main/java/de/markusressel/kodeeditor/library/syntaxhighlighter/colorscheme/SyntaxColorScheme.kt b/library/src/main/java/de/markusressel/kodeeditor/library/syntaxhighlighter/colorscheme/SyntaxColorScheme.kt
new file mode 100644
index 0000000..7e14cef
--- /dev/null
+++ b/library/src/main/java/de/markusressel/kodeeditor/library/syntaxhighlighter/colorscheme/SyntaxColorScheme.kt
@@ -0,0 +1,15 @@
+package de.markusressel.kodeeditor.library.syntaxhighlighter.colorscheme
+
+import android.text.style.CharacterStyle
+
+/**
+ * A color scheme for a syntax highlighter
+ */
+interface SyntaxColorScheme {
+
+ /**
+ * Get a set of styles to apply for a specific text/section type
+ */
+ fun getStyles(type: SectionTypeEnum): Set<() -> CharacterStyle>
+
+}
diff --git a/library/src/main/java/de/markusressel/kodeeditor/library/view/CodeEditText.kt b/library/src/main/java/de/markusressel/kodeeditor/library/view/CodeEditText.kt
new file mode 100644
index 0000000..b51379a
--- /dev/null
+++ b/library/src/main/java/de/markusressel/kodeeditor/library/view/CodeEditText.kt
@@ -0,0 +1,87 @@
+package de.markusressel.kodeeditor.library.view
+
+import android.content.Context
+import android.support.v7.widget.AppCompatEditText
+import android.util.AttributeSet
+import android.util.Log
+import com.jakewharton.rxbinding2.widget.RxTextView
+import com.trello.rxlifecycle2.kotlin.bindToLifecycle
+import de.markusressel.kodeeditor.library.syntaxhighlighter.SyntaxHighlighter
+import de.markusressel.mkdocseditor.syntaxhighlighter.markdown.MarkdownSyntaxHighlighter
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.disposables.Disposable
+import io.reactivex.rxkotlin.subscribeBy
+import io.reactivex.schedulers.Schedulers
+import java.util.concurrent.TimeUnit
+
+class CodeEditText : AppCompatEditText {
+
+ var syntaxHighlighter: SyntaxHighlighter = MarkdownSyntaxHighlighter()
+ private var highlightingTimeout = 50L to TimeUnit.MILLISECONDS
+
+ private var highlightingDisposable: Disposable? = null
+
+ constructor(context: Context) : super(context) {
+ reinit()
+ }
+
+ constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
+ reinit()
+ }
+
+ constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
+ reinit()
+ }
+
+ private fun reinit() {
+ highlightingDisposable
+ ?.dispose()
+
+ highlightingDisposable = RxTextView
+ .afterTextChangeEvents(this)
+ .debounce(highlightingTimeout.first, highlightingTimeout.second)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .bindToLifecycle(this)
+ .subscribeBy(onNext = {
+ // syntax highlighting
+ refreshSyntaxHighlighting()
+ }, onError = {
+ Log
+ .e(TAG, "Error while refreshing syntax highlighting", it)
+ })
+ }
+
+ /**
+ * Set the timeout before new text is highlighted after the user has stopped typing
+ *
+ * @param timeout arbitrary value
+ * @param timeUnit the timeunit to use
+ */
+ @Suppress("unused")
+ fun setHighlightingTimeout(timeout: Long, timeUnit: TimeUnit) {
+ highlightingTimeout = timeout to timeUnit
+ reinit()
+ }
+
+ /**
+ * Get the current timeout in milliseconds
+ */
+ @Suppress("unused")
+ fun getHighlightingTimeout(): Long {
+ return highlightingTimeout
+ .second
+ .toMillis(highlightingTimeout.first)
+ }
+
+ @Synchronized
+ fun refreshSyntaxHighlighting() {
+ syntaxHighlighter
+ .highlight(text)
+ }
+
+ companion object {
+ const val TAG = "CodeEditText"
+ }
+
+}
\ No newline at end of file
diff --git a/library/src/main/java/de/markusressel/kodeeditor/library/view/CodeEditorView.kt b/library/src/main/java/de/markusressel/kodeeditor/library/view/CodeEditorView.kt
new file mode 100644
index 0000000..91cd697
--- /dev/null
+++ b/library/src/main/java/de/markusressel/kodeeditor/library/view/CodeEditorView.kt
@@ -0,0 +1,255 @@
+package de.markusressel.kodeeditor.library.view
+
+import android.content.Context
+import android.graphics.Rect
+import android.graphics.drawable.Drawable
+import android.support.annotation.StringRes
+import android.support.v4.view.ViewCompat
+import android.util.AttributeSet
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.View
+import android.widget.LinearLayout
+import android.widget.TextView
+import com.jakewharton.rxbinding2.widget.RxTextView
+import com.otaliastudios.zoom.ZoomLayout
+import com.trello.rxlifecycle2.kotlin.bindToLifecycle
+import de.markusressel.kodeeditor.library.syntaxhighlighter.SyntaxHighlighter
+import de.markusressel.library.R
+import io.reactivex.Observable
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.rxkotlin.subscribeBy
+import io.reactivex.schedulers.Schedulers
+import java.util.concurrent.TimeUnit
+import kotlin.math.roundToInt
+
+
+class CodeEditorView : ZoomLayout {
+
+ private lateinit var contentLayout: LinearLayout
+ lateinit var lineNumberView: TextView
+ lateinit var editTextView: CodeEditText
+
+ var moveWithCursorEnabled = false
+
+ private var currentLineCount = -1
+
+ constructor(context: Context) : super(context) {
+ initialize(null)
+ }
+
+ constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
+ initialize(attrs)
+ }
+
+ constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
+ initialize(attrs)
+ }
+
+ private fun initialize(attrs: AttributeSet?) {
+ readParameters(attrs)
+
+ inflateViews(LayoutInflater.from(context))
+ addView(contentLayout)
+
+ setListeners()
+
+ editTextView
+ .setViewBackgroundWithoutResettingPadding(null)
+
+ editTextView
+ .post {
+ editTextView
+ .setSelection(0)
+ }
+ }
+
+ private fun View.setViewBackgroundWithoutResettingPadding(background: Drawable?) {
+ val paddingBottom = this
+ .paddingBottom
+ val paddingStart = ViewCompat
+ .getPaddingStart(this)
+ val paddingEnd = ViewCompat
+ .getPaddingEnd(this)
+ val paddingTop = this
+ .paddingTop
+ ViewCompat
+ .setBackground(this, background)
+ ViewCompat
+ .setPaddingRelative(this, paddingStart, paddingTop, paddingEnd, paddingBottom)
+ }
+
+
+ private fun readParameters(attrs: AttributeSet?) {
+
+ }
+
+ private fun inflateViews(inflater: LayoutInflater) {
+ contentLayout = inflater.inflate(R.layout.view_code_editor__inner_layout, null) as LinearLayout
+ lineNumberView = contentLayout.findViewById(R.id.codeLinesView) as TextView
+ editTextView = contentLayout.findViewById(R.id.codeEditText) as CodeEditText
+
+ post {
+ val displayMetrics = context
+ .resources
+ .displayMetrics
+
+ contentLayout
+ .minimumHeight = displayMetrics
+ .heightPixels
+ contentLayout
+ .minimumWidth = displayMetrics
+ .widthPixels
+ }
+ }
+
+ private fun setListeners() {
+ setOnTouchListener { view, motionEvent ->
+ when (motionEvent.action) {
+ MotionEvent.ACTION_MOVE -> {
+ moveWithCursorEnabled = false
+ }
+ }
+ false
+ }
+
+ editTextView
+ .setOnClickListener {
+ moveWithCursorEnabled = true
+ }
+
+ Observable
+ .interval(250, TimeUnit.MILLISECONDS)
+ .filter { moveWithCursorEnabled }
+ .bindToLifecycle(this)
+ .subscribeBy(onNext = {
+ moveScreenWithCursorIfNecessary()
+ }, onError = {
+ Log
+ .e(TAG, "Error moving screen with cursor", it)
+ })
+
+ RxTextView
+ .textChanges(editTextView)
+ .debounce(50, TimeUnit.MILLISECONDS)
+ .filter {
+ moveWithCursorEnabled = true
+ editTextView.lineCount != currentLineCount
+ }
+ .subscribeOn(Schedulers.computation())
+ .observeOn(AndroidSchedulers.mainThread())
+ .bindToLifecycle(this)
+ .subscribeBy(onNext = {
+ updateLineNumbers(editTextView.lineCount)
+ }, onError = {
+ Log
+ .e(TAG, "Error updating line numbers", it)
+ })
+ }
+
+ private fun moveScreenWithCursorIfNecessary() {
+ val pos = editTextView
+ .selectionStart
+ val layout = editTextView
+ .layout
+
+ if (layout != null) {
+ val line = layout
+ .getLineForOffset(pos)
+ val baseline = layout
+ .getLineBaseline(line)
+ val ascent = layout
+ .getLineAscent(line)
+ val x = layout
+ .getPrimaryHorizontal(pos)
+ val y = (baseline + ascent)
+ .toFloat()
+
+ val zoomLayoutRect = Rect()
+ getLocalVisibleRect(zoomLayoutRect)
+
+ val transformedX = x * realZoom + panX * realZoom + lineNumberView.width * realZoom
+ val transformedY = y * realZoom + panY * realZoom
+
+ if (!zoomLayoutRect.contains(transformedX.roundToInt(), transformedY.roundToInt())) {
+
+ var newX = panX
+ var newY = panY
+
+ if (transformedX < zoomLayoutRect.left || transformedX > zoomLayoutRect.right) {
+ newX = -x
+ }
+
+ if (transformedY < zoomLayoutRect.top || transformedY > zoomLayoutRect.bottom) {
+ newY = -y
+ }
+
+ moveTo(zoom, newX, newY, false)
+ }
+ }
+ }
+
+ private fun updateLineNumbers(lines: Int) {
+ currentLineCount = lines
+
+ val linesToDraw = if (lines < MIN_LINES) {
+ MIN_LINES
+ } else {
+ lines
+ }
+
+ val sb = StringBuilder()
+ for (i in 1..linesToDraw) {
+ sb
+ .append("$i:\n")
+ }
+ lineNumberView
+ .text = sb
+ .toString()
+ }
+
+ /**
+ * @param editable true = user can type, false otherwise
+ */
+ fun setEditable(editable: Boolean) {
+ editTextView
+ .isEnabled = editable
+ }
+
+ /**
+ * Set the text in the editor
+ */
+ fun setText(text: CharSequence) {
+ editTextView
+ .setText(text, TextView.BufferType.EDITABLE)
+ editTextView
+ .refreshSyntaxHighlighting()
+ }
+
+ /**
+ * Set the text in the editor
+ */
+ @Suppress("unused")
+ fun setText(@StringRes text: Int) {
+ editTextView
+ .setText(text, TextView.BufferType.EDITABLE)
+ editTextView
+ .refreshSyntaxHighlighting()
+ }
+
+ /**
+ * Set the syntax highlighter to use for this CodeEditor
+ */
+ @Suppress("unused")
+ fun setSyntaxHighlighter(syntaxHighlighter: SyntaxHighlighter) {
+ editTextView
+ .syntaxHighlighter = syntaxHighlighter
+ }
+
+ companion object {
+ const val TAG = "CodeEditorView"
+ const val MIN_LINES = 1
+ }
+
+}
diff --git a/library/src/main/res/layout/view_code_editor__inner_layout.xml b/library/src/main/res/layout/view_code_editor__inner_layout.xml
new file mode 100644
index 0000000..53d841c
--- /dev/null
+++ b/library/src/main/res/layout/view_code_editor__inner_layout.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..3306997
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+include ':app', ':library'