🇩🇰 Copenhagen Editor0.1.3
Copenhagen is a free, lightweight and hackable
open source code editor for the web. It's responsible for powering the
code-editing experience on Autocode,
and it's written entirely in vanilla JavaScript with only
highlight.js and
feather icons bundled as
dependencies. You can start playing with it yourself directly by
following the installation instructions:
view on GitHub or
install (fork) on Autocode.
// Welcome to the Copenhagen Editor!
// Try typing some JavaScript - there's even built-in typeahead!
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
};
};
Features
- A simple, intuitive API
- Extensible code completion via typeahead
- Configurable hotkeys
- Multi-cursor selection
- Find and replace
- Support for large files (currently up to 100kloc)
- Configurable language settings and highlighting
- Small package size (50kB minified)
- Autogrow mode (default) or maximized view
- Custom autocomplete dropdown support (WIP)
- Mobile support (WIP)
Mission
The mission of Copenhagen is to be the web's most intuitive code-editing component.
We need it to be easy to play with — we're working very
hard to deliver a best-in-class web-native development experience inside
of Autocode.
Both developers and non-developers spend hours of their time writing code
inside of our product each day and Copenhagen gives us complete control to
(1) innovate quickly while we (2) maintain a code-writing experience
as close as possible to the world's best native IDEs.
Why should I use Copenhagen?
VSCode's editor, Monaco,
is a great choice for a web-based code-editing component. However; it's
also over 2MB, not explicitly mobile-friendly, and has a pretty verbose API.
Copenhagen gets you 95% of the most useful functionality with 2.5% of the code.
Admittedly, Monaco currently performs better on gigantic files (>100kloc).
When comparing Copenhagen to Ace or
CodeMirror it really comes down to
personal preference — we like our API and editor and we hope you do, too.
We're committed to making sure it stays awesome.
How well-supported and stable is Copenhagen?
Copenhagen has been tested to work in most modern browsers: specifically
the latest versions of Chrome, Firefox, Safari and Edge as of March
16th, 2021. The API is likely to undergo significant improvement over
time, hence being labeled 0.1.3
. The editor as it exists
within Autocode is under constant development —
however, we don't know ahead of time what the public interest in Copenhagen
will be, so for now we'll be updating public releases as time permits.
Following: we have no public roadmap right now, but if you're
interested in helping, please let us know by
opening an issue on GitHub.
We're also hiring on the Autocode team, so feel free to e-mail us at
[email protected].
What's with the name?
The first version of Copenhagen was built in a weekend at the
Radisson Blu Scandinavia Hotel in Copenhagen, Denmark during early 2018.
We launched it as a standalone product
that would eventually become Autocode. Monaco was too big, we weren't
happy with CodeMirror's API, and truthfully it was a bit of a fun exercise
in seeing if we could build our own. We didn't name it until February
2021 when we decided we wanted to open source it. The name seemed natural,
if not a little tongue-in-cheek — Microsoft's Monaco editor is named
after the city-state that's home to the Casino de Monte-Carlo,
and the hotel we were at is literally attached to Casino Copenhagen.
Development and license
The development of Copenhagen is funded by commercial interests and is
owned by Polybit Inc. DBA Autocode
(here is our team and list of investors),
but is fully open source and MIT licensed. We do not profit off of the
development of Copenhagen directly; proceeds from our commercial offering
help fund its development.
Updates and who to follow
You can follow Autocode team updates on Twitter at
@AutocodeHQ. The primary
author of Copenhagen is @keithwhor (Keith Horwood)
with the support of @Hacubu (Jacob Lee).
Special thanks to @threesided (Scott Gamble)
and @YusufMusleh for hundreds
of hours of testing in total, and thanks to our users and customers for
plenty of feedback. Enjoy Copenhagen, we sure have! 😎