Switch code editor to Monaco (#11366)
* Switch code editor to Monaco This switches out CodeMirror for Monaco which is based on the same code base as VS code and should work pretty similar to it. It does add a few async chunks, totalling around 10MB to our build. It currently supports around 65 languages and in the default configuration, each language would emit one ugly [number].js chunk, so I opted to combine them all into a single file for now. CodeMirror is still being used under the hood by SimpleMDE so it can not be removed yet. * inline editorconfig, fix diff, use for markdown, remove more dead code * refactors, remove jquery usage * use tab_width * fix intellisense * rename function for clarity * misc tweaks, enable webpack progress display * only use --progress on dev build * remove useless borders in arc-green * fix typo * remove obsolete comment * small refactor * fix file creation and various refactors * unset useTabStops too when no editorconfig * small refactor * disable webpack's [big] warnings * remove useless await * fix dark theme check * rename chunk to 'monaco' * add to .gitignore and delete webpack dest before build * increase editor height * support more editorconfig properties * remove empty element filter * rename Co-authored-by: John Olheiser <john.olheiser@gmail.com>tokarchuk/v1.17
parent
984ee0113b
commit
9249c810b8
@ -0,0 +1,104 @@ |
|||||||
|
import {basename, extname, isObject, isDarkTheme} from '../utils.js'; |
||||||
|
|
||||||
|
const languagesByFilename = {}; |
||||||
|
const languagesByExt = {}; |
||||||
|
|
||||||
|
function getEditorconfig(input) { |
||||||
|
try { |
||||||
|
return JSON.parse(input.dataset.editorconfig); |
||||||
|
} catch (_err) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function initLanguages(monaco) { |
||||||
|
for (const {filenames, extensions, id} of monaco.languages.getLanguages()) { |
||||||
|
for (const filename of filenames || []) { |
||||||
|
languagesByFilename[filename] = id; |
||||||
|
} |
||||||
|
for (const extension of extensions || []) { |
||||||
|
languagesByExt[extension] = id; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function getLanguage(filename) { |
||||||
|
return languagesByFilename[filename] || languagesByExt[extname(filename)] || 'plaintext'; |
||||||
|
} |
||||||
|
|
||||||
|
function updateEditor(monaco, editor, filenameInput) { |
||||||
|
const newFilename = filenameInput.value; |
||||||
|
editor.updateOptions(getOptions(filenameInput)); |
||||||
|
const model = editor.getModel(); |
||||||
|
const language = model.getModeId(); |
||||||
|
const newLanguage = getLanguage(newFilename); |
||||||
|
if (language !== newLanguage) monaco.editor.setModelLanguage(model, newLanguage); |
||||||
|
} |
||||||
|
|
||||||
|
export async function createCodeEditor(textarea, filenameInput, previewFileModes) { |
||||||
|
const filename = basename(filenameInput.value); |
||||||
|
const previewLink = document.querySelector('a[data-tab=preview]'); |
||||||
|
const markdownExts = (textarea.dataset.markdownFileExts || '').split(','); |
||||||
|
const lineWrapExts = (textarea.dataset.lineWrapExtensions || '').split(','); |
||||||
|
const isMarkdown = markdownExts.includes(extname(filename)); |
||||||
|
|
||||||
|
if (previewLink) { |
||||||
|
if (isMarkdown && (previewFileModes || []).includes('markdown')) { |
||||||
|
previewLink.dataset.url = previewLink.dataset.url.replace(/(.*)\/.*/i, `$1/markdown`); |
||||||
|
previewLink.style.display = ''; |
||||||
|
} else { |
||||||
|
previewLink.style.display = 'none'; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const monaco = await import(/* webpackChunkName: "monaco" */'monaco-editor'); |
||||||
|
initLanguages(monaco); |
||||||
|
|
||||||
|
const container = document.createElement('div'); |
||||||
|
container.className = 'monaco-editor-container'; |
||||||
|
textarea.parentNode.appendChild(container); |
||||||
|
|
||||||
|
const editor = monaco.editor.create(container, { |
||||||
|
value: textarea.value, |
||||||
|
language: getLanguage(filename), |
||||||
|
...getOptions(filenameInput, lineWrapExts), |
||||||
|
}); |
||||||
|
|
||||||
|
const model = editor.getModel(); |
||||||
|
model.onDidChangeContent(() => { |
||||||
|
textarea.value = editor.getValue(); |
||||||
|
textarea.dispatchEvent(new Event('change')); // seems to be needed for jquery-are-you-sure
|
||||||
|
}); |
||||||
|
|
||||||
|
window.addEventListener('resize', () => { |
||||||
|
editor.layout(); |
||||||
|
}); |
||||||
|
|
||||||
|
filenameInput.addEventListener('keyup', () => { |
||||||
|
updateEditor(monaco, editor, filenameInput); |
||||||
|
}); |
||||||
|
|
||||||
|
const loading = document.querySelector('.editor-loading'); |
||||||
|
if (loading) loading.remove(); |
||||||
|
|
||||||
|
return editor; |
||||||
|
} |
||||||
|
|
||||||
|
function getOptions(filenameInput, lineWrapExts) { |
||||||
|
const ec = getEditorconfig(filenameInput); |
||||||
|
const theme = isDarkTheme() ? 'vs-dark' : 'vs'; |
||||||
|
const wordWrap = (lineWrapExts || []).includes(extname(filenameInput.value)) ? 'on' : 'off'; |
||||||
|
|
||||||
|
const opts = {theme, wordWrap}; |
||||||
|
if (isObject(ec)) { |
||||||
|
opts.detectIndentation = !('indent_style' in ec) || !('indent_size' in ec); |
||||||
|
if ('indent_size' in ec) opts.indentSize = Number(ec.indent_size); |
||||||
|
if ('tab_width' in ec) opts.tabSize = Number(ec.tab_width) || opts.indentSize; |
||||||
|
if ('max_line_length' in ec) opts.rulers = [Number(ec.max_line_length)]; |
||||||
|
opts.trimAutoWhitespace = ec.trim_trailing_whitespace === true; |
||||||
|
opts.insertSpaces = ec.indent_style === 'space'; |
||||||
|
opts.useTabStops = ec.indent_style === 'tab'; |
||||||
|
} |
||||||
|
|
||||||
|
return opts; |
||||||
|
} |
@ -1,3 +1,25 @@ |
|||||||
|
// retrieve a HTML string for given SVG icon name and size in pixels
|
||||||
export function svg(name, size) { |
export function svg(name, size) { |
||||||
return `<svg class="svg ${name}" width="${size}" height="${size}" aria-hidden="true"><use xlink:href="#${name}"/></svg>`; |
return `<svg class="svg ${name}" width="${size}" height="${size}" aria-hidden="true"><use xlink:href="#${name}"/></svg>`; |
||||||
} |
} |
||||||
|
|
||||||
|
// transform /path/to/file.ext to file.ext
|
||||||
|
export function basename(path = '') { |
||||||
|
return path ? path.replace(/^.*\//, '') : ''; |
||||||
|
} |
||||||
|
|
||||||
|
// transform /path/to/file.ext to .ext
|
||||||
|
export function extname(path = '') { |
||||||
|
const [_, ext] = /.+(\.[^.]+)$/.exec(path) || []; |
||||||
|
return ext || ''; |
||||||
|
} |
||||||
|
|
||||||
|
// test whether a variable is an object
|
||||||
|
export function isObject(obj) { |
||||||
|
return Object.prototype.toString.call(obj) === '[object Object]'; |
||||||
|
} |
||||||
|
|
||||||
|
// returns whether a dark theme is enabled
|
||||||
|
export function isDarkTheme() { |
||||||
|
return document.documentElement.classList.contains('theme-arc-green'); |
||||||
|
} |
||||||
|
Loading…
Reference in new issue