From 557729180f95b715605ff4cffb89aefaa710e2b3 Mon Sep 17 00:00:00 2001
From: Janne Koschinski <janne@kuschku.de>
Date: Sun, 8 Apr 2018 22:01:48 +0200
Subject: [PATCH] Cleaning up input code

---
 .../quasseldroid/ui/chat/ChatActivity.kt      | 101 +--
 .../ui/chat/ChatFragmentProvider.kt           |   6 +-
 .../ui/chat/input/AutoCompleteHelper.kt       | 219 +++++++
 .../ui/chat/input/AutoCompletionState.kt      |  10 +
 .../ui/chat/input/ChatlineFragment.kt         | 177 +++++
 .../quasseldroid/ui/chat/input/Editor.kt      | 610 ------------------
 .../ui/chat/input/EditorHelper.kt             | 206 ++++++
 .../{FormatHandler.kt => RichEditText.kt}     | 243 +++----
 .../quasseldroid/ui/chat/input/RichToolbar.kt | 108 ++++
 .../util/helper/CharSequenceHelper.kt         | 130 +++-
 .../util/helper/EditableHelper.kt             |  34 +
 .../quasseldroid/util/helper/SpannedHelper.kt | 142 +---
 .../quasseldroid/util/ui/DoubleClickHelper.kt |  24 +
 .../util/ui/EditTextSelectionChange.kt        |   4 +-
 app/src/main/res/layout-land/layout_main.xml  |   9 +-
 .../res/layout-sw600dp-land/layout_main.xml   |   9 +-
 ...ayout_slider.xml => fragment_chatline.xml} |   0
 app/src/main/res/layout/layout_editor.xml     |  16 +-
 app/src/main/res/layout/layout_main.xml       |   9 +-
 app/src/main/res/layout/widget_formatting.xml | 227 +++----
 .../quasseldroid/viewmodel/EditorViewModel.kt | 125 ++++
 .../viewmodel/QuasselViewModel.kt             | 105 ---
 22 files changed, 1317 insertions(+), 1197 deletions(-)
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/AutoCompleteHelper.kt
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/AutoCompletionState.kt
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/ChatlineFragment.kt
 delete mode 100644 app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/Editor.kt
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/EditorHelper.kt
 rename app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/{FormatHandler.kt => RichEditText.kt} (51%)
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/RichToolbar.kt
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid/util/helper/EditableHelper.kt
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid/util/ui/DoubleClickHelper.kt
 rename app/src/main/res/layout/{layout_slider.xml => fragment_chatline.xml} (100%)
 create mode 100644 viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/EditorViewModel.kt

diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatActivity.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatActivity.kt
index ad38ab225..de0b6ca40 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatActivity.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatActivity.kt
@@ -10,9 +10,6 @@ import android.os.Bundle
 import android.os.PersistableBundle
 import android.support.v4.widget.DrawerLayout
 import android.support.v7.app.ActionBarDrawerToggle
-import android.support.v7.widget.DefaultItemAnimator
-import android.support.v7.widget.LinearLayoutManager
-import android.support.v7.widget.RecyclerView
 import android.support.v7.widget.Toolbar
 import android.text.Html
 import android.view.*
@@ -25,7 +22,6 @@ import de.kuschku.libquassel.protocol.Buffer_Type
 import de.kuschku.libquassel.protocol.Message
 import de.kuschku.libquassel.protocol.Message_Type
 import de.kuschku.libquassel.protocol.message.HandshakeMessage
-import de.kuschku.libquassel.quassel.syncables.interfaces.IAliasManager
 import de.kuschku.libquassel.session.ConnectionState
 import de.kuschku.libquassel.util.flag.and
 import de.kuschku.libquassel.util.flag.hasFlag
@@ -36,8 +32,7 @@ import de.kuschku.quasseldroid.persistence.AccountDatabase
 import de.kuschku.quasseldroid.persistence.QuasselDatabase
 import de.kuschku.quasseldroid.settings.MessageSettings
 import de.kuschku.quasseldroid.settings.Settings
-import de.kuschku.quasseldroid.ui.chat.input.Editor
-import de.kuschku.quasseldroid.ui.chat.input.MessageHistoryAdapter
+import de.kuschku.quasseldroid.ui.chat.input.ChatlineFragment
 import de.kuschku.quasseldroid.ui.clientsettings.app.AppSettingsActivity
 import de.kuschku.quasseldroid.ui.coresettings.CoreSettingsActivity
 import de.kuschku.quasseldroid.util.helper.editCommit
@@ -47,7 +42,6 @@ import de.kuschku.quasseldroid.util.helper.toLiveData
 import de.kuschku.quasseldroid.util.irc.format.IrcFormatDeserializer
 import de.kuschku.quasseldroid.util.service.ServiceBoundActivity
 import de.kuschku.quasseldroid.util.ui.MaterialContentLoadingProgressBar
-import de.kuschku.quasseldroid.viewmodel.data.AutoCompleteItem
 import de.kuschku.quasseldroid.viewmodel.data.BufferData
 import javax.inject.Inject
 
@@ -64,14 +58,6 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
   @BindView(R.id.editor_panel)
   lateinit var editorPanel: SlidingUpPanelLayout
 
-  @BindView(R.id.history_panel)
-  lateinit var historyPanel: SlidingUpPanelLayout
-
-  @BindView(R.id.msg_history)
-  lateinit var msgHistory: RecyclerView
-
-  private lateinit var drawerToggle: ActionBarDrawerToggle
-
   @Inject
   lateinit var database: QuasselDatabase
 
@@ -84,25 +70,16 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
   @Inject
   lateinit var ircFormatDeserializer: IrcFormatDeserializer
 
-  private lateinit var editor: Editor
+  private lateinit var drawerToggle: ActionBarDrawerToggle
 
-  private val panelSlideListener: SlidingUpPanelLayout.PanelSlideListener = object :
-    SlidingUpPanelLayout.PanelSlideListener {
-    override fun onPanelSlide(panel: View?, slideOffset: Float) = Unit
-
-    override fun onPanelStateChanged(panel: View?,
-                                     previousState: SlidingUpPanelLayout.PanelState?,
-                                     newState: SlidingUpPanelLayout.PanelState?) {
-      editor.setMultiLine(newState == SlidingUpPanelLayout.PanelState.COLLAPSED)
-    }
-  }
+  private var chatlineFragment: ChatlineFragment? = null
 
   override fun onNewIntent(intent: Intent?) {
     super.onNewIntent(intent)
     if (intent != null) {
       when {
         intent.type == "text/plain" -> {
-          editor.formatHandler.replace(intent.getStringExtra(Intent.EXTRA_TEXT))
+          chatlineFragment?.editorHelper?.replaceText(intent.getStringExtra(Intent.EXTRA_TEXT))
         }
       }
     }
@@ -113,58 +90,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
     setContentView(R.layout.activity_main)
     ButterKnife.bind(this)
 
-    editor = Editor(
-      this,
-      viewModel,
-      findViewById(R.id.chatline),
-      findViewById(R.id.send),
-      findViewById(R.id.tab_complete),
-      listOf(
-        findViewById(R.id.autocomplete_list),
-        findViewById(R.id.autocomplete_list_expanded)
-      ),
-      findViewById(R.id.formatting_toolbar),
-      ircFormatDeserializer,
-      appearanceSettings,
-      autoCompleteSettings,
-      messageSettings
-    )
-
-    editor.setOnSendListener { lines ->
-      viewModel.session { sessionOptional ->
-        val session = sessionOptional.orNull()
-        viewModel.buffer { bufferId ->
-          session?.bufferSyncer?.bufferInfo(bufferId)?.also { bufferInfo ->
-            val output = mutableListOf<IAliasManager.Command>()
-            for ((stripped, formatted) in lines) {
-              viewModel.addRecentlySentMessage(stripped)
-              session.aliasManager?.processInput(bufferInfo, formatted, output)
-            }
-            for (command in output) {
-              session.rpcHandler?.sendInput(command.buffer, command.message)
-            }
-          }
-        }
-      }
-    }
-
-    editor.setOnPanelStateListener { expanded ->
-      historyPanel.panelState = if (expanded)
-        SlidingUpPanelLayout.PanelState.EXPANDED
-      else
-        SlidingUpPanelLayout.PanelState.COLLAPSED
-    }
-
-    msgHistory.itemAnimator = DefaultItemAnimator()
-    msgHistory.layoutManager = LinearLayoutManager(this)
-    val messageHistoryAdapter = MessageHistoryAdapter()
-    messageHistoryAdapter.setOnItemClickListener { text ->
-      editor.formatHandler.replace(text)
-      historyPanel.panelState = SlidingUpPanelLayout.PanelState.COLLAPSED
-    }
-    msgHistory.adapter = messageHistoryAdapter
-    viewModel.recentlySentMessages_liveData
-      .observe(this, Observer(messageHistoryAdapter::submitList))
+    chatlineFragment = supportFragmentManager.findFragmentById(R.id.fragment_chatline) as? ChatlineFragment
 
     setSupportActionBar(toolbar)
 
@@ -315,8 +241,10 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
       invalidateOptionsMenu()
     })
 
-    editorPanel.addPanelSlideListener(panelSlideListener)
     editorPanel.panelState = SlidingUpPanelLayout.PanelState.COLLAPSED
+    chatlineFragment?.panelSlideListener?.let(editorPanel::addPanelSlideListener)
+
+    onNewIntent(intent)
   }
 
   var bufferData: BufferData? = null
@@ -344,21 +272,8 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
       recreate()
     }
     super.onStart()
-    editor.onStart()
   }
 
