Copenhagen Code Editor Example
A simple, lightweight, extensible code editor that supports popular languages like HTML, CSS, JavaScript, JSON, Markdown, and Plain Text.
function CodeCompleter () {
this.suggestions = this.generateSuggestions(this.suggestionMap);
};
CodeCompleter.prototype.cursorCharacter = '·';
CodeCompleter.prototype.wildcardWordCharacter = '¤';
CodeCompleter.prototype.wildcardPhraseCharacter = '…';
CodeCompleter.prototype.wildcardReplaceCharacter = '\\$1';
CodeCompleter.prototype.suggestionMap = {
'javascript': [
'const ',
'const ¤ = ',
'const {…} = ',
'const […] = ',
'console.log(`·Got here: A·`);',
'console.error(`·Error·`);',
'let ',
'let ¤ = ',
'let {…} = ',
'let […] = ',
'var ',
'var ¤ = ',
'var {…} = ',
'var […] = ',
'lib.',
'module.exports = ',
'module.exports = async ',
'return ',
'require(\'·\')',
'class ',
'class ¤ {·}',
'function ',
'function (·)',
'function ¤ (·)',
'function () {·}',
'function ¤ () {·}',
'function (…) {·}',
'function ¤ (…) {·}',
'if (·true·)',
'if () {·}',
'if (…) {·}',
'else ',
'else {·}',
'else if (·true·)',
'for (let i = 0; i < ·10·; i++)',
'for () {·}',
'for (…) {·}',
'while (·true·)',
'while () {·}',
'while (…) {·}',
'await ',
'await lib.',
'await new Promise((resolve, reject) => {·});',
'async ',
'async (·)',
'() => {·}',
'(…) => {·}',
'/**\n * ·\n */',
'* @param {·}',
'* @param {…} ·paramName·',
'* @returns {·}',
'* @returns {…} ·returnValue·',
'true',
'false',
'null',
'new ',
'new Promise((resolve, reject) => {·});',
'Promise((resolve, reject) => {·});',
'Promise.all([·]);',
'setTimeout(() => {·}, 1);',
'setInterval(() => {·}, 1);',
'try {·}',
'catch (e) {·}',
'catch (…) {·}',
'throw ',
'throw new Error(`·Oops!·`);',
'new Error(`·Oops!·`)',
'Error(`·Oops!·`)',
'Error(…)'
]
};
CodeCompleter.prototype.generateSuggestions = function () {
var suggestionMap = this.suggestionMap;
var cursorCharacter = this.cursorCharacter;
return Object.keys(suggestionMap).reduce(function (suggestions, language) {
var phraseList = suggestionMap[language].map(function (value) {
var cursorStart = value.indexOf(cursorCharacter);
var cursorEnd = value.lastIndexOf(cursorCharacter);
var cursorLength = 0;
if (cursorStart !== cursorEnd) {
cursorLength = cursorEnd - cursorStart - 1;
value = value.slice(0, cursorEnd) + value.slice(cursorEnd + 1);
}
var adjust = cursorStart === -1
? 0
: cursorStart - value.length + 1;
if (adjust) {
value = value.substr(0, value.length + adjust - 1) + value.substr(value.length + adjust);
}
return {
value: value,
adjust: adjust,
cursorLength: cursorLength
};
}.bind(this));
suggestions[language] = {
lookup: this.generateLookupTrie(phraseList),
};
return suggestions;
}.bind(this), {});
};
CodeCompleter.prototype.generateLookupTrie = function (phraseList) {
var wildcardWord = this.wildcardWordCharacter;
var wildcardPhrase = this.wildcardPhraseCharacter;
var root = {};
var curNode, node, phrase, value;
var i, j, k;
for (i = 0; i < phraseList.length; i++) {
phrase = phraseList[i];
value = phrase.value;
for (j = value.length - 1; j >= 0; j--) {
curNode = root;
for (k = j; k >= 0; k--) {
char = value[k];
curNode[char] = curNode[char] || {};
if (char === wildcardWord || char === wildcardPhrase) {
curNode[char][char] = curNode[char][char] || curNode[char];
}
curNode = curNode[char];
}
curNode.phrases = curNode.phrases || [];
curNode.phrases.push({
value: value,
ranking: i,
adjust: phrase.adjust,
cursorLength: phrase.cursorLength,
re: phrase.re
});
}
}
return root;
};
CodeCompleter.prototype.complete = function (tree, value, index, subs, inWildcard) {
index = index || 0;
subs = subs || [];
inWildcard = inWildcard || '';
var wildcardWord = this.wildcardWordCharacter;
var wildcardPhrase = this.wildcardPhraseCharacter;
var wildcardReplace = this.wildcardReplaceCharacter;
var char;
var results = [];
var node = tree;
for (var i = value.length - 1; i >= 0; i--) {
index++;
var char = value[i];
if (node[wildcardWord]) {
if (char.match(/[0-9a-z_$]/i)) {
var newSubs = subs.slice();
if (inWildcard) {
newSubs[0] = char + newSubs[0];
} else {
newSubs.unshift(char);
}
results = results.concat(
this.complete(node[wildcardWord], value.substr(0, i), index - 1, newSubs, wildcardWord)
);
}
}
if (node[wildcardPhrase]) {
if (char.match(/[^\(\)\[\]\{\}\"\'\`]/i)) {
var newSubs = subs.slice();
if (inWildcard) {
newSubs[0] = char + newSubs[0];
} else {
newSubs.unshift(char);
}
results = results.concat(
this.complete(node[wildcardPhrase], value.substr(0, i), index - 1, newSubs, wildcardPhrase)
);
}
}
if (node[char]) {
inWildcard = '';
if (node.phrases && (char === ' ')) {
results = results.concat(
node.phrases.map(function (p) {
var curSubs = subs.slice();
return {
value: p.value.replace(
new RegExp('(' + [wildcardWord, wildcardPhrase, wildcardReplace].join('|') + ')', 'gi'),
function ($0) { return curSubs.shift() || ''; }
),
ranking: p.ranking,
adjust: p.adjust,
offset: index - 1 + subs.join('').length,
cursorLength: p.cursorLength
};
})
);
}
node = node[char];
} else {
break;
}
}
if (node.phrases && (i < 0 || value[i] === ' ')) {
(i < 0) && index++;
results = results.concat(
node.phrases.map(function (p) {
var curSubs = subs.slice();
return {
value: p.value.replace(
new RegExp('(' + [wildcardWord, wildcardPhrase, wildcardReplace].join('|') + ')', 'gi'),
function ($0) { return curSubs.shift() || ''; }
),
ranking: p.ranking,
adjust: p.adjust,
offset: index - 1 + subs.join('').length,
cursorLength: p.cursorLength
};
})
);
}
return results
.sort(function (p1, p2) { return p2.offset - p1.offset || p1.ranking - p2.ranking; })
.filter(function (p) { return p.offset < p.value.length; });
};
CodeCompleter.prototype.suggest = function (line, language, endOffset) {
endOffset = parseInt(endOffset) || 0;
var suggestions = this.suggestions[language];
if (!suggestions) {
return;
}
line = line.substring(0, line.length).replace(/^\s+/, '');
var phrases = this.complete(suggestions.lookup, line);
if (!phrases.length) {
return;
}
var suggest = phrases[0];
return {
value: suggest.value.substr(suggest.offset + endOffset),
adjust: suggest.adjust,
cursorLength: suggest.cursorLength
};
};
window.addEventListener('DOMContentLoaded', function () {
var editors = Copenhagen.initSelectorAll('.editor');
});