* update monaco * update monaco * update monaco * update monaco * update monaco * update monaco * update monaco * update monaco
617 lines
14 KiB
Svelte
617 lines
14 KiB
Svelte
<script lang="ts">
|
|
import { BROWSER } from 'esm-env'
|
|
import {
|
|
convertKind,
|
|
createDocumentationString,
|
|
createHash,
|
|
displayPartsToString,
|
|
editorConfig,
|
|
updateOptions
|
|
} from '$lib/editorUtils'
|
|
import libStdContent from '$lib/es5.d.ts.txt?raw'
|
|
import { editor as meditor, Uri as mUri, languages, Range, KeyMod, KeyCode } from 'monaco-editor'
|
|
import { createEventDispatcher, getContext, onDestroy, onMount } from 'svelte'
|
|
import type { AppViewerContext } from './apps/types'
|
|
import { writable } from 'svelte/store'
|
|
import { buildWorkerDefinition } from './build_workers'
|
|
import 'monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution'
|
|
import 'monaco-editor/esm/vs/basic-languages/typescript/typescript.contribution'
|
|
import 'monaco-editor/esm/vs/language/typescript/monaco.contribution'
|
|
import { initializeVscode } from './vscode'
|
|
import EditorTheme from './EditorTheme.svelte'
|
|
|
|
export const conf = {
|
|
wordPattern:
|
|
/(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g,
|
|
|
|
comments: {
|
|
lineComment: '//',
|
|
blockComment: ['/*', '*/'] as [string, string]
|
|
},
|
|
|
|
brackets: [
|
|
['{', '}'],
|
|
['[', ']'],
|
|
['(', ')']
|
|
] as [string, string][],
|
|
|
|
onEnterRules: [],
|
|
autoClosingPairs: [
|
|
{ open: '{', close: '}' },
|
|
{ open: '[', close: ']' },
|
|
{ open: '(', close: ')' },
|
|
{ open: '"', close: '"', notIn: ['string'] },
|
|
{ open: "'", close: "'", notIn: ['string', 'comment'] },
|
|
{ open: '`', close: '`', notIn: ['string', 'comment'] }
|
|
],
|
|
|
|
folding: {
|
|
markers: {
|
|
start: new RegExp('^\\s*//\\s*#?region\\b'),
|
|
end: new RegExp('^\\s*//\\s*#?endregion\\b')
|
|
}
|
|
}
|
|
}
|
|
|
|
export const language = {
|
|
// Set defaultToken to invalid to see what you do not tokenize yet
|
|
defaultToken: 'invalid',
|
|
tokenPostfix: '.ts',
|
|
|
|
keywords: [
|
|
// Should match the keys of textToKeywordObj in
|
|
// https://github.com/microsoft/TypeScript/blob/master/src/compiler/scanner.ts
|
|
'abstract',
|
|
'any',
|
|
'as',
|
|
'asserts',
|
|
'bigint',
|
|
'boolean',
|
|
'break',
|
|
'case',
|
|
'catch',
|
|
'class',
|
|
'continue',
|
|
'const',
|
|
'constructor',
|
|
'debugger',
|
|
'declare',
|
|
'default',
|
|
'delete',
|
|
'do',
|
|
'else',
|
|
'enum',
|
|
'export',
|
|
'extends',
|
|
'false',
|
|
'finally',
|
|
'for',
|
|
'from',
|
|
'function',
|
|
'get',
|
|
'if',
|
|
'implements',
|
|
'import',
|
|
'in',
|
|
'infer',
|
|
'instanceof',
|
|
'interface',
|
|
'is',
|
|
'keyof',
|
|
'let',
|
|
'module',
|
|
'namespace',
|
|
'never',
|
|
'new',
|
|
'null',
|
|
'number',
|
|
'object',
|
|
'out',
|
|
'package',
|
|
'private',
|
|
'protected',
|
|
'public',
|
|
'override',
|
|
'readonly',
|
|
'require',
|
|
'global',
|
|
'return',
|
|
'set',
|
|
'static',
|
|
'string',
|
|
'super',
|
|
'switch',
|
|
'symbol',
|
|
'this',
|
|
'throw',
|
|
'true',
|
|
'try',
|
|
'type',
|
|
'typeof',
|
|
'undefined',
|
|
'unique',
|
|
'unknown',
|
|
'var',
|
|
'void',
|
|
'while',
|
|
'with',
|
|
'yield',
|
|
'async',
|
|
'await',
|
|
'of'
|
|
],
|
|
|
|
operators: [
|
|
'<=',
|
|
'>=',
|
|
'==',
|
|
'!=',
|
|
'===',
|
|
'!==',
|
|
'=>',
|
|
'+',
|
|
'-',
|
|
'**',
|
|
'*',
|
|
'/',
|
|
'%',
|
|
'++',
|
|
'--',
|
|
'<<',
|
|
'</',
|
|
'>>',
|
|
'>>>',
|
|
'&',
|
|
'|',
|
|
'^',
|
|
'!',
|
|
'~',
|
|
'&&',
|
|
'||',
|
|
'??',
|
|
'?',
|
|
':',
|
|
'=',
|
|
'+=',
|
|
'-=',
|
|
'*=',
|
|
'**=',
|
|
'/=',
|
|
'%=',
|
|
'<<=',
|
|
'>>=',
|
|
'>>>=',
|
|
'&=',
|
|
'|=',
|
|
'^=',
|
|
'@'
|
|
],
|
|
|
|
// we include these common regular expressions
|
|
symbols: /[=><!~?:&|+\-*\/\^%]+/,
|
|
escapes: /\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,
|
|
digits: /\d+(_+\d+)*/,
|
|
octaldigits: /[0-7]+(_+[0-7]+)*/,
|
|
binarydigits: /[0-1]+(_+[0-1]+)*/,
|
|
hexdigits: /[[0-9a-fA-F]+(_+[0-9a-fA-F]+)*/,
|
|
|
|
regexpctl: /[(){}\[\]\$\^|\-*+?\.]/,
|
|
regexpesc: /\\(?:[bBdDfnrstvwWn0\\\/]|@regexpctl|c[A-Z]|x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4})/,
|
|
|
|
// The main tokenizer for our languages
|
|
tokenizer: {
|
|
root: [
|
|
[/\$\{/, { token: 'delimiter.bracket', next: '@bracketCounting' }],
|
|
[/[^\\`$]+/, 'string'],
|
|
[/@escapes/, 'string.escape'],
|
|
[/\\./, 'string.escape.invalid']
|
|
],
|
|
|
|
common: [
|
|
// identifiers and keywords
|
|
[
|
|
/[a-z_$][\w$]*/,
|
|
{
|
|
cases: {
|
|
'@keywords': 'keyword',
|
|
'@default': 'identifier'
|
|
}
|
|
}
|
|
],
|
|
[/[A-Z][\w\$]*/, 'type.identifier'], // to show class names nicely
|
|
// [/[A-Z][\w\$]*/, 'identifier'],
|
|
|
|
// whitespace
|
|
{ include: '@whitespace' },
|
|
|
|
// regular expression: ensure it is terminated before beginning (otherwise it is an opeator)
|
|
[
|
|
/\/(?=([^\\\/]|\\.)+\/([dgimsuy]*)(\s*)(\.|;|,|\)|\]|\}|$))/,
|
|
{ token: 'regexp', bracket: '@open', next: '@regexp' }
|
|
],
|
|
|
|
// delimiters and operators
|
|
[/[()\[\]]/, '@brackets'],
|
|
[/[<>](?!@symbols)/, '@brackets'],
|
|
[/!(?=([^=]|$))/, 'delimiter'],
|
|
[
|
|
/@symbols/,
|
|
{
|
|
cases: {
|
|
'@operators': 'delimiter',
|
|
'@default': ''
|
|
}
|
|
}
|
|
],
|
|
|
|
// numbers
|
|
[/(@digits)[eE]([\-+]?(@digits))?/, 'number.float'],
|
|
[/(@digits)\.(@digits)([eE][\-+]?(@digits))?/, 'number.float'],
|
|
[/0[xX](@hexdigits)n?/, 'number.hex'],
|
|
[/0[oO]?(@octaldigits)n?/, 'number.octal'],
|
|
[/0[bB](@binarydigits)n?/, 'number.binary'],
|
|
[/(@digits)n?/, 'number'],
|
|
|
|
// delimiter: after number because of .\d floats
|
|
[/[;,.]/, 'delimiter'],
|
|
|
|
// strings
|
|
[/"([^"\\]|\\.)*$/, 'string.invalid'], // non-teminated string
|
|
[/'([^'\\]|\\.)*$/, 'string.invalid'], // non-teminated string
|
|
[/"/, 'string', '@string_double'],
|
|
[/'/, 'string', '@string_single'],
|
|
[/`/, 'string', '@string_backtick']
|
|
],
|
|
|
|
whitespace: [
|
|
[/[ \t\r\n]+/, ''],
|
|
[/\/\*\*(?!\/)/, 'comment.doc', '@jsdoc'],
|
|
[/\/\*/, 'comment', '@comment'],
|
|
[/\/\/.*$/, 'comment']
|
|
],
|
|
|
|
comment: [
|
|
[/[^\/*]+/, 'comment'],
|
|
[/\*\//, 'comment', '@pop'],
|
|
[/[\/*]/, 'comment']
|
|
],
|
|
|
|
jsdoc: [
|
|
[/[^\/*]+/, 'comment.doc'],
|
|
[/\*\//, 'comment.doc', '@pop'],
|
|
[/[\/*]/, 'comment.doc']
|
|
],
|
|
|
|
// We match regular expression quite precisely
|
|
regexp: [
|
|
[
|
|
/(\{)(\d+(?:,\d*)?)(\})/,
|
|
['regexp.escape.control', 'regexp.escape.control', 'regexp.escape.control']
|
|
],
|
|
[
|
|
/(\[)(\^?)(?=(?:[^\]\\\/]|\\.)+)/,
|
|
['regexp.escape.control', { token: 'regexp.escape.control', next: '@regexrange' }]
|
|
],
|
|
[/(\()(\?:|\?=|\?!)/, ['regexp.escape.control', 'regexp.escape.control']],
|
|
[/[()]/, 'regexp.escape.control'],
|
|
[/@regexpctl/, 'regexp.escape.control'],
|
|
[/[^\\\/]/, 'regexp'],
|
|
[/@regexpesc/, 'regexp.escape'],
|
|
[/\\\./, 'regexp.invalid'],
|
|
[
|
|
/(\/)([dgimsuy]*)/,
|
|
[{ token: 'regexp', bracket: '@close', next: '@pop' }, 'keyword.other']
|
|
]
|
|
],
|
|
|
|
regexrange: [
|
|
[/-/, 'regexp.escape.control'],
|
|
[/\^/, 'regexp.invalid'],
|
|
[/@regexpesc/, 'regexp.escape'],
|
|
[/[^\]]/, 'regexp'],
|
|
[
|
|
/\]/,
|
|
{
|
|
token: 'regexp.escape.control',
|
|
next: '@pop',
|
|
bracket: '@close'
|
|
}
|
|
]
|
|
],
|
|
|
|
string_double: [
|
|
[/[^\\"]+/, 'string'],
|
|
[/@escapes/, 'string.escape'],
|
|
[/\\./, 'string.escape.invalid'],
|
|
[/"/, 'string', '@pop']
|
|
],
|
|
|
|
string_single: [
|
|
[/[^\\']+/, 'string'],
|
|
[/@escapes/, 'string.escape'],
|
|
[/\\./, 'string.escape.invalid'],
|
|
[/'/, 'string', '@pop']
|
|
],
|
|
|
|
string_backtick: [
|
|
[/\$\{/, { token: 'delimiter.bracket', next: '@bracketCounting' }],
|
|
[/[^\\`$]+/, 'string'],
|
|
[/@escapes/, 'string.escape'],
|
|
[/\\./, 'string.escape.invalid'],
|
|
[/`/, 'string', '@pop']
|
|
],
|
|
|
|
bracketCounting: [
|
|
[/\{/, 'delimiter.bracket', '@bracketCounting'],
|
|
[/\}/, 'delimiter.bracket', '@pop'],
|
|
{ include: 'common' }
|
|
]
|
|
}
|
|
}
|
|
|
|
let divEl: HTMLDivElement | null = null
|
|
let editor: meditor.IStandaloneCodeEditor
|
|
let model: meditor.ITextModel
|
|
|
|
const { componentControl, selectedComponent } = getContext<AppViewerContext>(
|
|
'AppViewerContext'
|
|
) || { componentControl: writable({}), selectedComponent: writable([]) }
|
|
|
|
if ($selectedComponent) {
|
|
$componentControl[$selectedComponent[0]] = {
|
|
...$componentControl[$selectedComponent[0]],
|
|
setCode: (value: string) => {
|
|
code = value
|
|
setCode(value)
|
|
}
|
|
}
|
|
}
|
|
|
|
export let code: string = ''
|
|
export let hash: string = createHash()
|
|
export let automaticLayout = true
|
|
export let extraLib: string = ''
|
|
export let autoHeight = true
|
|
export let fixedOverflowWidgets = true
|
|
export let fontSize = 16
|
|
|
|
if (typeof code != 'string') {
|
|
code = ''
|
|
}
|
|
|
|
const lang = 'template'
|
|
const dispatch = createEventDispatcher()
|
|
|
|
const uri = `file:///${hash}.ts`
|
|
|
|
buildWorkerDefinition('../../../workers', import.meta.url, false)
|
|
|
|
export function insertAtCursor(code: string): void {
|
|
if (editor) {
|
|
editor.trigger('keyboard', 'type', { text: code })
|
|
}
|
|
}
|
|
|
|
export function setCode(ncode: string): void {
|
|
code = ncode
|
|
if (editor) {
|
|
editor.setValue(ncode)
|
|
}
|
|
}
|
|
|
|
export function getCode(): string {
|
|
return editor?.getValue() ?? ''
|
|
}
|
|
|
|
let cip
|
|
let extraModel
|
|
|
|
let width = 0
|
|
let widgets: HTMLElement | undefined = document.getElementById('monaco-widgets-root') ?? undefined
|
|
|
|
let initialized = false
|
|
async function loadMonaco() {
|
|
await initializeVscode()
|
|
initialized = true
|
|
languages.typescript.javascriptDefaults.setCompilerOptions({
|
|
target: languages.typescript.ScriptTarget.Latest,
|
|
allowNonTsExtensions: true,
|
|
noLib: true
|
|
})
|
|
|
|
languages.register({ id: 'template' })
|
|
|
|
// Register a tokens provider for the language
|
|
languages.registerTokensProviderFactory('template', {
|
|
create: () => language as languages.IMonarchLanguage
|
|
})
|
|
|
|
languages.setLanguageConfiguration('template', conf)
|
|
|
|
model = meditor.createModel(code, lang, mUri.parse(uri))
|
|
|
|
model.updateOptions(updateOptions)
|
|
|
|
editor = meditor.create(divEl as HTMLDivElement, {
|
|
...editorConfig(code, lang, automaticLayout, fixedOverflowWidgets),
|
|
model,
|
|
overflowWidgetsDomNode: widgets,
|
|
lineNumbers: 'off',
|
|
fontSize,
|
|
suggestOnTriggerCharacters: true,
|
|
lineDecorationsWidth: 14
|
|
})
|
|
|
|
editor.onDidFocusEditorText(() => {
|
|
dispatch('focus')
|
|
|
|
editor.addCommand(KeyMod.CtrlCmd | KeyCode.KeyS, function () {})
|
|
})
|
|
|
|
extraModel = meditor.createModel('`' + model.getValue() + '`', 'javascript')
|
|
const worker = await languages.typescript.getJavaScriptWorker()
|
|
const client = await worker(extraModel.uri)
|
|
|
|
cip = languages.registerCompletionItemProvider('template', {
|
|
triggerCharacters: ['.'],
|
|
|
|
provideCompletionItems: async (model, position) => {
|
|
extraModel.setValue('`' + model.getValue() + '`')
|
|
|
|
const offset = model.getOffsetAt(position) + 1
|
|
const info = await client.getCompletionsAtPosition(extraModel.uri.toString(), offset)
|
|
if (!info) {
|
|
return { suggestions: [] }
|
|
}
|
|
const wordInfo = model.getWordUntilPosition(position)
|
|
const wordRange = new Range(
|
|
position.lineNumber,
|
|
wordInfo.startColumn,
|
|
position.lineNumber,
|
|
wordInfo.endColumn
|
|
)
|
|
|
|
const suggestions = info.entries
|
|
.filter((x) => x.kind != 'keyword' && x.kind != 'var')
|
|
.map((entry) => {
|
|
let range = wordRange
|
|
if (entry.replacementSpan) {
|
|
const p1 = model.getPositionAt(entry.replacementSpan.start)
|
|
const p2 = model.getPositionAt(
|
|
entry.replacementSpan.start + entry.replacementSpan.length
|
|
)
|
|
range = new Range(p1.lineNumber, p1.column, p2.lineNumber, p2.column)
|
|
}
|
|
|
|
const tags: languages.CompletionItemTag[] = []
|
|
if (entry.kindModifiers?.indexOf('deprecated') !== -1) {
|
|
tags.push(languages.CompletionItemTag.Deprecated)
|
|
}
|
|
return {
|
|
uri: model.uri,
|
|
position: position,
|
|
offset: offset,
|
|
range: range,
|
|
label: entry.name,
|
|
insertText: entry.name,
|
|
sortText: entry.sortText,
|
|
kind: convertKind(entry.kind),
|
|
tags
|
|
}
|
|
})
|
|
return { suggestions }
|
|
},
|
|
resolveCompletionItem: async (item: languages.CompletionItem, token: any) => {
|
|
extraModel.setValue('`' + model.getValue() + '`')
|
|
|
|
const myItem = <any>item
|
|
const position = myItem.position
|
|
const offset = myItem.offset
|
|
|
|
const details = await client.getCompletionEntryDetails(
|
|
extraModel.uri.toString(),
|
|
offset,
|
|
myItem.label
|
|
)
|
|
if (!details) {
|
|
return myItem
|
|
}
|
|
return <any>{
|
|
uri: model.uri,
|
|
position: position,
|
|
label: details.name,
|
|
kind: convertKind(details.kind),
|
|
detail: displayPartsToString(details.displayParts),
|
|
documentation: {
|
|
value: createDocumentationString(details)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
let timeoutModel: NodeJS.Timeout | undefined = undefined
|
|
editor.onDidChangeModelContent((event) => {
|
|
timeoutModel && clearTimeout(timeoutModel)
|
|
timeoutModel = setTimeout(() => {
|
|
code = getCode()
|
|
dispatch('change', { code })
|
|
}, 200)
|
|
})
|
|
|
|
if (autoHeight) {
|
|
const updateHeight = () => {
|
|
const contentHeight = Math.min(1000, editor.getContentHeight())
|
|
if (divEl) {
|
|
divEl.style.height = `${contentHeight}px`
|
|
}
|
|
try {
|
|
editor.layout({ width, height: contentHeight })
|
|
} catch {}
|
|
}
|
|
editor.onDidContentSizeChange(updateHeight)
|
|
updateHeight()
|
|
}
|
|
|
|
editor.onDidFocusEditorText(() => {
|
|
dispatch('focus')
|
|
})
|
|
|
|
editor.onDidBlurEditorText(() => {
|
|
code = getCode()
|
|
})
|
|
}
|
|
|
|
export function focus() {
|
|
editor?.focus()
|
|
}
|
|
|
|
let mounted = false
|
|
onMount(async () => {
|
|
if (BROWSER) {
|
|
await loadMonaco()
|
|
mounted = true
|
|
}
|
|
})
|
|
|
|
$: mounted && extraLib && initialized && loadExtraLib()
|
|
|
|
function loadExtraLib() {
|
|
const stdLib = { content: libStdContent, filePath: 'es5.d.ts' }
|
|
if (extraLib != '') {
|
|
languages.typescript.javascriptDefaults.setExtraLibs([
|
|
{
|
|
content: extraLib,
|
|
filePath: 'windmill.d.ts'
|
|
},
|
|
stdLib
|
|
])
|
|
} else {
|
|
languages.typescript.javascriptDefaults.setExtraLibs([stdLib])
|
|
}
|
|
}
|
|
|
|
onDestroy(() => {
|
|
try {
|
|
model && model.dispose()
|
|
editor && editor.dispose()
|
|
cip && cip.dispose()
|
|
extraModel && extraModel.dispose()
|
|
} catch (err) {}
|
|
})
|
|
</script>
|
|
|
|
<EditorTheme />
|
|
|
|
<div
|
|
bind:this={divEl}
|
|
style="height: 18px;"
|
|
class="{$$props.class ?? ''} border template rounded min-h-4 mx-0.5 overflow-clip"
|
|
bind:clientWidth={width}
|
|
/>
|
|
|
|
<style>
|
|
:global(.template .mtk20) {
|
|
color: black !important;
|
|
}
|
|
</style>
|