-  override fun onStop() {
-    editor.onStop()
-    super.onStop()
-  }
-
-  data class AutoCompletionState(
-    val originalWord: String,
-    val range: IntRange,
-    val lastCompletion: AutoCompleteItem? = null,
-    val completion: AutoCompleteItem
-  )
-
   override fun onSaveInstanceState(outState: Bundle?) {
     super.onSaveInstanceState(outState)
     outState?.putInt("OPEN_BUFFER", viewModel.buffer.value ?: -1)
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatFragmentProvider.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatFragmentProvider.kt
index ec98c3eac..905b76960 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatFragmentProvider.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatFragmentProvider.kt
@@ -3,6 +3,7 @@ package de.kuschku.quasseldroid.ui.chat
 import dagger.Module
 import dagger.android.ContributesAndroidInjector
 import de.kuschku.quasseldroid.ui.chat.buffers.BufferViewConfigFragment
+import de.kuschku.quasseldroid.ui.chat.input.ChatlineFragment
 import de.kuschku.quasseldroid.ui.chat.messages.MessageListFragment
 import de.kuschku.quasseldroid.ui.chat.nicks.NickListFragment
 
@@ -19,4 +20,7 @@ abstract class ChatFragmentProvider {
 
   @ContributesAndroidInjector
   abstract fun bindToolbarFragment(): ToolbarFragment
-}
\ No newline at end of file
+
+  @ContributesAndroidInjector
+  abstract fun bindChatlineFragment(): ChatlineFragment
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/AutoCompleteHelper.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/AutoCompleteHelper.kt
new file mode 100644
index 000000000..c0046653e
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/AutoCompleteHelper.kt
@@ -0,0 +1,219 @@
+package de.kuschku.quasseldroid.ui.chat.input
+
+import android.arch.lifecycle.Observer
+import android.graphics.Typeface
+import android.support.v4.app.FragmentActivity
+import android.text.SpannableString
+import android.text.style.ForegroundColorSpan
+import android.text.style.StyleSpan
+import de.kuschku.libquassel.protocol.Buffer_Type
+import de.kuschku.libquassel.quassel.syncables.IrcChannel
+import de.kuschku.libquassel.util.IrcUserUtils
+import de.kuschku.libquassel.util.flag.hasFlag
+import de.kuschku.libquassel.util.helpers.value
+import de.kuschku.quasseldroid.R
+import de.kuschku.quasseldroid.settings.AutoCompleteSettings
+import de.kuschku.quasseldroid.settings.MessageSettings
+import de.kuschku.quasseldroid.util.helper.styledAttributes
+import de.kuschku.quasseldroid.util.helper.toLiveData
+import de.kuschku.quasseldroid.util.irc.format.IrcFormatDeserializer
+import de.kuschku.quasseldroid.util.ui.TextDrawable
+import de.kuschku.quasseldroid.viewmodel.EditorViewModel
+import de.kuschku.quasseldroid.viewmodel.data.AutoCompleteItem
+import de.kuschku.quasseldroid.viewmodel.data.BufferStatus
+
+class AutoCompleteHelper(
+  private val activity: FragmentActivity,
+  private val autoCompleteSettings: AutoCompleteSettings,
+  private val messageSettings: MessageSettings,
+  private val ircFormatDeserializer: IrcFormatDeserializer,
+  private val viewModel: EditorViewModel
+) {
+  private var autocompleteListener: ((AutoCompletionState) -> Unit)? = null
+  private var dataListener: ((List<AutoCompleteItem>) -> Unit)? = null
+
+  var autoCompletionState: AutoCompletionState? = null
+
+  private val senderColors = activity.theme.styledAttributes(
+    R.attr.senderColor0, R.attr.senderColor1, R.attr.senderColor2, R.attr.senderColor3,
+    R.attr.senderColor4, R.attr.senderColor5, R.attr.senderColor6, R.attr.senderColor7,
+    R.attr.senderColor8, R.attr.senderColor9, R.attr.senderColorA, R.attr.senderColorB,
+    R.attr.senderColorC, R.attr.senderColorD, R.attr.senderColorE, R.attr.senderColorF
+  ) {
+    IntArray(length()) { getColor(it, 0) }
+  }
+
+  init {
+    viewModel.autoCompleteData.toLiveData().observe(activity, Observer {
+      val query = it?.first ?: ""
+      val shouldShowResults = (autoCompleteSettings.auto && query.length >= 3) ||
+                              (autoCompleteSettings.prefix && query.startsWith('@')) ||
+                              (autoCompleteSettings.prefix && query.startsWith('#'))
+      val list = if (shouldShowResults) it?.second.orEmpty() else emptyList()
+      dataListener?.invoke(list.map {
+        if (it is AutoCompleteItem.UserItem) {
+          val nickName = it.nick
+          val senderColorIndex = IrcUserUtils.senderColor(nickName)
+          val rawInitial = nickName.trimStart(*IGNORED_CHARS).firstOrNull()
+                           ?: nickName.firstOrNull()
+          val initial = rawInitial?.toUpperCase().toString()
+          val senderColor = senderColors[senderColorIndex]
+
+          fun formatNick(nick: CharSequence): CharSequence {
+            val spannableString = SpannableString(nick)
+            spannableString.setSpan(
+              ForegroundColorSpan(senderColor),
+              0,
+              nick.length,
+              SpannableString.SPAN_INCLUSIVE_EXCLUSIVE
+            )
+            spannableString.setSpan(
+              StyleSpan(Typeface.BOLD),
+              0,
+              nick.length,
+              SpannableString.SPAN_INCLUSIVE_EXCLUSIVE
+            )
+            return spannableString
+          }
+
+          it.copy(
+            displayNick = formatNick(it.nick),
+            fallbackDrawable = TextDrawable.builder().buildRound(initial, senderColor),
+            modes = when (messageSettings.showPrefix) {
+              MessageSettings.ShowPrefixMode.ALL ->
+                it.modes
+              else                               ->
+                it.modes.substring(0, Math.min(it.modes.length, 1))
+            },
+            realname = ircFormatDeserializer.formatString(
+              activity, it.realname.toString(), messageSettings.colorizeMirc
+            )
+          )
+        } else {
+          it
+        }
+      })
+    })
+  }
+
+  fun setAutocompleteListener(listener: ((AutoCompletionState) -> Unit)?) {
+    this.autocompleteListener = listener
+  }
+
+  fun setDataListener(listener: ((List<AutoCompleteItem>) -> Unit)?) {
+    this.dataListener = listener
+  }
+
+  private fun autoCompleteDataFull(): List<AutoCompleteItem> {
+    return viewModel.rawAutoCompleteData.value?.let { (sessionOptional, id, lastWord) ->
+      val session = sessionOptional.orNull()
+      val bufferInfo = session?.bufferSyncer?.bufferInfo(id)
+      session?.networks?.let { networks ->
+        session.bufferSyncer?.bufferInfos()?.let { infos ->
+          if (bufferInfo?.type?.hasFlag(Buffer_Type.ChannelBuffer) == true) {
+            val network = networks[bufferInfo.networkId]
+            network?.ircChannel(
+              bufferInfo.bufferName
+            )?.let { ircChannel ->
+              val users = ircChannel.ircUsers()
+              val buffers = infos
+                .filter {
+                  it.type.toInt() == Buffer_Type.ChannelBuffer.toInt()
+                }.mapNotNull { info ->
+                  networks[info.networkId]?.let { info to it }
+                }.map { (info, network) ->
+                  val channel = network.ircChannel(info.bufferName) ?: IrcChannel.NULL
+                  AutoCompleteItem.ChannelItem(
+                    info = info,
+                    network = network.networkInfo(),
+                    bufferStatus = when (channel) {
+                      IrcChannel.NULL -> BufferStatus.OFFLINE
+                      else            -> BufferStatus.ONLINE
+                    },
+                    description = channel.topic()
+                  )
+                }
+              val nicks = users.map { user ->
+                val userModes = ircChannel.userModes(user)
+                val prefixModes = network.prefixModes()
+
+                val lowestMode = userModes.mapNotNull(prefixModes::indexOf).min()
+                                 ?: prefixModes.size
+
+                AutoCompleteItem.UserItem(
+                  user.nick(),
+                  network.modesToPrefixes(userModes),
+                  lowestMode,
+                  user.realName(),
+                  user.isAway(),
+                  network.support("CASEMAPPING"),
+                  Regex("[us]id(\\d+)").matchEntire(user.user())?.groupValues?.lastOrNull()?.let {
+                    "https://www.irccloud.com/avatar-redirect/$it"
+                  }
+                )
+              }
+
+              (nicks + buffers).filter {
+                it.name.trimStart(*IGNORED_CHARS)
+                  .startsWith(
+                    lastWord.first.trimStart(*IGNORED_CHARS),
+                    ignoreCase = true
+                  )
+              }.sorted()
+            }
+          } else null
+        }
+      }
+    } ?: emptyList()
+  }
+
+  fun autoComplete(reverse: Boolean = false) {
+    viewModel.lastWord.switchMap { it }.value?.let { originalWord ->
+      val previous = autoCompletionState
+      if (!originalWord.second.isEmpty()) {
+        val autoCompletedWords = autoCompleteDataFull()
+        if (previous != null && originalWord.first == previous.originalWord && originalWord.second.start == previous.range.start) {
+          val previousIndex = autoCompletedWords.indexOf(previous.completion)
+          val autoCompletedWord = if (previousIndex != -1) {
+            val change = if (reverse) -1 else +1
+            val newIndex = (previousIndex + change + autoCompletedWords.size) % autoCompletedWords.size
+
+            autoCompletedWords[newIndex]
+          } else {
+            autoCompletedWords.firstOrNull()
+          }
+          if (autoCompletedWord != null) {
+            val newState = AutoCompletionState(
+              previous.originalWord,
+              originalWord.second,
+              previous.completion,
+              autoCompletedWord
+            )
+            autoCompletionState = newState
+            autocompleteListener?.invoke(newState)
+          } else {
+            autoCompletionState = null
+          }
+        } else {
+          val autoCompletedWord = autoCompletedWords.firstOrNull()
+          if (autoCompletedWord != null) {
+            val newState = AutoCompletionState(
+              originalWord.first,
+              originalWord.second,
+              null,
+              autoCompletedWord
+            )
+            autoCompletionState = newState
+            autocompleteListener?.invoke(newState)
+          } else {
+            autoCompletionState = null
+          }
+        }
+      }
+    }
+  }
+
+  companion object {
+    val IGNORED_CHARS = charArrayOf('-', '_', '[', ']', '{', '}', '|', '`', '^', '.', '\\', '@')
+  }
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/AutoCompletionState.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/AutoCompletionState.kt
new file mode 100644
index 000000000..91b1a8bd0
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/AutoCompletionState.kt
@@ -0,0 +1,10 @@
+package de.kuschku.quasseldroid.ui.chat.input
+
+import de.kuschku.quasseldroid.viewmodel.data.AutoCompleteItem
+
+data class AutoCompletionState(
+  val originalWord: String,
+  val range: IntRange,
+  val lastCompletion: AutoCompleteItem? = null,
+  val completion: AutoCompleteItem
+)
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/ChatlineFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/ChatlineFragment.kt
new file mode 100644
index 000000000..fcd2f8578
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/ChatlineFragment.kt
@@ -0,0 +1,177 @@
+package de.kuschku.quasseldroid.ui.chat.input
+
+import android.arch.lifecycle.Observer
+import android.arch.lifecycle.ViewModelProviders
+import android.os.Bundle
+import android.support.v7.widget.AppCompatImageButton
+import android.support.v7.widget.DefaultItemAnimator
+import android.support.v7.widget.LinearLayoutManager
+import android.support.v7.widget.RecyclerView
+import android.text.SpannableString
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import butterknife.BindView
+import butterknife.ButterKnife
+import com.sothree.slidinguppanel.SlidingUpPanelLayout
+import de.kuschku.libquassel.quassel.syncables.interfaces.IAliasManager
+import de.kuschku.quasseldroid.R
+import de.kuschku.quasseldroid.settings.AppearanceSettings
+import de.kuschku.quasseldroid.settings.AutoCompleteSettings
+import de.kuschku.quasseldroid.settings.MessageSettings
+import de.kuschku.quasseldroid.util.helper.invoke
+import de.kuschku.quasseldroid.util.helper.lineSequence
+import de.kuschku.quasseldroid.util.helper.retint
+import de.kuschku.quasseldroid.util.helper.visibleIf
+import de.kuschku.quasseldroid.util.irc.format.IrcFormatDeserializer
+import de.kuschku.quasseldroid.util.irc.format.IrcFormatSerializer
+import de.kuschku.quasseldroid.util.service.ServiceBoundFragment
+import de.kuschku.quasseldroid.viewmodel.EditorViewModel
+import javax.inject.Inject
+
+class ChatlineFragment : ServiceBoundFragment() {
+  @BindView(R.id.chatline)
+  lateinit var chatline: RichEditText
+
+  @BindView(R.id.formatting_toolbar)
+  lateinit var toolbar: RichToolbar
+
+  @BindView(R.id.send)
+  lateinit var send: AppCompatImageButton
+
+  @BindView(R.id.tab_complete)
+  lateinit var tabComplete: AppCompatImageButton
+
+  @BindView(R.id.msg_history)
+  lateinit var messageHistory: RecyclerView
+
+  @BindView(R.id.history_panel)
+  lateinit var historyPanel: SlidingUpPanelLayout
+
+  @Inject
+  lateinit var autoCompleteSettings: AutoCompleteSettings
+
+  @Inject
+  lateinit var messageSettings: MessageSettings
+
+  @Inject
+  lateinit var appearanceSettings: AppearanceSettings
+
+  @Inject
+  lateinit var ircFormatDeserializer: IrcFormatDeserializer
+
+  @Inject
+  lateinit var ircFormatSerializer: IrcFormatSerializer
+
+  lateinit var editorHelper: EditorHelper
+
+  val panelSlideListener = object : SlidingUpPanelLayout.PanelSlideListener {
+    override fun onPanelSlide(panel: View?, slideOffset: Float) = Unit
+
+    override fun onPanelStateChanged(panel: View?,
+                                     previousState: SlidingUpPanelLayout.PanelState?,
+                                     newState: SlidingUpPanelLayout.PanelState?) {
+      editorHelper.setMultiLine(newState == SlidingUpPanelLayout.PanelState.COLLAPSED)
+    }
+  }
+
+  override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
+                            savedInstanceState: Bundle?): View? {
+    val view = LayoutInflater.from(activity).inflate(R.layout.fragment_chatline, container, false)
+    ButterKnife.bind(this, view)
+
+
+    val editorViewModel = ViewModelProviders.of(this).get(EditorViewModel::class.java)
+    editorViewModel.quasselViewModel.onNext(viewModel)
+
+    val autoCompleteHelper = AutoCompleteHelper(
+      requireActivity(),
+      autoCompleteSettings,
+      messageSettings,
+      ircFormatDeserializer,
+      editorViewModel
+    )
+
+    editorHelper = EditorHelper(
+      requireActivity(),
+      chatline,
+      toolbar,
+      autoCompleteHelper,
+      autoCompleteSettings,
+      appearanceSettings
+    )
+
+    editorViewModel.lastWord.onNext(editorHelper.lastWord)
+
+    if (autoCompleteSettings.prefix || autoCompleteSettings.auto) {
+      val autoCompleteLists = listOfNotNull<RecyclerView>(
+        view.findViewById(R.id.autocomplete_list),
+        view.findViewById(R.id.autocomplete_list_expanded)
+      )
+      val autocompleteAdapter = AutoCompleteAdapter(messageSettings, chatline::autoComplete)
+      for (autoCompleteList in autoCompleteLists) {
+        autoCompleteList.layoutManager = LinearLayoutManager(activity)
+        autoCompleteList.itemAnimator = DefaultItemAnimator()
+        autoCompleteList.adapter = autocompleteAdapter
+      }
+    }
+
+    messageHistory.itemAnimator = DefaultItemAnimator()
+    messageHistory.layoutManager = LinearLayoutManager(requireContext())
+    val messageHistoryAdapter = MessageHistoryAdapter()
+    messageHistoryAdapter.setOnItemClickListener { text ->
+      editorHelper.replaceText(text)
+      historyPanel.panelState = SlidingUpPanelLayout.PanelState.COLLAPSED
+    }
+    messageHistory.adapter = messageHistoryAdapter
+    viewModel.recentlySentMessages_liveData.observe(
+      this, Observer(messageHistoryAdapter::submitList)
+    )
+
+    fun send() {
+      if (chatline.text.isNotBlank()) {
+        val lines = chatline.text.lineSequence().map {
+          it.toString() to ircFormatSerializer.toEscapeCodes(SpannableString(it))
+        }
+
+        viewModel.session { sessionOptional ->
+          val session = sessionOptional.orNull()
+          viewModel.buffer { bufferId ->
+            session?.bufferSyncer?.bufferInfo(bufferId)?.also { bufferInfo ->
+              val output = mutableListOf<IAliasManager.Command>()
+              for ((stripped, formatted) in lines) {
+                viewModel.addRecentlySentMessage(stripped)
+                session.aliasManager?.processInput(bufferInfo, formatted, output)
+              }
+              for (command in output) {
+                session.rpcHandler?.sendInput(command.buffer, command.message)
+              }
+            }
+          }
+        }
+      }
+      chatline.setText("")
+    }
+
+    send.setOnClickListener { send() }
+
+    tabComplete.visibleIf(autoCompleteSettings.button)
+    tabComplete.setOnClickListener {
+      autoCompleteHelper.autoComplete()
+    }
+
+    toolbar.inflateMenu(R.menu.editor)
+    toolbar.menu.retint(requireActivity())
+    toolbar.setOnMenuItemClickListener {
+      when (it.itemId) {
+        R.id.action_input_history -> {
+          historyPanel.panelState = SlidingUpPanelLayout.PanelState.EXPANDED
+          true
+        }
+        else                      -> false
+      }
+    }
+
+    return view
+  }
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/Editor.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/Editor.kt
deleted file mode 100644
index cbcbd45ec..000000000
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/Editor.kt
+++ /dev/null
@@ -1,610 +0,0 @@
-package de.kuschku.quasseldroid.ui.chat.input
-
-import android.arch.lifecycle.Observer
-import android.graphics.Typeface
-import android.support.annotation.ColorInt
-import android.support.annotation.StringRes
-import android.support.v4.app.FragmentActivity
-import android.support.v7.app.AppCompatActivity
-import android.support.v7.widget.*
-import android.text.Editable
-import android.text.InputType
-import android.text.SpannableString
-import android.text.TextWatcher
-import android.text.style.ForegroundColorSpan
-import android.text.style.StyleSpan
-import android.view.*
-import android.view.inputmethod.EditorInfo
-import butterknife.BindView
-import butterknife.ButterKnife
-import de.kuschku.libquassel.protocol.Buffer_Type
-import de.kuschku.libquassel.quassel.syncables.IrcChannel
-import de.kuschku.libquassel.util.IrcUserUtils
-import de.kuschku.libquassel.util.flag.hasFlag
-import de.kuschku.libquassel.util.helpers.value
-import de.kuschku.quasseldroid.R
-import de.kuschku.quasseldroid.settings.AppearanceSettings
-import de.kuschku.quasseldroid.settings.AutoCompleteSettings
-import de.kuschku.quasseldroid.settings.MessageSettings
-import de.kuschku.quasseldroid.ui.chat.ChatActivity
-import de.kuschku.quasseldroid.util.helper.*
-import de.kuschku.quasseldroid.util.irc.format.IrcFormatDeserializer
-import de.kuschku.quasseldroid.util.ui.ColorChooserDialog
-import de.kuschku.quasseldroid.util.ui.EditTextSelectionChange
-import de.kuschku.quasseldroid.util.ui.TextDrawable
-import de.kuschku.quasseldroid.viewmodel.QuasselViewModel
-import de.kuschku.quasseldroid.viewmodel.data.AutoCompleteItem
-import de.kuschku.quasseldroid.viewmodel.data.BufferStatus
-import io.reactivex.subjects.BehaviorSubject
-
-class Editor(
-  // Contexts
-  activity: AppCompatActivity,
-  // LiveData
-  private val viewModel: QuasselViewModel,
-  // Views
-  val chatline: EditTextSelectionChange,
-  send: AppCompatImageButton,
-  tabComplete: AppCompatImageButton,
-  autoCompleteLists: List<RecyclerView>,
-  formattingToolbar: Toolbar,
-  // Helpers
-  private val ircFormatDeserializer: IrcFormatDeserializer,
-  // Settings
-  private val appearanceSettings: AppearanceSettings,
-  private val autoCompleteSettings: AutoCompleteSettings,
-  private val messageSettings: MessageSettings
-  // Listeners
-) : ActionMenuView.OnMenuItemClickListener, Toolbar.OnMenuItemClickListener {
-  private var sendListener: ((Sequence<Pair<CharSequence, String>>) -> Unit)? = null
-  private var panelStateListener: ((Boolean) -> Unit)? = null
-
-  fun setOnSendListener(listener: (Sequence<Pair<CharSequence, String>>) -> Unit) {
-    this.sendListener = listener
-  }
-
-  fun setOnPanelStateListener(listener: (Boolean) -> Unit) {
-    this.panelStateListener = listener
-  }
-
-  override fun onMenuItemClick(item: MenuItem?) = when (item?.itemId) {
-    R.id.action_input_history -> {
-      panelStateListener?.invoke(true)
-      true
-    }
-    else                      -> false
-  }
-
-  private val senderColors = activity.theme.styledAttributes(
-    R.attr.senderColor0, R.attr.senderColor1, R.attr.senderColor2, R.attr.senderColor3,
-    R.attr.senderColor4, R.attr.senderColor5, R.attr.senderColor6, R.attr.senderColor7,
-    R.attr.senderColor8, R.attr.senderColor9, R.attr.senderColorA, R.attr.senderColorB,
-    R.attr.senderColorC, R.attr.senderColorD, R.attr.senderColorE, R.attr.senderColorF
-  ) {
-    IntArray(16) {
-      getColor(it, 0)
-    }
-  }
-
-  private val lastWord = BehaviorSubject.createDefault(Pair("", IntRange.EMPTY))
-  private val textWatcher = object : TextWatcher {
-    override fun afterTextChanged(s: Editable?) {
-      val previous = autocompletionState
-      val next = if (previous != null && s != null) {
-        val suffix = if (previous.range.start == 0) ": " else " "
-        val end = Math.min(
-          s.length, previous.range.start + previous.completion.name.length + suffix.length
-        )
-        val sequence = if (end < previous.range.start) ""
-        else s.substring(previous.range.start, end)
-        if (sequence == previous.completion.name + suffix) {
-          previous.originalWord to (previous.range.start until end)
-        } else {
-          autocompletionState = null
-          s.lastWordIndices(chatline.selectionStart, onlyBeforeCursor = true)?.let { indices ->
-            s.substring(indices) to indices
-          }
-        }
-      } else {
-        s?.lastWordIndices(chatline.selectionStart, onlyBeforeCursor = true)?.let { indices ->
-          s.substring(indices) to indices
-        }
-      }
-
-      lastWord.onNext(next ?: Pair("", IntRange.EMPTY))
-
-      updateButtons(chatline.selection)
-    }
-
-    override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
-    override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
-  }
-
-  val formatHandler = FormatHandler(chatline)
-
-  private var autocompletionState: ChatActivity.AutoCompletionState? = null
-
-  @BindView(R.id.action_format_bold)
-  lateinit var boldButton: View
-
-  @BindView(R.id.action_format_italic)
-  lateinit var italicButton: View
-
-  @BindView(R.id.action_format_underline)
-  lateinit var underlineButton: View
-
-  @BindView(R.id.action_format_strikethrough)
-  lateinit var strikethroughButton: View
-
-  @BindView(R.id.action_format_monospace)
-  lateinit var monospaceButton: View
-
-  @BindView(R.id.action_format_foreground)
-  lateinit var foregroundButton: View
-
-  @BindView(R.id.action_format_foreground_preview)
-  lateinit var foregroundButtonPreview: View
-
-  @BindView(R.id.action_format_background)
-  lateinit var backgroundButton: View
-
-  @BindView(R.id.action_format_background_preview)
-  lateinit var backgroundButtonPreview: View
-
-  @BindView(R.id.action_format_clear)
-  lateinit var clearButton: View
-
-  init {
-    send.setOnClickListener {
-      send()
-    }
-
-    chatline.imeOptions = when (appearanceSettings.inputEnter) {
-      AppearanceSettings.InputEnterMode.EMOJI -> listOf(
-        EditorInfo.IME_ACTION_NONE,
-        EditorInfo.IME_FLAG_NO_EXTRACT_UI
-      )
-      AppearanceSettings.InputEnterMode.SEND  -> listOf(
-        EditorInfo.IME_ACTION_SEND,
-        EditorInfo.IME_FLAG_NO_EXTRACT_UI
-      )
-    }.fold(0, Int::or)
-
-    val autocompleteAdapter = AutoCompleteAdapter(
-      messageSettings,
-      // This is still broken when mixing tab complete and UI auto complete
-      formatHandler::autoComplete
-    )
-
-    viewModel.autoCompleteData.toLiveData().observe(activity, Observer {
-      val query = it?.first ?: ""
-      val shouldShowResults = (autoCompleteSettings.auto && query.length >= 3) ||
-                              (autoCompleteSettings.prefix && query.startsWith('@')) ||
-                              (autoCompleteSettings.prefix && query.startsWith('#'))
-      val list = if (shouldShowResults) it?.second.orEmpty() else emptyList()
-      autocompleteAdapter.submitList(list.map {
-        if (it is AutoCompleteItem.UserItem) {
-          val nickName = it.nick
-          val senderColorIndex = IrcUserUtils.senderColor(nickName)
-          val rawInitial = nickName.trimStart('-',
-                                              '_',
-                                              '[',
-                                              ']',
-                                              '{',
-                                              '}',
-                                              '|',
-                                              '`',
-                                              '^',
-                                              '.',
-                                              '\\')
-                             .firstOrNull() ?: nickName.firstOrNull()
-          val initial = rawInitial?.toUpperCase().toString()
-          val senderColor = senderColors[senderColorIndex]
-
-          fun formatNick(nick: CharSequence): CharSequence {
-            val spannableString = SpannableString(nick)
-            spannableString.setSpan(
-              ForegroundColorSpan(senderColor),
-              0,
-              nick.length,
-              SpannableString.SPAN_INCLUSIVE_EXCLUSIVE
-            )
-            spannableString.setSpan(
-              StyleSpan(Typeface.BOLD),
-              0,
-              nick.length,
-              SpannableString.SPAN_INCLUSIVE_EXCLUSIVE
-            )
-            return spannableString
-          }
-
-          it.copy(
-            displayNick = formatNick(it.nick),
-            fallbackDrawable = TextDrawable.builder().buildRound(initial, senderColor),
-            modes = when (messageSettings.showPrefix) {
-              MessageSettings.ShowPrefixMode.ALL ->
-                it.modes
-              else                               ->
-                it.modes.substring(0, Math.min(it.modes.length, 1))
-            },
-            realname = ircFormatDeserializer.formatString(
-              activity, it.realname.toString(), messageSettings.colorizeMirc
-            )
-          )
-        } else {
-          it
-        }
-      })
-    })
-
-    if (autoCompleteSettings.prefix || autoCompleteSettings.auto) {
-      for (autoCompleteList in autoCompleteLists) {
-        autoCompleteList.layoutManager = LinearLayoutManager(activity)
-        autoCompleteList.itemAnimator = DefaultItemAnimator()
-        autoCompleteList.adapter = autocompleteAdapter
-      }
-    }
-
-    if (autoCompleteSettings.doubleTap) {
-      val gestureDetector = GestureDetector(
-        chatline.context, object : GestureDetector.SimpleOnGestureListener() {
-        override fun onDoubleTap(e: MotionEvent?): Boolean {
-          autoComplete()
-          return true
-        }
-
-        override fun onDoubleTapEvent(e: MotionEvent?): Boolean {
-          return true
-        }
-      })
-      chatline.setOnTouchListener { _, event ->
-        gestureDetector.onTouchEvent(event)
-      }
-    }
-
-    tabComplete.visibleIf(autoCompleteSettings.button)
-    tabComplete.setOnClickListener {
-      autoComplete()
-    }
-
-    viewModel.lastWord.onNext(lastWord)
-
-    activity.menuInflater.inflate(R.menu.editor, formattingToolbar.menu)
-    formattingToolbar.menu.retint(activity)
-    formattingToolbar.setOnMenuItemClickListener(this)
-
-    ButterKnife.bind(this, formattingToolbar)
-
-    boldButton.setOnClickListener {
-      formatHandler.toggleBold(chatline.selection)
-      updateButtons(chatline.selection)
-    }
-    TooltipCompat.setTooltipText(boldButton, boldButton.contentDescription)
-
-    italicButton.setOnClickListener {
-      formatHandler.toggleItalic(chatline.selection)
-      updateButtons(chatline.selection)
-    }
-    TooltipCompat.setTooltipText(italicButton, italicButton.contentDescription)
-
-    underlineButton.setOnClickListener {
-      formatHandler.toggleUnderline(chatline.selection)
-      updateButtons(chatline.selection)
-    }
-    TooltipCompat.setTooltipText(underlineButton, underlineButton.contentDescription)
-
-    strikethroughButton.setOnClickListener {
-      formatHandler.toggleStrikethrough(chatline.selection)
-      updateButtons(chatline.selection)
-    }
-    TooltipCompat.setTooltipText(strikethroughButton, strikethroughButton.contentDescription)
-
-    monospaceButton.setOnClickListener {
-      formatHandler.toggleMonospace(chatline.selection)
-      updateButtons(chatline.selection)
-    }
-    TooltipCompat.setTooltipText(monospaceButton, monospaceButton.contentDescription)
-
-    foregroundButton.setOnClickListener {
-      showColorChooser(
-        activity,
-        R.string.label_foreground,
-        formatHandler.foregroundColor(chatline.selection),
-        formatHandler.defaultForegroundColor
-      ) { color ->
-        formatHandler.toggleForeground(chatline.selection, color,
-                                       formatHandler.mircColorMap[color])
-        updateButtons(chatline.selection)
-      }
-    }
-    TooltipCompat.setTooltipText(foregroundButton, foregroundButton.contentDescription)
-
-    backgroundButton.setOnClickListener {
-      showColorChooser(
-        activity,
-        R.string.label_background,
-        formatHandler.backgroundColor(chatline.selection),
-        formatHandler.defaultBackgroundColor
-      ) { color ->
-        formatHandler.toggleBackground(chatline.selection, color,
-                                       formatHandler.mircColorMap[color])
-        updateButtons(chatline.selection)
-      }
-    }
-    TooltipCompat.setTooltipText(backgroundButton, backgroundButton.contentDescription)
-
-    clearButton.setOnClickListener {
-      formatHandler.clearFormatting(chatline.selection)
-      updateButtons(chatline.selection)
-    }
-    TooltipCompat.setTooltipText(clearButton, clearButton.contentDescription)
-
-    chatline.setOnEditorActionListener { _, actionId, event: KeyEvent? ->
-      when (actionId) {
-        EditorInfo.IME_ACTION_SEND,
-        EditorInfo.IME_ACTION_DONE -> {
-          if (event?.action == KeyEvent.ACTION_DOWN) send()
-          true
-        }
-        else                       -> false
-      }
-    }
-
-    chatline.setOnKeyListener { _, keyCode, event: KeyEvent? ->
-      if (event?.action == KeyEvent.ACTION_DOWN) {
-        if (event.isCtrlPressed && !event.isAltPressed) when (keyCode) {
-          KeyEvent.KEYCODE_B -> {
-            formatHandler.toggleBold(chatline.selection)
-            updateButtons(chatline.selection)
-            true
-          }
-          KeyEvent.KEYCODE_I -> {
-            formatHandler.toggleItalic(chatline.selection)
-            updateButtons(chatline.selection)
-            true
-          }
-          KeyEvent.KEYCODE_U -> {
-            formatHandler.toggleUnderline(chatline.selection)
-            updateButtons(chatline.selection)
-            true
-          }
-          else               -> false
-        } else when (keyCode) {
-          KeyEvent.KEYCODE_ENTER,
-          KeyEvent.KEYCODE_NUMPAD_ENTER -> if (event.isShiftPressed) {
-            false
-          } else {
-            send()
-            true
-          }
-          KeyEvent.KEYCODE_TAB          -> {
-            if (!event.isAltPressed && !event.isCtrlPressed) {
-              autoComplete(event.isShiftPressed)
-              true
-            } else {
-              false
-            }
-          }
-          else                          -> false
-        }
-      } else if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER) {
-        !(event?.isShiftPressed ?: false)
-      } else {
-        false
-      }
-    }
-  }
-
-  private fun showColorChooser(
-    activity: FragmentActivity,
-    @StringRes title: Int,
-    @ColorInt preselect: Int?,
-    @ColorInt default: Int,
-    f: (Int?) -> Unit
-  ) {
-    var selectedColor: Int? = preselect
-    ColorChooserDialog.Builder(chatline.context, title)
-      .customColors(intArrayOf(
-        formatHandler.mircColors[0],
-        formatHandler.mircColors[1],
-        formatHandler.mircColors[2],
-        formatHandler.mircColors[3],
-        formatHandler.mircColors[4],
-        formatHandler.mircColors[5],
-        formatHandler.mircColors[6],
-        formatHandler.mircColors[7],
-        formatHandler.mircColors[8],
-        formatHandler.mircColors[9],
-        formatHandler.mircColors[10],
-        formatHandler.mircColors[11],
-        formatHandler.mircColors[12],
-        formatHandler.mircColors[13],
-        formatHandler.mircColors[14],
-        formatHandler.mircColors[15]
-      ), null)
-      .doneButton(R.string.label_select)
-      .cancelButton(R.string.label_reset)
-      .backButton(R.string.label_back)
-      .customButton(R.string.label_colors_custom)
-      .presetsButton(R.string.label_colors_mirc)
-      .preselect(preselect ?: default)
-      .dynamicButtonColor(false)
-      .allowUserColorInputAlpha(false)
-      .callback(object : ColorChooserDialog.ColorCallback {
-        override fun onColorReset(dialog: ColorChooserDialog) {
-          selectedColor = null
-        }
-
-        override fun onColorSelection(dialog: ColorChooserDialog, color: Int) {
-          selectedColor = color
-        }
-
-        override fun onColorChooserDismissed(dialog: ColorChooserDialog) {
-          f(selectedColor)
-        }
-      })
-      .show(activity)
-  }
-
-  fun updateButtons(selection: IntRange) {
-    boldButton.isSelected = formatHandler.isBold(selection)
-    italicButton.isSelected = formatHandler.isItalic(selection)
-    underlineButton.isSelected = formatHandler.isUnderline(selection)
-    strikethroughButton.isSelected = formatHandler.isStrikethrough(selection)
-    monospaceButton.isSelected = formatHandler.isMonospace(selection)
-    foregroundButtonPreview.setBackgroundColor(formatHandler.foregroundColor(selection)
-                                               ?: formatHandler.defaultForegroundColor)
-    backgroundButtonPreview.setBackgroundColor(formatHandler.backgroundColor(selection)
-                                               ?: formatHandler.defaultBackgroundColor)
-  }
-
-  fun onStart() {
-    chatline.addTextChangedListener(textWatcher)
-    chatline.setSelectionChangeListener(::updateButtons)
-  }
-
-  fun onStop() {
-    chatline.removeTextChangedListener(textWatcher)
-    chatline.removeSelectionChangeListener()
-  }
-
-  private fun send() {
-    if (rawText.isNotBlank()) {
-      sendListener?.invoke(strippedText.lineSequence().zip(formattedText))
-    }
-    chatline.setText("")
-  }
-
-  fun setMultiLine(enabled: Boolean) {
-    val selectionStart = chatline.selectionStart
-    val selectionEnd = chatline.selectionEnd
-
-    if (enabled) {
-      chatline.inputType = chatline.inputType and InputType.TYPE_TEXT_FLAG_MULTI_LINE.inv()
-    } else {
-      chatline.inputType = chatline.inputType or InputType.TYPE_TEXT_FLAG_MULTI_LINE
-    }
-
-    chatline.setSelection(selectionStart, selectionEnd)
-  }
-
-  private fun autoCompleteDataFull(): List<AutoCompleteItem> {
-    return viewModel.rawAutoCompleteData.value?.let { (sessionOptional, id, lastWord) ->
-      val session = sessionOptional.orNull()
-      val bufferInfo = session?.bufferSyncer?.bufferInfo(id)
-      session?.networks?.let { networks ->
-        session.bufferSyncer?.bufferInfos()?.let { infos ->
-          if (bufferInfo?.type?.hasFlag(Buffer_Type.ChannelBuffer) == true) {
-            val network = networks[bufferInfo.networkId]
-            network?.ircChannel(
-              bufferInfo.bufferName
-            )?.let { ircChannel ->
-              val users = ircChannel.ircUsers()
-              val buffers = infos
-                .filter {
-                  it.type.toInt() == Buffer_Type.ChannelBuffer.toInt()
-                }.mapNotNull { info ->
-                  networks[info.networkId]?.let { info to it }
-                }.map { (info, network) ->
-                  val channel = network.ircChannel(info.bufferName) ?: IrcChannel.NULL
-                  AutoCompleteItem.ChannelItem(
-                    info = info,
-                    network = network.networkInfo(),
-                    bufferStatus = when (channel) {
-                      IrcChannel.NULL -> BufferStatus.OFFLINE
-                      else            -> BufferStatus.ONLINE
-                    },
-                    description = channel.topic()
-                  )
-                }
-              val nicks = users.map { user ->
-                val userModes = ircChannel.userModes(user)
-                val prefixModes = network.prefixModes()
-
-                val lowestMode = userModes.mapNotNull(prefixModes::indexOf).min()
-                                 ?: prefixModes.size
-
-                AutoCompleteItem.UserItem(
-                  user.nick(),
-                  network.modesToPrefixes(userModes),
-                  lowestMode,
-                  user.realName(),
-                  user.isAway(),
-                  network.support("CASEMAPPING"),
-                  Regex("[us]id(\\d+)").matchEntire(user.user())?.groupValues?.lastOrNull()?.let {
-                    "https://www.irccloud.com/avatar-redirect/$it"
-                  }
-                )
-              }
-
-              val ignoredStartingCharacters = charArrayOf(
-                '-', '_', '[', ']', '{', '}', '|', '`', '^', '.', '\\', '@'
-              )
-
-              (nicks + buffers).filter {
-                it.name.trimStart(*ignoredStartingCharacters)
-                  .startsWith(
-                    lastWord.first.trimStart(*ignoredStartingCharacters),
-                    ignoreCase = true
-                  )
-              }.sorted()
-            }
-          } else null
-        }
-      }
-    } ?: emptyList()
-  }
-
-  private fun autoComplete(reverse: Boolean = false) {
-    val originalWord = lastWord.value
-
-    val previous = autocompletionState
-    if (!originalWord.second.isEmpty()) {
-      val autoCompletedWords = autoCompleteDataFull()
-      if (previous != null && lastWord.value.first == previous.originalWord && lastWord.value.second.start == previous.range.start) {
-        val previousIndex = autoCompletedWords.indexOf(previous.completion)
-        val autoCompletedWord = if (previousIndex != -1) {
-          val change = if (reverse) -1 else +1
-          val newIndex = (previousIndex + change + autoCompletedWords.size) % autoCompletedWords.size
-
-          autoCompletedWords[newIndex]
-        } else {
-          autoCompletedWords.firstOrNull()
-        }
-        if (autoCompletedWord != null) {
-          val newState = ChatActivity.AutoCompletionState(
-            previous.originalWord,
-            originalWord.second,
-            previous.completion,
-            autoCompletedWord
-          )
-          autocompletionState = newState
-          formatHandler.autoComplete(newState)
-        } else {
-          autocompletionState = null
-        }
-      } else {
-        val autoCompletedWord = autoCompletedWords.firstOrNull()
-        if (autoCompletedWord != null) {
-          val newState = ChatActivity.AutoCompletionState(
-            originalWord.first,
-            originalWord.second,
-            null,
-            autoCompletedWord
-          )
-          autocompletionState = newState
-          formatHandler.autoComplete(newState)
-        } else {
-          autocompletionState = null
-        }
-      }
-    }
-  }
-
-  val formattedText: Sequence<String>
-    get() = formatHandler.formattedText
-  val rawText: CharSequence
-    get() = formatHandler.rawText
-  val strippedText: CharSequence
-    get() = formatHandler.strippedText
-}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/EditorHelper.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/EditorHelper.kt
new file mode 100644
index 000000000..a16dd2f1e
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/EditorHelper.kt
@@ -0,0 +1,206 @@
+package de.kuschku.quasseldroid.ui.chat.input
+
+import android.support.annotation.ColorInt
+import android.support.annotation.StringRes
+import android.support.v4.app.FragmentActivity
+import android.text.Editable
+import android.text.TextWatcher
+import android.view.KeyEvent
+import android.view.inputmethod.EditorInfo
+import de.kuschku.quasseldroid.R
+import de.kuschku.quasseldroid.settings.AppearanceSettings
+import de.kuschku.quasseldroid.settings.AutoCompleteSettings
+import de.kuschku.quasseldroid.util.helper.lastWordIndices
+import de.kuschku.quasseldroid.util.helper.styledAttributes
+import de.kuschku.quasseldroid.util.ui.ColorChooserDialog
+import io.reactivex.subjects.BehaviorSubject
+
+class EditorHelper(
+  private val activity: FragmentActivity,
+  private val editText: RichEditText,
+  private val toolbar: RichToolbar,
+  private val autoCompleteHelper: AutoCompleteHelper,
+  autoCompleteSettings: AutoCompleteSettings,
+  appearanceSettings: AppearanceSettings
+) {
+  private var enterListener: (() -> Unit)? = null
+
+  private val mircColors = editText.context.theme.styledAttributes(
+    R.attr.mircColor00, R.attr.mircColor01, R.attr.mircColor02, R.attr.mircColor03,
+    R.attr.mircColor04, R.attr.mircColor05, R.attr.mircColor06, R.attr.mircColor07,
+    R.attr.mircColor08, R.attr.mircColor09, R.attr.mircColor10, R.attr.mircColor11,
+    R.attr.mircColor12, R.attr.mircColor13, R.attr.mircColor14, R.attr.mircColor15
+  ) {
+    IntArray(length(), { getColor(it, 0) })
+  }
+
+  private val defaultForegroundColor = editText.context.theme.styledAttributes(R.attr.colorForeground) {
+    getColor(0, 0)
+  }
+
+  private val defaultBackgroundColor = editText.context.theme.styledAttributes(R.attr.colorBackground) {
+    getColor(0, 0)
+  }
+
+  val lastWord = BehaviorSubject.createDefault(Pair("", IntRange.EMPTY))
+  private val textWatcher = object : TextWatcher {
+    override fun afterTextChanged(s: Editable?) {
+      val previous = autoCompleteHelper.autoCompletionState
+      val next = if (previous != null && s != null) {
+        val suffix = if (previous.range.start == 0) ": " else " "
+        val end = Math.min(
+          s.length, previous.range.start + previous.completion.name.length + suffix.length
+        )
+        val sequence = if (end < previous.range.start) ""
+        else s.substring(previous.range.start, end)
+        if (sequence == previous.completion.name + suffix) {
+          previous.originalWord to (previous.range.start until end)
+        } else {
+          autoCompleteHelper.autoCompletionState = null
+          s.lastWordIndices(editText.selectionStart, onlyBeforeCursor = true)?.let { indices ->
+            s.substring(indices) to indices
+          }
+        }
+      } else {
+        s?.lastWordIndices(editText.selectionStart, onlyBeforeCursor = true)?.let { indices ->
+          s.substring(indices) to indices
+        }
+      }
+
+      lastWord.onNext(next ?: Pair("", IntRange.EMPTY))
+    }
+
+    override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
+    override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
+  }
+
+  init {
+    toolbar.setFormattingListener(object : RichToolbar.FormattingListener {
+      override fun onBold() = editText.toggleBold()
+      override fun onItalic() = editText.toggleItalic()
+      override fun onUnderline() = editText.toggleUnderline()
+      override fun onStrikethrough() = editText.toggleStrikethrough()
+      override fun onMonospace() = editText.toggleMonospace()
+      override fun onForeground() = showColorChooser(R.string.label_foreground,
+                                                     editText.foregroundColor(),
+                                                     defaultForegroundColor) {
+        editText.toggleForeground(color = it)
+      }
+
+      override fun onBackground() = showColorChooser(R.string.label_background,
+                                                     editText.backgroundColor(),
+                                                     defaultBackgroundColor) {
+        editText.toggleBackground(color = it)
+      }
+
+      override fun onClear() = editText.clearFormatting()
+    })
+    autoCompleteHelper.setAutocompleteListener(editText::autoComplete)
+    editText.setFormattingListener { bold, italic, underline, strikethrough, monospace, foreground, background ->
+      toolbar.update(
+        bold,
+        italic,
+        underline,
+        strikethrough,
+        monospace,
+        foreground ?: defaultForegroundColor,
+        background ?: defaultBackgroundColor
+      )
+    }
+    editText.addTextChangedListener(textWatcher)
+    editText.setOnKeyListener { _, keyCode, event: KeyEvent? ->
+      if (event?.action == KeyEvent.ACTION_DOWN) {
+        if (event.isCtrlPressed && !event.isAltPressed) when (keyCode) {
+          KeyEvent.KEYCODE_B -> {
+            editText.toggleBold()
+            true
+          }
+          KeyEvent.KEYCODE_I -> {
+            editText.toggleItalic()
+            true
+          }
+          KeyEvent.KEYCODE_U -> {
+            editText.toggleUnderline()
+            true
+          }
+          else               -> false
+        } else when (keyCode) {
+          KeyEvent.KEYCODE_ENTER,
+          KeyEvent.KEYCODE_NUMPAD_ENTER -> if (event.isShiftPressed) {
+            false
+          } else {
+            enterListener?.invoke()
+            true
+          }
+          KeyEvent.KEYCODE_TAB          -> {
+            if (!event.isAltPressed && !event.isCtrlPressed) {
+              autoCompleteHelper.autoComplete(event.isShiftPressed)
+              true
+            } else {
+              false
+            }
+          }
+          else                          -> false
+        }
+      } else if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER) {
+        !(event?.isShiftPressed ?: false)
+      } else {
+        false
+      }
+    }
+
+    if (autoCompleteSettings.doubleTap) {
+      editText.setDoubleClickListener {
+        autoCompleteHelper.autoComplete()
+      }
+    }
+
+    editText.imeOptions = when (appearanceSettings.inputEnter) {
+      AppearanceSettings.InputEnterMode.EMOJI -> listOf(
+        EditorInfo.IME_ACTION_NONE,
+        EditorInfo.IME_FLAG_NO_EXTRACT_UI
+      )
+      AppearanceSettings.InputEnterMode.SEND  -> listOf(
+        EditorInfo.IME_ACTION_SEND,
+        EditorInfo.IME_FLAG_NO_EXTRACT_UI
+      )
+    }.fold(0, Int::or)
+  }
+
+  fun setMultiLine(enabled: Boolean) = editText.setMultiLine(enabled)
+
+  fun replaceText(text: CharSequence?) = editText.replace(text)
+
+  private fun showColorChooser(
+    @StringRes title: Int,
+    @ColorInt preselect: Int?,
+    @ColorInt default: Int,
+    f: (Int?) -> Unit
+  ) {
+    var selectedColor: Int? = preselect
+    ColorChooserDialog.Builder(editText.context, title)
+      .customColors(mircColors, null)
+      .doneButton(R.string.label_select)
+      .cancelButton(R.string.label_reset)
+      .backButton(R.string.label_back)
+      .customButton(R.string.label_colors_custom)
+      .presetsButton(R.string.label_colors_mirc)
+      .preselect(preselect ?: default)
+      .dynamicButtonColor(false)
+      .allowUserColorInputAlpha(false)
+      .callback(object : ColorChooserDialog.ColorCallback {
+        override fun onColorReset(dialog: ColorChooserDialog) {
+          selectedColor = null
+        }
+
+        override fun onColorSelection(dialog: ColorChooserDialog, color: Int) {
+          selectedColor = color
+        }
+
+        override fun onColorChooserDismissed(dialog: ColorChooserDialog) {
+          f(selectedColor)
+        }
+      })
+      .show(activity)
+  }
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/FormatHandler.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/RichEditText.kt
similarity index 51%
rename from app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/FormatHandler.kt
rename to app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/RichEditText.kt
index af1987c58..be0b235b7 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/FormatHandler.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/RichEditText.kt
@@ -1,22 +1,20 @@
 package de.kuschku.quasseldroid.ui.chat.input
 
+import android.content.Context
 import android.graphics.Typeface
 import android.support.annotation.ColorInt
-import android.text.Editable
-import android.text.SpannableString
+import android.text.InputType
 import android.text.Spanned
 import android.text.style.*
-import android.widget.EditText
+import android.util.AttributeSet
 import de.kuschku.quasseldroid.R
-import de.kuschku.quasseldroid.ui.chat.ChatActivity
 import de.kuschku.quasseldroid.util.helper.*
-import de.kuschku.quasseldroid.util.irc.format.IrcFormatSerializer
 import de.kuschku.quasseldroid.util.irc.format.spans.*
+import de.kuschku.quasseldroid.util.ui.DoubleClickHelper
+import de.kuschku.quasseldroid.util.ui.EditTextSelectionChange
 
-class FormatHandler(
-  private val editText: EditText
-) {
-  val mircColors = editText.context.theme.styledAttributes(
+class RichEditText : EditTextSelectionChange {
+  private val mircColors = this.context.theme.styledAttributes(
     R.attr.mircColor00, R.attr.mircColor01, R.attr.mircColor02, R.attr.mircColor03,
     R.attr.mircColor04, R.attr.mircColor05, R.attr.mircColor06, R.attr.mircColor07,
     R.attr.mircColor08, R.attr.mircColor09, R.attr.mircColor10, R.attr.mircColor11,
@@ -43,45 +41,40 @@ class FormatHandler(
     R.attr.mircColor92, R.attr.mircColor93, R.attr.mircColor94, R.attr.mircColor95,
     R.attr.mircColor96, R.attr.mircColor97, R.attr.mircColor98
   ) {
-    (0..98).map { getColor(it, 0) }
+    IntArray(length(), { getColor(it, 0) })
   }
-  val mircColorMap = mircColors.withIndex().map { (key, value) -> key to value }.toMap()
+  private val mircColorMap = mircColors.withIndex().map { (key, value) -> key to value }.toMap()
 
-  val defaultForegroundColor = editText.context.theme.styledAttributes(R.attr.colorForeground) {
-    getColor(0, 0)
+  private var formattingListener: ((Boolean, Boolean, Boolean, Boolean, Boolean, Int?, Int?) -> Unit)? = null
+
+  private val doubleClickHelper = DoubleClickHelper(this)
+
+  constructor(context: Context?) : super(context)
+  constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
+  constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) :
+    super(context, attrs, defStyleAttr)
+
+  init {
+    setSelectionChangeListener(this::selectedFormattingChanged)
+    setOnTouchListener(doubleClickHelper)
   }
 
-  val defaultBackgroundColor = editText.context.theme.styledAttributes(R.attr.colorBackground) {
-    getColor(0, 0)
+  fun setFormattingListener(
+    listener: ((Boolean, Boolean, Boolean, Boolean, Boolean, Int?, Int?) -> Unit)?) {
+    this.formattingListener = listener
   }
 
-  private val serializer = IrcFormatSerializer(editText.context)
-  val formattedText: Sequence<String>
-    get() = editText.text.lineSequence().map { serializer.toEscapeCodes(SpannableString(it)) }
-  val rawText: CharSequence
-    get() = editText.text
-  val strippedText: CharSequence
-    get() = editText.text.let {
-      val text = SpannableString(it)
-      val toRemove = mutableListOf<Any>()
-      for (span in text.getSpans(0, text.length, Any::class.java)) {
-        if ((text.getSpanFlags(span) and Spanned.SPAN_COMPOSING) != 0) {
-          toRemove.add(span)
-        }
-      }
-      for (span in toRemove) {
-        text.removeSpan(span)
-      }
-      text
-    }
+  fun setDoubleClickListener(listener: (() -> Unit)?) {
+    this.doubleClickHelper.doubleClickListener = listener
+  }
 
-  fun isBold(range: IntRange) = editText.text.hasSpans<StyleSpan>(range) {
+  fun isBold(range: IntRange = selection) = this.text.hasSpans<StyleSpan>(range) {
     it.style == Typeface.BOLD || it.style == Typeface.BOLD_ITALIC
   }
 
-  fun toggleBold(range: IntRange, createNew: Boolean = true) {
+  fun toggleBold(range: IntRange = selection, createNew: Boolean = true) {
     val bold = isBold(range)
-    editText.text.removeSpans<StyleSpan, IrcBoldSpan>(range) { span ->
+    this.text.removeSpans<StyleSpan, IrcBoldSpan>(range) { span ->
       when {
         span is IrcBoldSpan         -> span
         span.style == Typeface.BOLD -> IrcBoldSpan()
@@ -90,20 +83,21 @@ class FormatHandler(
     }
 
     if (!bold && createNew) {
-      editText.text.setSpan(
+      this.text.setSpan(
         IrcBoldSpan(), range.start, range.endInclusive + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE
       )
     }
+    selectedFormattingChanged()
   }
 
-  fun isItalic(range: IntRange) = editText.text.hasSpans<StyleSpan>(range) {
+  fun isItalic(range: IntRange = selection) = this.text.hasSpans<StyleSpan>(range) {
     it.style == Typeface.ITALIC || it.style == Typeface.BOLD_ITALIC
   }
 
-  fun toggleItalic(range: IntRange, createNew: Boolean = true) {
+  fun toggleItalic(range: IntRange = selection, createNew: Boolean = true) {
     val italic = isItalic(range)
 
-    editText.text.removeSpans<StyleSpan, IrcItalicSpan>(range) { span ->
+    this.text.removeSpans<StyleSpan, IrcItalicSpan>(range) { span ->
       when {
         span is IrcItalicSpan         -> span
         span.style == Typeface.ITALIC -> IrcItalicSpan()
@@ -112,18 +106,19 @@ class FormatHandler(
     }
 
     if (!italic && createNew) {
-      editText.text.setSpan(
+      this.text.setSpan(
         IrcItalicSpan(), range.start, range.endInclusive + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE
       )
     }
+    selectedFormattingChanged()
   }
 
-  fun isUnderline(range: IntRange) = editText.text.hasSpans<UnderlineSpan>(range)
+  fun isUnderline(range: IntRange = selection) = this.text.hasSpans<UnderlineSpan>(range)
 
-  fun toggleUnderline(range: IntRange, createNew: Boolean = true) {
+  fun toggleUnderline(range: IntRange = selection, createNew: Boolean = true) {
     val underline = isUnderline(range)
 
-    editText.text.removeSpans<UnderlineSpan, IrcUnderlineSpan>(range) { span ->
+    this.text.removeSpans<UnderlineSpan, IrcUnderlineSpan>(range) { span ->
       when (span) {
         is IrcUnderlineSpan -> span
         else                -> IrcUnderlineSpan()
@@ -131,18 +126,19 @@ class FormatHandler(
     }
 
     if (!underline && createNew) {
-      editText.text.setSpan(
+      this.text.setSpan(
         IrcUnderlineSpan(), range.start, range.endInclusive + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE
       )
     }
+    selectedFormattingChanged()
   }
 
-  fun isStrikethrough(range: IntRange) = editText.text.hasSpans<StrikethroughSpan>(range)
+  fun isStrikethrough(range: IntRange = selection) = this.text.hasSpans<StrikethroughSpan>(range)
 
-  fun toggleStrikethrough(range: IntRange, createNew: Boolean = true) {
+  fun toggleStrikethrough(range: IntRange = selection, createNew: Boolean = true) {
     val strikethrough = isStrikethrough(range)
 
-    editText.text.removeSpans<StrikethroughSpan, IrcStrikethroughSpan>(range) { span ->
+    this.text.removeSpans<StrikethroughSpan, IrcStrikethroughSpan>(range) { span ->
       when (span) {
         is IrcStrikethroughSpan -> span
         else                    -> IrcStrikethroughSpan()
@@ -150,21 +146,22 @@ class FormatHandler(
     }
 
     if (!strikethrough && createNew) {
-      editText.text.setSpan(
+      this.text.setSpan(
         IrcStrikethroughSpan(), range.start, range.endInclusive + 1,
         Spanned.SPAN_INCLUSIVE_INCLUSIVE
       )
     }
+    selectedFormattingChanged()
   }
 
-  fun isMonospace(range: IntRange) = editText.text.hasSpans<TypefaceSpan>(range) {
+  fun isMonospace(range: IntRange = selection) = this.text.hasSpans<TypefaceSpan>(range) {
     it.family == "monospace"
   }
 
-  fun toggleMonospace(range: IntRange, createNew: Boolean = true) {
+  fun toggleMonospace(range: IntRange = selection, createNew: Boolean = true) {
     val monospace = isMonospace(range)
 
-    editText.text.removeSpans<TypefaceSpan, IrcMonospaceSpan>(range) { span ->
+    this.text.removeSpans<TypefaceSpan, IrcMonospaceSpan>(range) { span ->
       when {
         span is IrcMonospaceSpan   -> span
         span.family == "monospace" -> IrcMonospaceSpan()
@@ -173,16 +170,20 @@ class FormatHandler(
     }
 
     if (!monospace && createNew) {
-      editText.text.setSpan(
+      this.text.setSpan(
         IrcMonospaceSpan(), range.start, range.endInclusive + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE
       )
     }
+    selectedFormattingChanged()
   }
 
-  fun foregroundColors(range: IntRange) = editText.text.spans<ForegroundColorSpan>(range)
-  fun foregroundColor(range: IntRange) = foregroundColors(range).singleOrNull()?.foregroundColor
-  fun toggleForeground(range: IntRange, @ColorInt color: Int? = null, mircColor: Int? = null) {
-    editText.text.removeSpans<ForegroundColorSpan, IrcForegroundColorSpan<*>>(range) { span ->
+  fun foregroundColors(range: IntRange = selection) = this.text.spans<ForegroundColorSpan>(range)
+  fun foregroundColor(range: IntRange = selection) =
+    foregroundColors(range).singleOrNull()?.foregroundColor
+
+  fun toggleForeground(range: IntRange = selection, @ColorInt color: Int? = null,
+                       mircColor: Int? = null) {
+    this.text.removeSpans<ForegroundColorSpan, IrcForegroundColorSpan<*>>(range) { span ->
       val mirc = mircColorMap[span.foregroundColor]
       when {
         span is IrcForegroundColorSpan<*> -> span
@@ -193,14 +194,14 @@ class FormatHandler(
 
     if (color != null) {
       if (mircColor != null) {
-        editText.text.setSpan(
+        this.text.setSpan(
           IrcForegroundColorSpan.MIRC(mircColor, color),
           range.start,
           range.last + 1,
           Spanned.SPAN_INCLUSIVE_INCLUSIVE
         )
       } else {
-        editText.text.setSpan(
+        this.text.setSpan(
           IrcForegroundColorSpan.HEX(color),
           range.start,
           range.last + 1,
@@ -208,12 +209,16 @@ class FormatHandler(
         )
       }
     }
+    selectedFormattingChanged()
   }
 
-  fun backgroundColors(range: IntRange) = editText.text.spans<BackgroundColorSpan>(range)
-  fun backgroundColor(range: IntRange) = backgroundColors(range).singleOrNull()?.backgroundColor
-  fun toggleBackground(range: IntRange, @ColorInt color: Int? = null, mircColor: Int? = null) {
-    editText.text.removeSpans<BackgroundColorSpan, IrcBackgroundColorSpan<*>>(range) { span ->
+  fun backgroundColors(range: IntRange = selection) = this.text.spans<BackgroundColorSpan>(range)
+  fun backgroundColor(range: IntRange = selection) =
+    backgroundColors(range).singleOrNull()?.backgroundColor
+
+  fun toggleBackground(range: IntRange = selection, @ColorInt color: Int? = null,
+                       mircColor: Int? = null) {
+    this.text.removeSpans<BackgroundColorSpan, IrcBackgroundColorSpan<*>>(range) { span ->
       val mirc = mircColorMap[span.backgroundColor]
       when {
         span is IrcBackgroundColorSpan<*> -> span
@@ -224,14 +229,14 @@ class FormatHandler(
 
     if (color != null) {
       if (mircColor != null) {
-        editText.text.setSpan(
+        this.text.setSpan(
           IrcBackgroundColorSpan.MIRC(mircColor, color),
           range.start,
           range.last + 1,
           Spanned.SPAN_INCLUSIVE_INCLUSIVE
         )
       } else {
-        editText.text.setSpan(
+        this.text.setSpan(
           IrcBackgroundColorSpan.HEX(color),
           range.start,
           range.last + 1,
@@ -239,9 +244,10 @@ class FormatHandler(
         )
       }
     }
+    selectedFormattingChanged()
   }
 
-  fun clearFormatting(range: IntRange) {
+  fun clearFormatting(range: IntRange = selection) {
     toggleBold(range, false)
     toggleItalic(range, false)
     toggleUnderline(range, false)
@@ -251,68 +257,21 @@ class FormatHandler(
     toggleBackground(range, null, null)
   }
 
-  private inline fun <reified U> Spanned.spans(range: IntRange) =
-    getSpans(range.start, range.endInclusive + 1, U::class.java).filter {
-      getSpanFlags(it) and Spanned.SPAN_COMPOSING == 0 &&
-      (getSpanEnd(it) != range.start ||
-       getSpanFlags(it) and 0x02 != 0)
-    }
-
-  private inline fun <reified U> Spanned.spans(range: IntRange, f: (U) -> Boolean) =
-    getSpans(range.start, range.last + 1, U::class.java).filter {
-      f(it) &&
-      getSpanFlags(it) and Spanned.SPAN_COMPOSING == 0 &&
-      (getSpanEnd(it) != range.start ||
-       getSpanFlags(it) and 0x02 != 0)
-    }
+  fun setMultiLine(enabled: Boolean) {
+    val selectionStart = selectionStart
+    val selectionEnd = selectionEnd
 
-  private inline fun <reified U> Spanned.hasSpans(range: IntRange) =
-    getSpans(range.start, range.endInclusive + 1, U::class.java).any {
-      getSpanFlags(it) and Spanned.SPAN_COMPOSING == 0 &&
-      (getSpanEnd(it) != range.start ||
-       getSpanFlags(it) and 0x02 != 0)
-    }
-
-  private inline fun <reified U> Spanned.hasSpans(range: IntRange, f: (U) -> Boolean) =
-    getSpans(range.start, range.last + 1, U::class.java).any {
-      f(it) &&
-      getSpanFlags(it) and Spanned.SPAN_COMPOSING == 0 &&
-      (getSpanEnd(it) != range.start ||
-       getSpanFlags(it) and 0x02 != 0)
+    inputType = if (enabled) {
+      inputType and InputType.TYPE_TEXT_FLAG_MULTI_LINE.inv()
+    } else {
+      inputType or InputType.TYPE_TEXT_FLAG_MULTI_LINE
     }
 
-  private inline fun <reified U, T> Editable.removeSpans(
-    range: IntRange, removeInvalid: Boolean = false, f: (U) -> T?
-  ) where T : Copyable<T> {
-
-    for (raw in getSpans<U>(range.start, range.endInclusive + 1, U::class.java)) {
-      val spanFlags = getSpanFlags(raw)
-      if (spanFlags and Spanned.SPAN_COMPOSING != 0) continue
-
-      val spanEnd = getSpanEnd(raw)
-      val spanStart = getSpanStart(raw)
-
-      val span = f(raw)
-      if (span == null) {
-        if (removeInvalid)
-          removeSpan(raw)
-      } else {
-        removeSpan(raw)
-
-        for (spanRange in spanStart until spanEnd without range) {
-          setSpan(
-            span.copy(),
-            spanRange.start,
-            spanRange.endInclusive + 1,
-            (spanFlags and 0x03.inv()) or 0x01
-          )
-        }
-      }
-    }
+    setSelection(selectionStart, selectionEnd)
   }
 
   fun autoComplete(text: CharSequence) {
-    val range = editText.text.lastWordIndices(editText.selection.start, true)
+    val range = this.text.lastWordIndices(this.selection.start, true)
     val replacement = if (range?.start == 0) {
       "$text: "
     } else {
@@ -320,36 +279,48 @@ class FormatHandler(
     }
 
     if (range != null) {
-      editText.text.replace(range.start, range.endInclusive + 1, replacement)
-      editText.setSelection(range.start + replacement.length)
+      this.text.replace(range.start, range.endInclusive + 1, replacement)
+      this.setSelection(range.start + replacement.length)
     } else {
-      editText.text.append(replacement)
-      editText.setSelection(editText.text.length)
+      this.text.append(replacement)
+      this.setSelection(this.text.length)
     }
   }
 
-  fun autoComplete(item: ChatActivity.AutoCompletionState) {
+  fun autoComplete(item: AutoCompletionState) {
     val suffix = if (item.range.start == 0) ": " else " "
     val replacement = "${item.completion.name}$suffix"
     val previousReplacement = item.lastCompletion?.let { "${item.lastCompletion.name}$suffix" }
 
     if (previousReplacement != null &&
-        editText.text.length >= item.range.start + previousReplacement.length &&
-        editText.text.substring(
+        this.text.length >= item.range.start + previousReplacement.length &&
+        this.text.substring(
           item.range.start, item.range.start + previousReplacement.length
         ) == previousReplacement) {
-      editText.text.replace(
+      this.text.replace(
         item.range.start, item.range.start + previousReplacement.length, replacement
       )
-      editText.setSelection(item.range.start + replacement.length)
+      this.setSelection(item.range.start + replacement.length)
     } else {
-      editText.text.replace(item.range.start, item.range.endInclusive + 1, replacement)
-      editText.setSelection(item.range.start + replacement.length)
+      this.text.replace(item.range.start, item.range.endInclusive + 1, replacement)
+      this.setSelection(item.range.start + replacement.length)
     }
   }
 
   fun replace(text: CharSequence?) {
-    editText.setText(text)
-    editText.setSelection(editText.text.length)
+    this.setText(text)
+    this.setSelection(this.text.length)
+  }
+
+  private fun selectedFormattingChanged(range: IntRange = selection) {
+    formattingListener?.invoke(
+      isBold(range),
+      isItalic(range),
+      isUnderline(range),
+      isStrikethrough(range),
+      isMonospace(range),
+      foregroundColor(range),
+      backgroundColor(range)
+    )
   }
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/RichToolbar.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/RichToolbar.kt
new file mode 100644
index 000000000..a952197a1
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/RichToolbar.kt
@@ -0,0 +1,108 @@
+package de.kuschku.quasseldroid.ui.chat.input
+
+import android.content.Context
+import android.support.annotation.ColorInt
+import android.support.v7.widget.Toolbar
+import android.support.v7.widget.TooltipCompat
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
+import butterknife.BindView
+import butterknife.ButterKnife
+import de.kuschku.quasseldroid.R
+
+class RichToolbar : Toolbar {
+  @BindView(R.id.action_format_bold)
+  lateinit var boldButton: View
+
+  @BindView(R.id.action_format_italic)
+  lateinit var italicButton: View
+
+  @BindView(R.id.action_format_underline)
+  lateinit var underlineButton: View
+
+  @BindView(R.id.action_format_strikethrough)
+  lateinit var strikethroughButton: View
+
+  @BindView(R.id.action_format_monospace)
+  lateinit var monospaceButton: View
+
+  @BindView(R.id.action_format_foreground)
+  lateinit var foregroundButton: View
+
+  @BindView(R.id.action_format_foreground_preview)
+  lateinit var foregroundButtonPreview: View
+
+  @BindView(R.id.action_format_background)
+  lateinit var backgroundButton: View
+
+  @BindView(R.id.action_format_background_preview)
+  lateinit var backgroundButtonPreview: View
+
+  @BindView(R.id.action_format_clear)
+  lateinit var clearButton: View
+
+  private var listener: FormattingListener? = null
+
+  constructor(context: Context?) : super(context)
+  constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
+  constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) :
+    super(context, attrs, defStyleAttr)
+
+  init {
+    LayoutInflater.from(context).inflate(R.layout.widget_formatting, this, true)
+    ButterKnife.bind(this)
+
+    TooltipCompat.setTooltipText(boldButton, boldButton.contentDescription)
+    TooltipCompat.setTooltipText(italicButton, italicButton.contentDescription)
+    TooltipCompat.setTooltipText(underlineButton, underlineButton.contentDescription)
+    TooltipCompat.setTooltipText(strikethroughButton, strikethroughButton.contentDescription)
+    TooltipCompat.setTooltipText(monospaceButton, monospaceButton.contentDescription)
+    TooltipCompat.setTooltipText(foregroundButton, foregroundButton.contentDescription)
+    TooltipCompat.setTooltipText(backgroundButton, backgroundButton.contentDescription)
+    TooltipCompat.setTooltipText(clearButton, clearButton.contentDescription)
+
+    boldButton.setOnClickListener { listener?.onBold() }
+    italicButton.setOnClickListener { listener?.onItalic() }
+    underlineButton.setOnClickListener { listener?.onUnderline() }
+    strikethroughButton.setOnClickListener { listener?.onStrikethrough() }
+    monospaceButton.setOnClickListener { listener?.onMonospace() }
+    foregroundButton.setOnClickListener { listener?.onForeground() }
+    backgroundButton.setOnClickListener { listener?.onBackground() }
+    clearButton.setOnClickListener { listener?.onClear() }
+  }
+
+  fun setFormattingListener(listener: FormattingListener?) {
+    this.listener = listener
+  }
+
+  fun update(
+    bold: Boolean = false,
+    italic: Boolean = false,
+    underline: Boolean = false,
+    strikethrough: Boolean = false,
+    monospace: Boolean = false,
+    @ColorInt foreground: Int,
+    @ColorInt background: Int
+  ) {
+    boldButton.isSelected = bold
+    italicButton.isSelected = italic
+    underlineButton.isSelected = underline
+    strikethroughButton.isSelected = strikethrough
+    monospaceButton.isSelected = monospace
+
+    foregroundButtonPreview.setBackgroundColor(foreground)
+    backgroundButtonPreview.setBackgroundColor(background)
+  }
+
+  interface FormattingListener {
+    fun onBold() = Unit
+    fun onItalic() = Unit
+    fun onUnderline() = Unit
+    fun onStrikethrough() = Unit
+    fun onMonospace() = Unit
+    fun onForeground() = Unit
+    fun onBackground() = Unit
+    fun onClear() = Unit
+  }
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/helper/CharSequenceHelper.kt b/app/src/main/java/de/kuschku/quasseldroid/util/helper/CharSequenceHelper.kt
index 9f8856f87..cefe3ead6 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/util/helper/CharSequenceHelper.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/helper/CharSequenceHelper.kt
@@ -1,5 +1,133 @@
 package de.kuschku.quasseldroid.util.helper
 
+private class DelimitedRangesSequence(
+  private val input: CharSequence,
+  private val startIndex: Int,
+  private val limit: Int,
+  private val getNextMatch: CharSequence.(Int) -> Pair<Int, Int>?
+) : Sequence<IntRange> {
+  override fun iterator(): Iterator<IntRange> = object : Iterator<IntRange> {
+    var nextState: Int = -1 // -1 for unknown, 0 for done, 1 for continue
+    var currentStartIndex: Int = startIndex.coerceIn(0, input.length)
+    var nextSearchIndex: Int = currentStartIndex
+    var nextItem: IntRange? = null
+    var counter: Int = 0
+
+    private fun calcNext() {
+      if (nextSearchIndex < 0) {
+        nextState = 0
+        nextItem = null
+      } else {
+        if (limit > 0 && ++counter >= limit || nextSearchIndex > input.length) {
+          nextItem = currentStartIndex..input.lastIndex
+          nextSearchIndex = -1
+        } else {
+          val match = input.getNextMatch(nextSearchIndex)
+          if (match == null) {
+            nextItem = currentStartIndex..input.lastIndex
+            nextSearchIndex = -1
+          } else {
+            val (index, length) = match
+            nextItem = currentStartIndex until index
+            currentStartIndex = index + length
+            nextSearchIndex = currentStartIndex + if (length == 0) 1 else 0
+          }
+        }
+        nextState = 1
+      }
+    }
+
+    override fun next(): IntRange {
+      if (nextState == -1)
+        calcNext()
+      if (nextState == 0)
+        throw NoSuchElementException()
+      val result = nextItem as IntRange
+      // Clean next to avoid keeping reference on yielded instance
+      nextItem = null
+      nextState = -1
+      return result
+    }
+
+    override fun hasNext(): Boolean {
+      if (nextState == -1)
+        calcNext()
+      return nextState == 1
+    }
+  }
+}
+
+internal fun CharSequence.regionMatchesImpl(thisOffset: Int, other: CharSequence, otherOffset: Int,
+                                            length: Int, ignoreCase: Boolean): Boolean {
+  if ((otherOffset < 0) || (thisOffset < 0) || (thisOffset > this.length - length)
+      || (otherOffset > other.length - length)) {
+    return false
+  }
+
+  for (index in 0 until length) {
+    if (!this[thisOffset + index].equals(other[otherOffset + index], ignoreCase))
+      return false
+  }
+  return true
+}
+
+private fun CharSequence.findAnyOf(strings: Collection<String>, startIndex: Int,
+                                   ignoreCase: Boolean, last: Boolean): Pair<Int, String>? {
+  if (!ignoreCase && strings.size == 1) {
+    val string = strings.single()
+    val index = if (!last) indexOf(string, startIndex) else lastIndexOf(string, startIndex)
+    return if (index < 0) null else index to string
+  }
+
+  val indices = if (!last) startIndex.coerceAtLeast(0)..length else startIndex.coerceAtMost(
+    lastIndex
+  ) downTo 0
+
+  if (this is String) {
+    for (index in indices) {
+      val matchingString = strings.firstOrNull {
+        it.regionMatches(
+          0, this, index, it.length, ignoreCase
+        )
+      }
+      if (matchingString != null)
+        return index to matchingString
+    }
+  } else {
+    for (index in indices) {
+      val matchingString = strings.firstOrNull {
+        it.regionMatchesImpl(
+          0, this, index, it.length, ignoreCase
+        )
+      }
+      if (matchingString != null)
+        return index to matchingString
+    }
+  }
+
+  return null
+}
+
+private fun CharSequence.rangesDelimitedBy(delimiters: Array<out String>, startIndex: Int = 0,
+                                           ignoreCase: Boolean = false,
+                                           limit: Int = 0): Sequence<IntRange> {
+  require(limit >= 0, { "Limit must be non-negative, but was $limit." })
+  val delimitersList = delimiters.asList()
+
+  return DelimitedRangesSequence(
+    this, startIndex, limit, { startIndex ->
+    findAnyOf(
+      delimitersList, startIndex, ignoreCase = ignoreCase, last = false
+    )?.let { it.first to it.second.length }
+  })
+}
+
+fun CharSequence.splitToSequence(vararg delimiters: String, ignoreCase: Boolean = false,
+                                 limit: Int = 0): Sequence<CharSequence> =
+  rangesDelimitedBy(delimiters, ignoreCase = ignoreCase, limit = limit).map { subSequence(it) }
+
+fun CharSequence.lineSequence(): Sequence<CharSequence> = splitToSequence("\r\n", "\n", "\r")
+
 fun CharSequence.lastWord(cursor: Int = this.length,
                           onlyBeforeCursor: Boolean = false): CharSequence {
   return lastWordIndices(cursor, onlyBeforeCursor)?.let { subSequence(it) } ?: ""
@@ -28,4 +156,4 @@ fun CharSequence.lastWordIndices(cursor: Int = this.length,
   } else {
     null
   }
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/helper/EditableHelper.kt b/app/src/main/java/de/kuschku/quasseldroid/util/helper/EditableHelper.kt
new file mode 100644
index 000000000..aa6fdbbc5
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/helper/EditableHelper.kt
@@ -0,0 +1,34 @@
+package de.kuschku.quasseldroid.util.helper
+
+import android.text.Editable
+import android.text.Spanned
+import de.kuschku.quasseldroid.util.irc.format.spans.Copyable
+
+inline fun <reified U, T> Editable.removeSpans(
+  range: IntRange, removeInvalid: Boolean = false, f: (U) -> T?
+) where T : Copyable<T> {
+  for (raw in getSpans<U>(range.start, range.endInclusive + 1, U::class.java)) {
+    val spanFlags = getSpanFlags(raw)
+    if (spanFlags and Spanned.SPAN_COMPOSING != 0) continue
+
+    val spanEnd = getSpanEnd(raw)
+    val spanStart = getSpanStart(raw)
+
+    val span = f(raw)
+    if (span == null) {
+      if (removeInvalid)
+        removeSpan(raw)
+    } else {
+      removeSpan(raw)
+
+      for (spanRange in spanStart until spanEnd without range) {
+        setSpan(
+          span.copy(),
+          spanRange.start,
+          spanRange.endInclusive + 1,
+          (spanFlags and 0x03.inv()) or 0x01
+        )
+      }
+    }
+  }
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/helper/SpannedHelper.kt b/app/src/main/java/de/kuschku/quasseldroid/util/helper/SpannedHelper.kt
index cb1fdb818..fa613a52a 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/util/helper/SpannedHelper.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/helper/SpannedHelper.kt
@@ -1,129 +1,33 @@
 package de.kuschku.quasseldroid.util.helper
 
-private class DelimitedRangesSequence(
-  private val input: CharSequence,
-  private val startIndex: Int,
-  private val limit: Int,
-  private val getNextMatch: CharSequence.(Int) -> Pair<Int, Int>?
-) : Sequence<IntRange> {
-  override fun iterator(): Iterator<IntRange> = object : Iterator<IntRange> {
-    var nextState: Int = -1 // -1 for unknown, 0 for done, 1 for continue
-    var currentStartIndex: Int = startIndex.coerceIn(0, input.length)
-    var nextSearchIndex: Int = currentStartIndex
-    var nextItem: IntRange? = null
-    var counter: Int = 0
+import android.text.Spanned
 
-    private fun calcNext() {
-      if (nextSearchIndex < 0) {
-        nextState = 0
-        nextItem = null
-      } else {
-        if (limit > 0 && ++counter >= limit || nextSearchIndex > input.length) {
-          nextItem = currentStartIndex..input.lastIndex
-          nextSearchIndex = -1
-        } else {
-          val match = input.getNextMatch(nextSearchIndex)
-          if (match == null) {
-            nextItem = currentStartIndex..input.lastIndex
-            nextSearchIndex = -1
-          } else {
-            val (index, length) = match
-            nextItem = currentStartIndex until index
-            currentStartIndex = index + length
-            nextSearchIndex = currentStartIndex + if (length == 0) 1 else 0
-          }
-        }
-        nextState = 1
-      }
-    }
-
-    override fun next(): IntRange {
-      if (nextState == -1)
-        calcNext()
-      if (nextState == 0)
-        throw NoSuchElementException()
-      val result = nextItem as IntRange
-      // Clean next to avoid keeping reference on yielded instance
-      nextItem = null
-      nextState = -1
-      return result
-    }
-
-    override fun hasNext(): Boolean {
-      if (nextState == -1)
-        calcNext()
-      return nextState == 1
-    }
+inline fun <reified U> Spanned.spans(range: IntRange) =
+  getSpans(range.start, range.endInclusive + 1, U::class.java).filter {
+    getSpanFlags(it) and Spanned.SPAN_COMPOSING == 0 &&
+    (getSpanEnd(it) != range.start ||
+     getSpanFlags(it) and 0x02 != 0)
   }
-}
 
-internal fun CharSequence.regionMatchesImpl(thisOffset: Int, other: CharSequence, otherOffset: Int,
-                                            length: Int, ignoreCase: Boolean): Boolean {
-  if ((otherOffset < 0) || (thisOffset < 0) || (thisOffset > this.length - length)
-      || (otherOffset > other.length - length)) {
-    return false
+inline fun <reified U> Spanned.spans(range: IntRange, f: (U) -> Boolean) =
+  getSpans(range.start, range.last + 1, U::class.java).filter {
+    f(it) &&
+    getSpanFlags(it) and Spanned.SPAN_COMPOSING == 0 &&
+    (getSpanEnd(it) != range.start ||
+     getSpanFlags(it) and 0x02 != 0)
   }
 
-  for (index in 0 until length) {
-    if (!this[thisOffset + index].equals(other[otherOffset + index], ignoreCase))
-      return false
+inline fun <reified U> Spanned.hasSpans(range: IntRange) =
+  getSpans(range.start, range.endInclusive + 1, U::class.java).any {
+    getSpanFlags(it) and Spanned.SPAN_COMPOSING == 0 &&
+    (getSpanEnd(it) != range.start ||
+     getSpanFlags(it) and 0x02 != 0)
   }
-  return true
-}
-
-private fun CharSequence.findAnyOf(strings: Collection<String>, startIndex: Int,
-                                   ignoreCase: Boolean, last: Boolean): Pair<Int, String>? {
-  if (!ignoreCase && strings.size == 1) {
-    val string = strings.single()
-    val index = if (!last) indexOf(string, startIndex) else lastIndexOf(string, startIndex)
-    return if (index < 0) null else index to string
-  }
-
-  val indices = if (!last) startIndex.coerceAtLeast(0)..length else startIndex.coerceAtMost(
-    lastIndex
-  ) downTo 0
 
-  if (this is String) {
-    for (index in indices) {
-      val matchingString = strings.firstOrNull {
-        it.regionMatches(
-          0, this, index, it.length, ignoreCase
-        )
-      }
-      if (matchingString != null)
-        return index to matchingString
-    }
-  } else {
-    for (index in indices) {
-      val matchingString = strings.firstOrNull {
-        it.regionMatchesImpl(
-          0, this, index, it.length, ignoreCase
-        )
-      }
-      if (matchingString != null)
-        return index to matchingString
-    }
+inline fun <reified U> Spanned.hasSpans(range: IntRange, f: (U) -> Boolean) =
+  getSpans(range.start, range.last + 1, U::class.java).any {
+    f(it) &&
+    getSpanFlags(it) and Spanned.SPAN_COMPOSING == 0 &&
+    (getSpanEnd(it) != range.start ||
+     getSpanFlags(it) and 0x02 != 0)
   }
-
-  return null
-}
-
-private fun CharSequence.rangesDelimitedBy(delimiters: Array<out String>, startIndex: Int = 0,
-                                           ignoreCase: Boolean = false,
-                                           limit: Int = 0): Sequence<IntRange> {
-  require(limit >= 0, { "Limit must be non-negative, but was $limit." })
-  val delimitersList = delimiters.asList()
-
-  return DelimitedRangesSequence(
-    this, startIndex, limit, { startIndex ->
-    findAnyOf(
-      delimitersList, startIndex, ignoreCase = ignoreCase, last = false
-    )?.let { it.first to it.second.length }
-  })
-}
-
-fun CharSequence.splitToSequence(vararg delimiters: String, ignoreCase: Boolean = false,
-                                 limit: Int = 0): Sequence<CharSequence> =
-  rangesDelimitedBy(delimiters, ignoreCase = ignoreCase, limit = limit).map { subSequence(it) }
-
-fun CharSequence.lineSequence(): Sequence<CharSequence> = splitToSequence("\r\n", "\n", "\r")
\ No newline at end of file
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/ui/DoubleClickHelper.kt b/app/src/main/java/de/kuschku/quasseldroid/util/ui/DoubleClickHelper.kt
new file mode 100644
index 000000000..917f84c55
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/ui/DoubleClickHelper.kt
@@ -0,0 +1,24 @@
+package de.kuschku.quasseldroid.util.ui
+
+import android.view.GestureDetector
+import android.view.MotionEvent
+import android.view.View
+import android.widget.EditText
+
+class DoubleClickHelper(editText: EditText) : View.OnTouchListener {
+  var doubleClickListener: (() -> Unit)? = null
+
+  private val gestureDetector = GestureDetector(
+    editText.context, object : GestureDetector.SimpleOnGestureListener() {
+    override fun onDoubleTap(e: MotionEvent?): Boolean {
+      doubleClickListener?.invoke()
+      return true
+    }
+
+    override fun onDoubleTapEvent(e: MotionEvent?): Boolean {
+      return true
+    }
+  })
+
+  override fun onTouch(v: View?, event: MotionEvent?) = gestureDetector.onTouchEvent(event)
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/ui/EditTextSelectionChange.kt b/app/src/main/java/de/kuschku/quasseldroid/util/ui/EditTextSelectionChange.kt
index ad316e498..aff74edc5 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/util/ui/EditTextSelectionChange.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/ui/EditTextSelectionChange.kt
@@ -4,7 +4,7 @@ import android.content.Context
 import android.support.v7.widget.AppCompatEditText
 import android.util.AttributeSet
 
-class EditTextSelectionChange : AppCompatEditText {
+open class EditTextSelectionChange : AppCompatEditText {
   constructor(context: Context?) : super(context)
   constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
   constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) :
@@ -24,4 +24,4 @@ class EditTextSelectionChange : AppCompatEditText {
     super.onSelectionChanged(selStart, selEnd)
     selectionChangeListener?.invoke(selStart until selEnd)
   }
-}
\ No newline at end of file
+}
diff --git a/app/src/main/res/layout-land/layout_main.xml b/app/src/main/res/layout-land/layout_main.xml
index 3920f6034..9c4f28b83 100644
--- a/app/src/main/res/layout-land/layout_main.xml
+++ b/app/src/main/res/layout-land/layout_main.xml
@@ -34,6 +34,11 @@
       android:background="?colorBackgroundCard" />
   </LinearLayout>
 
-  <include layout="@layout/layout_slider" />
+  <fragment
+    android:id="@+id/fragment_chatline"
+    android:name="de.kuschku.quasseldroid.ui.chat.input.ChatlineFragment"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:layout="@layout/fragment_chatline" />
 
-</com.sothree.slidinguppanel.SlidingUpPanelLayout>
\ No newline at end of file
+</com.sothree.slidinguppanel.SlidingUpPanelLayout>
diff --git a/app/src/main/res/layout-sw600dp-land/layout_main.xml b/app/src/main/res/layout-sw600dp-land/layout_main.xml
index 736c78827..c88d225cd 100644
--- a/app/src/main/res/layout-sw600dp-land/layout_main.xml
+++ b/app/src/main/res/layout-sw600dp-land/layout_main.xml
@@ -40,8 +40,13 @@
 
     </LinearLayout>
 
-    <include layout="@layout/layout_slider" />
+    <fragment
+      android:id="@+id/fragment_chatline"
+      android:name="de.kuschku.quasseldroid.ui.chat.input.ChatlineFragment"
+      android:layout_width="match_parent"
+      android:layout_height="match_parent"
+      tools:layout="@layout/fragment_chatline" />
 
   </com.sothree.slidinguppanel.SlidingUpPanelLayout>
 
-</LinearLayout>
\ No newline at end of file
+</LinearLayout>
diff --git a/app/src/main/res/layout/layout_slider.xml b/app/src/main/res/layout/fragment_chatline.xml
similarity index 100%
rename from app/src/main/res/layout/layout_slider.xml
rename to app/src/main/res/layout/fragment_chatline.xml
diff --git a/app/src/main/res/layout/layout_editor.xml b/app/src/main/res/layout/layout_editor.xml
index 87b97cf1f..65fc344c9 100644
--- a/app/src/main/res/layout/layout_editor.xml
+++ b/app/src/main/res/layout/layout_editor.xml
@@ -27,7 +27,7 @@
     app:layout_constraintStart_toEndOf="@+id/tab_complete"
     app:layout_constraintTop_toTopOf="parent">
 
-    <de.kuschku.quasseldroid.util.ui.EditTextSelectionChange
+    <de.kuschku.quasseldroid.ui.chat.input.RichEditText
       android:id="@+id/chatline"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
@@ -71,18 +71,10 @@
     android:background="?attr/colorBackgroundCard"
     app:layout_constraintBottom_toBottomOf="parent">
 
-    <android.support.v7.widget.Toolbar
+    <de.kuschku.quasseldroid.ui.chat.input.RichToolbar
       android:id="@+id/formatting_toolbar"
       android:layout_width="match_parent"
       android:layout_height="?attr/actionBarSize"
-      app:contentInsetStart="0dip">
-
-      <HorizontalScrollView
-        android:layout_width="match_parent"
-        android:layout_height="match_parent">
-
-        <include layout="@layout/widget_formatting" />
-      </HorizontalScrollView>
-    </android.support.v7.widget.Toolbar>
+      app:contentInsetStart="0dip" />
   </android.support.design.widget.AppBarLayout>
-</android.support.constraint.ConstraintLayout>
\ No newline at end of file
+</android.support.constraint.ConstraintLayout>
diff --git a/app/src/main/res/layout/layout_main.xml b/app/src/main/res/layout/layout_main.xml
index 52a728255..b57db18f8 100644
--- a/app/src/main/res/layout/layout_main.xml
+++ b/app/src/main/res/layout/layout_main.xml
@@ -39,8 +39,13 @@
         android:background="?colorBackgroundCard" />
     </LinearLayout>
 
-    <include layout="@layout/layout_slider" />
+    <fragment
+      android:id="@+id/fragment_chatline"
+      android:name="de.kuschku.quasseldroid.ui.chat.input.ChatlineFragment"
+      android:layout_width="match_parent"
+      android:layout_height="match_parent"
+      tools:layout="@layout/fragment_chatline" />
 
   </com.sothree.slidinguppanel.SlidingUpPanelLayout>
 
-</LinearLayout>
\ No newline at end of file
+</LinearLayout>
diff --git a/app/src/main/res/layout/widget_formatting.xml b/app/src/main/res/layout/widget_formatting.xml
index eebeda3c6..c333c4185 100644
--- a/app/src/main/res/layout/widget_formatting.xml
+++ b/app/src/main/res/layout/widget_formatting.xml
@@ -1,127 +1,130 @@
 <?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
-  android:layout_width="wrap_content"
-  android:layout_height="match_parent"
-  android:padding="2dp">
-
-  <android.support.v7.widget.AppCompatImageButton
-    android:id="@+id/action_format_bold"
-    style="@style/Widget.Button.Format"
-    android:contentDescription="@string/label_bold"
-    app:srcCompat="@drawable/ic_format_bold"
-    app:tint="?colorControlNormal" />
-
-  <Space
-    android:layout_width="2dp"
-    android:layout_height="match_parent" />
-
-  <android.support.v7.widget.AppCompatImageButton
-    android:id="@+id/action_format_italic"
-    style="@style/Widget.Button.Format"
-    android:contentDescription="@string/label_italic"
-    app:srcCompat="@drawable/ic_format_italic"
-    app:tint="?colorControlNormal" />
-
-  <Space
-    android:layout_width="2dp"
-    android:layout_height="match_parent" />
-
-  <android.support.v7.widget.AppCompatImageButton
-    android:id="@+id/action_format_underline"
-    style="@style/Widget.Button.Format"
-    android:contentDescription="@string/label_underline"
-    app:srcCompat="@drawable/ic_format_underline"
-    app:tint="?colorControlNormal" />
-
-  <Space
-    android:layout_width="2dp"
-    android:layout_height="match_parent" />
-
-  <android.support.v7.widget.AppCompatImageButton
-    android:id="@+id/action_format_strikethrough"
-    style="@style/Widget.Button.Format"
-    android:contentDescription="@string/label_strikethrough"
-    app:srcCompat="@drawable/ic_format_strikethrough"
-    app:tint="?colorControlNormal" />
-
-  <Space
-    android:layout_width="2dp"
-    android:layout_height="match_parent" />
-
-  <android.support.v7.widget.AppCompatImageButton
-    android:id="@+id/action_format_monospace"
-    style="@style/Widget.Button.Format"
-    android:contentDescription="@string/label_bold"
-    app:srcCompat="@drawable/ic_format_monospace"
-    app:tint="?colorControlNormal" />
-
-  <Space
-    android:layout_width="2dp"
-    android:layout_height="match_parent" />
-
-  <FrameLayout
-    android:layout_width="48dp"
-    android:layout_height="48dp"
-    android:layout_gravity="center">
+  android:layout_width="match_parent"
+  android:layout_height="match_parent">
+
+  <LinearLayout
+    android:layout_width="wrap_content"
+    android:layout_height="match_parent"
+    android:padding="2dp">
 
     <android.support.v7.widget.AppCompatImageButton
-      android:id="@+id/action_format_foreground"
+      android:id="@+id/action_format_bold"
       style="@style/Widget.Button.Format"
-      android:layout_width="match_parent"
-      android:layout_height="match_parent"
-      android:contentDescription="@string/label_foreground"
-      android:paddingBottom="4dp"
-      app:srcCompat="@drawable/ic_format_foreground"
+      android:contentDescription="@string/label_bold"
+      app:srcCompat="@drawable/ic_format_bold"
       app:tint="?colorControlNormal" />
 
-    <View
-      android:id="@+id/action_format_foreground_preview"
-      android:layout_width="match_parent"
-      android:layout_height="4dp"
-      android:layout_gravity="center_horizontal|bottom"
-      android:layout_margin="8dp"
-      android:background="?colorForeground" />
+    <Space
+      android:layout_width="2dp"
+      android:layout_height="match_parent" />
 
-  </FrameLayout>
+    <android.support.v7.widget.AppCompatImageButton
+      android:id="@+id/action_format_italic"
+      style="@style/Widget.Button.Format"
+      android:contentDescription="@string/label_italic"
+      app:srcCompat="@drawable/ic_format_italic"
+      app:tint="?colorControlNormal" />
 
-  <Space
-    android:layout_width="2dp"
-    android:layout_height="match_parent" />
+    <Space
+      android:layout_width="2dp"
+      android:layout_height="match_parent" />
 
-  <FrameLayout
-    android:layout_width="48dp"
-    android:layout_height="48dp"
-    android:layout_gravity="center">
+    <android.support.v7.widget.AppCompatImageButton
+      android:id="@+id/action_format_underline"
+      style="@style/Widget.Button.Format"
+      android:contentDescription="@string/label_underline"
+      app:srcCompat="@drawable/ic_format_underline"
+      app:tint="?colorControlNormal" />
+
+    <Space
+      android:layout_width="2dp"
+      android:layout_height="match_parent" />
 
     <android.support.v7.widget.AppCompatImageButton
-      android:id="@+id/action_format_background"
+      android:id="@+id/action_format_strikethrough"
       style="@style/Widget.Button.Format"
-      android:layout_width="match_parent"
-      android:layout_height="match_parent"
-      android:contentDescription="@string/label_background"
-      android:paddingBottom="4dp"
-      app:srcCompat="@drawable/ic_format_background"
+      android:contentDescription="@string/label_strikethrough"
+      app:srcCompat="@drawable/ic_format_strikethrough"
       app:tint="?colorControlNormal" />
 
-    <View
-      android:id="@+id/action_format_background_preview"
-      android:layout_width="match_parent"
-      android:layout_height="4dp"
-      android:layout_gravity="center_horizontal|bottom"
-      android:layout_margin="8dp"
-      android:background="?colorBackground" />
-
-  </FrameLayout>
-
-  <Space
-    android:layout_width="2dp"
-    android:layout_height="match_parent" />
-
-  <android.support.v7.widget.AppCompatImageButton
-    android:id="@+id/action_format_clear"
-    style="@style/Widget.Button.Format"
-    android:contentDescription="@string/label_clear_formatting"
-    app:srcCompat="@drawable/ic_format_clear"
-    app:tint="?colorControlNormal" />
-</LinearLayout>
\ No newline at end of file
+    <Space
+      android:layout_width="2dp"
+      android:layout_height="match_parent" />
+
+    <android.support.v7.widget.AppCompatImageButton
+      android:id="@+id/action_format_monospace"
+      style="@style/Widget.Button.Format"
+      android:contentDescription="@string/label_bold"
+      app:srcCompat="@drawable/ic_format_monospace"
+      app:tint="?colorControlNormal" />
+
+    <Space
+      android:layout_width="2dp"
+      android:layout_height="match_parent" />
+
+    <FrameLayout
+      android:layout_width="48dp"
+      android:layout_height="48dp"
+      android:layout_gravity="center">
+
+      <android.support.v7.widget.AppCompatImageButton
+        android:id="@+id/action_format_foreground"
+        style="@style/Widget.Button.Format"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:contentDescription="@string/label_foreground"
+        android:paddingBottom="4dp"
+        app:srcCompat="@drawable/ic_format_foreground"
+        app:tint="?colorControlNormal" />
+
+      <View
+        android:id="@+id/action_format_foreground_preview"
+        android:layout_width="match_parent"
+        android:layout_height="4dp"
+        android:layout_gravity="center_horizontal|bottom"
+        android:layout_margin="8dp"
+        android:background="?colorForeground" />
+    </FrameLayout>
+
+    <Space
+      android:layout_width="2dp"
+      android:layout_height="match_parent" />
+
+    <FrameLayout
+      android:layout_width="48dp"
+      android:layout_height="48dp"
+      android:layout_gravity="center">
+
+      <android.support.v7.widget.AppCompatImageButton
+        android:id="@+id/action_format_background"
+        style="@style/Widget.Button.Format"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:contentDescription="@string/label_background"
+        android:paddingBottom="4dp"
+        app:srcCompat="@drawable/ic_format_background"
+        app:tint="?colorControlNormal" />
+
+      <View
+        android:id="@+id/action_format_background_preview"
+        android:layout_width="match_parent"
+        android:layout_height="4dp"
+        android:layout_gravity="center_horizontal|bottom"
+        android:layout_margin="8dp"
+        android:background="?colorBackground" />
+    </FrameLayout>
+
+    <Space
+      android:layout_width="2dp"
+      android:layout_height="match_parent" />
+
+    <android.support.v7.widget.AppCompatImageButton
+      android:id="@+id/action_format_clear"
+      style="@style/Widget.Button.Format"
+      android:contentDescription="@string/label_clear_formatting"
+      app:srcCompat="@drawable/ic_format_clear"
+      app:tint="?colorControlNormal" />
+  </LinearLayout>
+</HorizontalScrollView>
diff --git a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/EditorViewModel.kt b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/EditorViewModel.kt
new file mode 100644
index 000000000..b785ae2d9
--- /dev/null
+++ b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/EditorViewModel.kt
@@ -0,0 +1,125 @@
+package de.kuschku.quasseldroid.viewmodel
+
+import android.arch.lifecycle.ViewModel
+import de.kuschku.libquassel.protocol.Buffer_Type
+import de.kuschku.libquassel.quassel.syncables.IrcChannel
+import de.kuschku.libquassel.quassel.syncables.IrcUser
+import de.kuschku.libquassel.session.ISession
+import de.kuschku.libquassel.util.Optional
+import de.kuschku.libquassel.util.flag.hasFlag
+import de.kuschku.quasseldroid.util.helper.combineLatest
+import de.kuschku.quasseldroid.viewmodel.data.AutoCompleteItem
+import de.kuschku.quasseldroid.viewmodel.data.BufferStatus
+import io.reactivex.Observable
+import io.reactivex.subjects.BehaviorSubject
+import java.util.concurrent.TimeUnit
+
+class EditorViewModel : ViewModel() {
+  val quasselViewModel = BehaviorSubject.create<QuasselViewModel>()
+
+  val session = quasselViewModel.switchMap(QuasselViewModel::session)
+  val buffer = quasselViewModel.switchMap(QuasselViewModel::buffer)
+
+  val lastWord = BehaviorSubject.create<Observable<Pair<String, IntRange>>>()
+
+  val rawAutoCompleteData: Observable<Triple<Optional<ISession>, Int, Pair<String, IntRange>>> =
+    combineLatest(session, buffer, lastWord).switchMap { (sessionOptional, id, lastWordWrapper) ->
+      lastWordWrapper
+        .distinctUntilChanged()
+        .map { lastWord ->
+          Triple(sessionOptional, id, lastWord)
+        }
+    }
+
+  val autoCompleteData = rawAutoCompleteData
+    .distinctUntilChanged()
+    .debounce(300, TimeUnit.MILLISECONDS)
+    .switchMap { (sessionOptional, id, lastWord) ->
+      val session = sessionOptional.orNull()
+      val bufferSyncer = session?.bufferSyncer
+      val bufferInfo = bufferSyncer?.bufferInfo(id)
+      if (bufferSyncer != null) {
+        session.liveNetworks().switchMap { networks ->
+          bufferSyncer.liveBufferInfos().switchMap { infos ->
+            if (bufferInfo?.type?.hasFlag(Buffer_Type.ChannelBuffer) == true) {
+              val network = networks[bufferInfo.networkId]
+              val ircChannel = network?.ircChannel(
+                bufferInfo.bufferName
+              )
+              if (ircChannel != null) {
+                ircChannel.liveIrcUsers().switchMap { users ->
+                  val buffers: List<Observable<AutoCompleteItem.ChannelItem>?> = infos.values
+                    .filter {
+                      it.type.toInt() == Buffer_Type.ChannelBuffer.toInt()
+                    }.mapNotNull { info ->
+                      networks[info.networkId]?.let { info to it }
+                    }.map { (info, network) ->
+                      network.liveIrcChannel(
+                        info.bufferName
+                      ).switchMap { channel ->
+                        channel.liveUpdates().map {
+                          AutoCompleteItem.ChannelItem(
+                            info = info,
+                            network = network.networkInfo(),
+                            bufferStatus = when (it) {
+                              IrcChannel.NULL -> BufferStatus.OFFLINE
+                              else            -> BufferStatus.ONLINE
+                            },
+                            description = it.topic()
+                          )
+                        }
+                      }
+                    }
+                  val nicks = users.map<IrcUser, Observable<AutoCompleteItem.UserItem>?> {
+                    it.updates().map { user ->
+                      val userModes = ircChannel.userModes(user)
+                      val prefixModes = network.prefixModes()
+
+                      val lowestMode = userModes.mapNotNull(prefixModes::indexOf).min()
+                                       ?: prefixModes.size
+
+                      AutoCompleteItem.UserItem(
+                        user.nick(),
+                        network.modesToPrefixes(userModes),
+                        lowestMode,
+                        user.realName(),
+                        user.isAway(),
+                        network.support("CASEMAPPING"),
+                        Regex("[us]id(\\d+)").matchEntire(user.user())?.groupValues?.lastOrNull()?.let {
+                          "https://www.irccloud.com/avatar-redirect/$it"
+                        }
+                      )
+                    }
+                  }
+
+                  combineLatest<AutoCompleteItem>(nicks + buffers)
+                    .map { list ->
+                      val ignoredStartingCharacters = charArrayOf(
+                        '-', '_', '[', ']', '{', '}', '|', '`', '^', '.', '\\', '@'
+                      )
+
+                      Pair(
+                        lastWord.first,
+                        list.filter {
+                          it.name.trimStart(*ignoredStartingCharacters)
+                            .startsWith(
+                              lastWord.first.trimStart(*ignoredStartingCharacters),
+                              ignoreCase = true
+                            )
+                        }.sorted()
+                      )
+                    }
+                }
+              } else {
+                Observable.just(Pair(lastWord.first, emptyList()))
+              }
+            } else {
+              Observable.just(Pair(lastWord.first, emptyList()))
+            }
+          }
+        }
+      } else {
+        Observable.just(Pair(lastWord.first, emptyList()))
+      }
+    }
+}
diff --git a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/QuasselViewModel.kt b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/QuasselViewModel.kt
index c87652c40..53d8762cb 100644
--- a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/QuasselViewModel.kt
+++ b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/QuasselViewModel.kt
@@ -212,111 +212,6 @@ class QuasselViewModel : ViewModel() {
       }
     }
 
-  val lastWord = BehaviorSubject.create<Observable<Pair<String, IntRange>>>()
-
-  val rawAutoCompleteData: Observable<Triple<Optional<ISession>, Int, Pair<String, IntRange>>> =
-    combineLatest(session, buffer, lastWord).switchMap { (sessionOptional, id, lastWordWrapper) ->
-      lastWordWrapper
-        .distinctUntilChanged()
-        .map { lastWord ->
-          Triple(sessionOptional, id, lastWord)
-        }
-    }
-
-  var time = 0L
-  var previous: Any? = null
-  val autoCompleteData = rawAutoCompleteData
-    .distinctUntilChanged()
-    .debounce(300, TimeUnit.MILLISECONDS)
-    .switchMap { (sessionOptional, id, lastWord) ->
-      val session = sessionOptional.orNull()
-      val bufferSyncer = session?.bufferSyncer
-      val bufferInfo = bufferSyncer?.bufferInfo(id)
-      if (bufferSyncer != null) {
-        session.liveNetworks().switchMap { networks ->
-          bufferSyncer.liveBufferInfos().switchMap { infos ->
-            if (bufferInfo?.type?.hasFlag(Buffer_Type.ChannelBuffer) == true) {
-              val network = networks[bufferInfo.networkId]
-              val ircChannel = network?.ircChannel(
-                bufferInfo.bufferName
-              )
-              if (ircChannel != null) {
-                ircChannel.liveIrcUsers().switchMap { users ->
-                  val buffers: List<Observable<AutoCompleteItem.ChannelItem>?> = infos.values
-                    .filter {
-                      it.type.toInt() == Buffer_Type.ChannelBuffer.toInt()
-                    }.mapNotNull { info ->
-                      networks[info.networkId]?.let { info to it }
-                    }.map { (info, network) ->
-                      network.liveIrcChannel(
-                        info.bufferName
-                      ).switchMap { channel ->
-                        channel.liveUpdates().map {
-                          AutoCompleteItem.ChannelItem(
-                            info = info,
-                            network = network.networkInfo(),
-                            bufferStatus = when (it) {
-                              IrcChannel.NULL -> BufferStatus.OFFLINE
-                              else            -> BufferStatus.ONLINE
-                            },
-                            description = it.topic()
-                          )
-                        }
-                      }
-                    }
-                  val nicks = users.map<IrcUser, Observable<AutoCompleteItem.UserItem>?> {
-                    it.updates().map { user ->
-                      val userModes = ircChannel.userModes(user)
-                      val prefixModes = network.prefixModes()
-
-                      val lowestMode = userModes.mapNotNull(prefixModes::indexOf).min()
-                                       ?: prefixModes.size
-
-                      AutoCompleteItem.UserItem(
-                        user.nick(),
-                        network.modesToPrefixes(userModes),
-                        lowestMode,
-                        user.realName(),
-                        user.isAway(),
-                        network.support("CASEMAPPING"),
-                        Regex("[us]id(\\d+)").matchEntire(user.user())?.groupValues?.lastOrNull()?.let {
-                          "https://www.irccloud.com/avatar-redirect/$it"
-                        }
-                      )
-                    }
-                  }
-
-                  combineLatest<AutoCompleteItem>(nicks + buffers)
-                    .map { list ->
-                      val ignoredStartingCharacters = charArrayOf(
-                        '-', '_', '[', ']', '{', '}', '|', '`', '^', '.', '\\', '@'
-                      )
-
-                      Pair(
-                        lastWord.first,
-                        list.filter {
-                          it.name.trimStart(*ignoredStartingCharacters)
-                            .startsWith(
-                              lastWord.first.trimStart(*ignoredStartingCharacters),
-                              ignoreCase = true
-                            )
-                        }.sorted()
-                      )
-                    }
-                }
-              } else {
-                Observable.just(Pair(lastWord.first, emptyList()))
-              }
-            } else {
-              Observable.just(Pair(lastWord.first, emptyList()))
-            }
-          }
-        }
-      } else {
-        Observable.just(Pair(lastWord.first, emptyList()))
-      }
-    }
-
   val bufferViewConfigs = bufferViewManager.mapSwitchMap { manager ->
     manager.liveBufferViewConfigs().map { ids ->
       ids.mapNotNull { id ->
-- 
GitLab