plain-mentions

by tnhu

HTML

<input/>
<textarea>
$hello}       $$hello     $hello${world    $hello_world

$hello.world

${hello world}

${helloworld }

${helloworld}

${hello

$$$
$$$hello

$$${hello

${

$

$hello

abc

(\$\{?)?[_a-zA-Z][_a-zA-Z0-9.]*\}?

missing $:

\$?[{_a-zA-Z][_a-zA-Z0-9.]*\}?

with $

(\$?[{_a-zA-Z][_a-zA-Z0-9.]*\}?)|(\$)
$hello}       $$hello     $hello${world    $hello_world

$hello.world

${hello world}

${helloworld }
</textarea>
<span id="caret"/>

CSS

html, body {
  width: 100%;
  height: 100%;
  margin: 0; padding: 0;
}

body {
  display: flex;
  flex-direction: column;
  position: relative;
}

#caret {
  display: inline-block;
  position: absolute;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: rgba(255, 0, 0, .4);
}

input, textarea {
  padding: 5px;
}

textarea {
  flex-grow: 1;
}

JavaScript

// Refs:
// - https://microsoft.github.io/monaco-editor/monarch.html
// Notes:
//	1- Keyboard could be faster than processing, make a scheduler to pick only latest typing.
//  2- Ignore meta keys (shift, f1..., alt...)
//  3- Caret position: https://jsfiddle.net/dandv/aFPA7/
//               https://github.com/akiroom/caretposition.js
//               https://jsfiddle.net/tnhu/jdb1d8xt/
// Maybe the best? https://stackoverflow.com/questions/11966369/get-cursor-position-in-selection-relative-to-document
// http://akiroom.github.io/caretposition.js/demo/sample.html

const trigger = ['$', '${'].sort((a, b) => a.length > b.length ? -1 : 1) // Longer trigger goes first
const replacementRule = match => '${' + match + '}'
const triggerSet = new Set(trigger.join(''))
const MatchingRulesLeftRight = replacementRule('sample.text').split('sample.text')

const IdentifiersSet = /[A-Za-z0-9_.]/
var $caret = document.getElementById('caret')

function getTokens(value, atIndex) {
  const valueLen = value.length
  const triggerLen = trigger.length
  let tokenIndex = atIndex
  let tokenLen = 0
  let token, word, wordLen, match = ''
  
  while (tokenIndex--) {
    const char = value[tokenIndex]
    
    // If char is out of indentifier and trigger ranges, exit loop
    if (!triggerSet.has(char) && !IdentifiersSet.test(char)) {
	    break
    }
    
    const copiedStr = value.substr(tokenIndex, ++tokenLen)

		for (let i = 0; i < triggerLen; i++) {
    	if (copiedStr.indexOf(trigger[i]) === 0) {
      	token = copiedStr
        break
      }
    }
    
    if (token) {
      wordLen = tokenLen
      let index = tokenIndex + wordLen
      
      while (index < valueLen) {
      	if (!IdentifiersSet.test(value[index])) {
	        if (MatchingRulesLeftRight.length === 2
          	&& token.indexOf(MatchingRulesLeftRight[0]) === 0
            && value.indexOf(MatchingRulesLeftRight[1], index) === index) {
          		wordLen += MatchingRulesLeftRight[1].length
          }
          break
...