diff --git a/packages/ui-default/build/config/webpack.ts b/packages/ui-default/build/config/webpack.ts index f4ede936d..0e7d38fe3 100644 --- a/packages/ui-default/build/config/webpack.ts +++ b/packages/ui-default/build/config/webpack.ts @@ -5,7 +5,6 @@ import CopyWebpackPlugin from 'copy-webpack-plugin'; import { EsbuildPlugin } from 'esbuild-loader'; import { DuplicatesPlugin } from 'inspectpack/plugin'; import ExtractCssPlugin from 'mini-css-extract-plugin'; -import MonacoWebpackPlugin from 'monaco-editor-webpack-plugin'; import { dirname } from 'path'; import webpack from 'webpack'; import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; @@ -14,17 +13,6 @@ import WebpackBar from 'webpackbar'; import root from '../utils/root'; export default function (env: { watch?: boolean, production?: boolean, measure?: boolean } = {}) { - function esbuildLoader() { - return { - loader: 'esbuild-loader', - options: { - loader: 'tsx', - target: 'es2015', - sourcemap: true, - }, - }; - } - function cssLoader() { return { loader: 'css-loader', @@ -97,10 +85,14 @@ export default function (env: { watch?: boolean, production?: boolean, measure?: chunkFilename: '[name].[chunkhash:6].chunk.js', }, resolve: { - extensions: ['.js', '.jsx', '.ts', '.tsx', '.cjs'], + extensions: ['.js', '.jsx', '.ts', '.tsx'], alias: { vj: root(), }, + fallback: { + url: require.resolve('url/'), + util: require.resolve('util/'), + }, }, module: { rules: [ @@ -153,14 +145,7 @@ export default function (env: { watch?: boolean, production?: boolean, measure?: }, }, { - test: /\.[mc]?[jt]sx?$/, - exclude: [/@types\//, /components\/message\//, /entry\.js/], - type: 'javascript/auto', - use: [esbuildLoader()], - }, - { - test: /\.[mc]?[jt]sx?$/, - include: [/components\/message\//, /entry\.js/], + test: /\.m?(jsx|tsx|ts)?$/, type: 'javascript/auto', use: [{ loader: 'ts-loader', @@ -185,6 +170,7 @@ export default function (env: { watch?: boolean, production?: boolean, measure?: }, optimization: { splitChunks: { + chunks: 'async', minSize: 256000, maxAsyncRequests: 5, maxInitialRequests: 3, @@ -198,12 +184,15 @@ export default function (env: { watch?: boolean, production?: boolean, measure?: enforce: true, }, vendors: { - test: /[\\/]node_modules[\\/].+\.([jt]sx?|json|yaml)$/, + test: /[\\/]node_modules[\\/].+\.([jt]sx?|json|yaml|txt|mp3|md)$/, priority: -10, name(module) { const packageName = module.context.replace(/\\/g, '/').split('node_modules/').pop().split('/')[0]; - if (packageName === 'monaco-editor-nls') { - return `i.monaco.${module.userRequest.replace(/\\/g, '/').split('/').pop().split('.')[0]}`; + if (packageName.startsWith('@codingame')) { + const subName = module.context.replace(/\\/g, '/').split('node_modules/').pop().split('/')[1]; + if (subName.startsWith('monaco-vscode-language-pack-')) { + return `i.${subName.split('pack-').pop()}`; + } } return `n.${packageName.replace('@', '')}`; }, @@ -239,7 +228,6 @@ export default function (env: { watch?: boolean, production?: boolean, measure?: jQuery: 'jquery', 'window.jQuery': 'jquery', React: 'react', - monaco: 'monaco-editor/esm/vs/editor/editor.api', }), new ExtractCssPlugin({ filename: '[name].css?[fullhash:6]', @@ -257,31 +245,26 @@ export default function (env: { watch?: boolean, production?: boolean, measure?: { from: `${dirname(require.resolve('monaco-themes/package.json'))}/themes`, to: 'monaco/themes/' }, ], }), - sentryWebpackPlugin({ - authToken: process.env.SENTRY_AUTH_TOKEN, - org: 'hydro-dev', - project: 'hydro-web', - url: 'https://sentry.hydro.ac', - }), new webpack.DefinePlugin({ 'process.env.VERSION': JSON.stringify(require('@hydrooj/ui-default/package.json').version), }), new webpack.optimize.MinChunkSizePlugin({ minChunkSize: 128000, }), - new webpack.NormalModuleReplacementPlugin(/\/(vscode-)?nls\.js/, require.resolve('../../components/monaco/nls')), + new webpack.NormalModuleReplacementPlugin(/^node:/, (r) => { r.request = r.request.replace(/^node:/, ''); }), new webpack.NormalModuleReplacementPlugin(/^prettier[$/]/, root('../../modules/nop.ts')), - new MonacoWebpackPlugin({ - filename: '[name].[hash:6].worker.js', - customLanguages: [{ - label: 'yaml', - entry: require.resolve('monaco-yaml/index.js'), - worker: { - id: 'vs/language/yaml/yamlWorker', - entry: require.resolve('monaco-yaml/yaml.worker.js'), - }, - }], - }), + new webpack.NormalModuleReplacementPlugin( + /@codingame\/monaco-vscode-(css|fsharp|html|groovy|scss|xml)-default-extension/, + root('../../modules/nop.ts'), + ), + ...(env.production ? [ + sentryWebpackPlugin({ + authToken: process.env.SENTRY_AUTH_TOKEN, + org: 'hydro-dev', + project: 'hydro-web', + url: 'https://sentry.hydro.ac', + }), + ] : []), ...env.measure ? [ new BundleAnalyzerPlugin({ analyzerPort: 'auto' }), new DuplicatesPlugin(), diff --git a/packages/ui-default/components/editor/index.ts b/packages/ui-default/components/editor/index.ts index c7573ac1f..67263759e 100644 --- a/packages/ui-default/components/editor/index.ts +++ b/packages/ui-default/components/editor/index.ts @@ -1,4 +1,5 @@ import $ from 'jquery'; +import type { editor } from 'monaco-editor'; import { nanoid } from 'nanoid'; import DOMAttachedObject from 'vj/components/DOMAttachedObject'; import Notification from 'vj/components/notification'; @@ -49,8 +50,8 @@ type Options = MonacoOptions & VditorOptions; export default class Editor extends DOMAttachedObject { static DOMAttachKey = 'vjEditorInstance'; - model: import('../monaco').default.editor.IModel; - editor: import('../monaco').default.editor.IStandaloneCodeEditor; + model: editor.IModel; + editor: editor.IStandaloneCodeEditor; vditor: import('vditor').default; isValid: boolean; @@ -70,7 +71,7 @@ export default class Editor extends DOMAttachedObject { autoResize = true, autoLayout = true, hide = [], lineNumbers = 'on', } = this.options; - const { monaco, registerAction } = await load([language]); + const { monaco } = await load([language]); const { $dom } = this; const hasFocus = $dom.is(':focus') || $dom.hasClass('autofocus'); const origin = $dom.get(0); @@ -85,7 +86,7 @@ export default class Editor extends DOMAttachedObject { || monaco.editor.createModel(value, language === 'auto' ? undefined : language, monaco.Uri.parse(model)) : model; if (!this.options.model) this.model.setValue(value); - const cfg: import('../monaco').default.editor.IStandaloneEditorConstructionOptions = { + const cfg: editor.IStandaloneEditorConstructionOptions = { theme, lineNumbers, glyphMargin: true, @@ -146,7 +147,7 @@ export default class Editor extends DOMAttachedObject { } this.editor.deltaDecorations([], ranges.map((range) => ({ range, options: { inlineClassName: 'decoration-hide' } }))); } - registerAction(this.editor, this.model, this.$dom); + // registerAction(this.editor, this.model, this.$dom); if (autoResize) { this.editor.onDidChangeModelDecorations(() => { updateEditorHeight(); // typing diff --git a/packages/ui-default/components/monaco/index.ts b/packages/ui-default/components/monaco/index.ts deleted file mode 100644 index b6a8204ab..000000000 --- a/packages/ui-default/components/monaco/index.ts +++ /dev/null @@ -1,176 +0,0 @@ -import './monaco.styl'; - -import $ from 'jquery'; -import { EditorAction, registerEditorAction } from 'monaco-editor/esm/vs/editor/browser/editorExtensions'; -import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; -import { IQuickInputService } from 'monaco-editor/esm/vs/platform/quickinput/common/quickInput'; -import list from 'monaco-themes/themes/themelist.json'; -import { nanoid } from 'nanoid'; -import { i18n, request } from 'vj/utils'; - -export default monaco; -export const customOptions: monaco.editor.IStandaloneDiffEditorConstructionOptions = JSON.parse(localStorage.getItem('editor.config') || '{}'); -export function saveCustomOptions() { - localStorage.setItem('editor.config', JSON.stringify(customOptions)); -} - -const loaded = {}; -async function fetchTheme(id: string, label: string) { - if (loaded[id]) return; - const res = await fetch(`${UiContext.cdn_prefix}monaco/themes/${label}.json`); - const data = await res.json(); - monaco.editor.defineTheme(id, data); - loaded[id] = true; -} -export const loadThemePromise = customOptions.theme - ? fetchTheme(customOptions.theme, list[customOptions.theme]) - : Promise.resolve(); - -// 这破烂 monaco 能不能给个 typings 啊 -// 我翻源码真的很累的好吧 -class ChangeThemeAction extends EditorAction { - constructor() { - super({ - id: 'hydro.changeEditorTheme', - label: i18n('Change Theme'), - alias: 'Change Theme', - }); - } - - async run(accessor, editor: monaco.editor.IStandaloneCodeEditor) { - const items = Object.keys(list).map((key) => ({ - label: list[key], - command: key, - })); - const quickInputService = accessor.get(IQuickInputService); - const oldTheme = (editor as any)._themeService._theme; - let focus = ''; - const selected = await quickInputService.pick(items, { - canPickMany: false, - async onDidFocus(item) { - focus = item.command; - await fetchTheme(item.command, item.label); - if (focus === item.command) monaco.editor.setTheme(item.command); - }, - }); - if (!selected) { - const themeId = oldTheme.type ? `${oldTheme.base}-${oldTheme.type}` : oldTheme.id; - return monaco.editor.setTheme(themeId); - } - await fetchTheme(selected.command, selected.label); - customOptions.theme = selected.command; - saveCustomOptions(); - return monaco.editor.setTheme(selected.command); - } -} - -registerEditorAction(ChangeThemeAction); - -const pagename = document.documentElement.getAttribute('data-page'); -const isProblemPage = ['problem_create', 'problem_edit'].includes(pagename); -const isProblemEdit = pagename === 'problem_edit'; -function handlePasteEvent(editor: monaco.editor.IStandaloneCodeEditor) { - window.addEventListener('paste', (ev: ClipboardEvent) => { - if (!editor.hasTextFocus()) return; - const selection = editor.getSelection(); - const { items } = ev.clipboardData; - let wrapper = ['', '']; - let ext; - const matches = items[0].type.match(/^image\/(png|jpg|jpeg|gif)$/i); - if (matches) { - wrapper = ['![image](', ')']; - [, ext] = matches; - } else if (items[0].type === 'application/x-zip-compressed') { - wrapper = ['[file](', ')']; - ext = 'zip'; - } - if (!ext) return; - ev.preventDefault(); - ev.stopPropagation(); - const filename = `${nanoid()}.${ext}`; - const data = new FormData(); - data.append('filename', filename); - data.append('file', items[0].getAsFile()); - data.append('operation', 'upload_file'); - if (isProblemEdit) data.append('type', 'additional_file'); - let range: monaco.Range = null; - editor.executeEdits('', [{ - range: new monaco.Range(selection.endLineNumber, selection.endColumn, selection.endLineNumber, selection.endColumn), - text: `![image](${i18n('Preparing Upload...')}) `, - }], (inverseOp) => { range = inverseOp[0].range; return null; }); - editor.setPosition(editor.getSelection().getEndPosition()); - - function updateText(text: string) { - const pos = editor.getSelection(); - const rangeBefore = range; - editor.executeEdits('', [{ range, text: `${wrapper[0]}${text}${wrapper[1]} ` }], (inverseOp) => { - range = inverseOp[0].range; - if (pos.endLineNumber !== pos.startLineNumber || pos.endLineNumber !== range.endLineNumber) return null; - const delta = rangeBefore.endColumn - range.endColumn; - editor.setPosition(new monaco.Position(pos.endLineNumber, pos.endColumn - delta)); - return null; - }); - } - let progress = 0; - request.postFile(isProblemEdit ? './files' : '/file', data, { - xhr() { - const xhr = new XMLHttpRequest(); - xhr.upload.addEventListener('loadstart', () => updateText(i18n('Uploading...'))); - xhr.upload.addEventListener('progress', (e) => { - if (!e.lengthComputable) return; - const percentComplete = Math.round((e.loaded / e.total) * 100); - if (percentComplete === progress) return; - progress = percentComplete; - updateText(`${i18n('Uploading...')} ${percentComplete}%`); - }, false); - return xhr; - }, - }) - .then(() => updateText(`${isProblemPage ? 'file://' : `/file/${UserContext._id}/`}${filename}`)) - .catch((e) => { - console.error(e); - updateText(`${i18n('Upload Failed')}: ${e.message}`); - }); - }, { capture: true }); -} - -export function registerAction( - editor: monaco.editor.IStandaloneCodeEditor, - model: monaco.editor.IModel, - element?, -) { - if (element) { - editor.addAction({ - id: 'hydro.submitForm', - label: 'Submit', - keybindings: [ - monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, - monaco.KeyMod.WinCtrl | monaco.KeyCode.Enter, - ], - run: () => { - const form = $(element).closest('form'); - if (form.find('[data-default-submit]').length) form.find('[data-default-submit]').click(); - else form.submit(); - }, - }); - } - editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyP, () => { - editor.getAction('editor.action.quickCommand').run(); - }); - editor.addCommand(monaco.KeyMod.Alt | monaco.KeyMod.Shift | monaco.KeyCode.KeyF, () => { - const action = editor.getAction('editor.action.format') || editor.getAction('editor.action.formatDocument'); - if (action) action.run(); - }); - editor.onDidChangeConfiguration(() => { - const current = editor.getOption(monaco.editor.EditorOptions.fontSize.id); - customOptions.fontSize = current; - saveCustomOptions(); - }); - if (model.getLanguageId() === 'markdown') { - const suggestWidget = (editor.getContribution('editor.contrib.suggestController') as any).widget?.value; - if (suggestWidget?._setDetailsVisible) suggestWidget._setDetailsVisible(true); - handlePasteEvent(editor); - } - if (!customOptions.theme) return null; - return loadThemePromise.then(() => editor.updateOptions({ theme: customOptions.theme })); -} diff --git a/packages/ui-default/components/monaco/languages/markdown.ts b/packages/ui-default/components/monaco/languages/markdown.ts index f3a57f61a..5adb89dab 100644 --- a/packages/ui-default/components/monaco/languages/markdown.ts +++ b/packages/ui-default/components/monaco/languages/markdown.ts @@ -1,109 +1,131 @@ +import 'vscode/localExtensionHost'; + import keyword from 'emojis-keywords'; import list from 'emojis-list'; -import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; import qface from 'qface'; -import { api, gql } from 'vj/utils'; +import * as vscode from 'vscode'; -function emoji(range) { - return keyword.map((i, index) => ({ - label: `${list[index]} ${i}`, - kind: monaco.languages.CompletionItemKind.Keyword, - documentation: i, - insertText: list[index], - range, - })); -} - -function qqEmoji(range) { - return qface.data.flatMap((i) => { - const url = qface.getUrl(i.QSid, 'https://qq-face.vercel.app'); - return [i.QDes.substring(1), ...(i.Input || [])].map((input) => ({ - label: `/${input}`, - kind: monaco.languages.CompletionItemKind.Keyword, - documentation: { value: `![${i.QDes}](${url})`, isTrusted: true }, - insertText: `![${i.Input ? i.Input[0] : i.QDes.substring(1)}](${url} =32x32) `, - range, - })); - }); -} - -monaco.editor.registerCommand('hydro.openUserPage', (accesser, uid) => { +vscode.commands.registerCommand('hydro.openUserPage', (accesser, uid) => { window.open(`/user/${uid}`); }); -monaco.languages.registerCodeLensProvider('markdown', { - async provideCodeLenses(model) { - const users = model.findMatches('\\[\\]\\(/user/(\\d+)\\)', true, true, true, null, true); - if (!users.length) { - return { - lenses: [], - dispose: () => { }, - }; - } - const { data } = await api(gql` - users(ids: ${users.map((i) => +i.matches[1])}) { - _id - uname - } - `); - return { - lenses: users.map((i, index) => ({ - range: i.range, - id: `${index}.${i.matches[1]}`, - command: { - id: 'hydro.openUserPage', - arguments: [i.matches[1]], - title: `@${data.users.find((doc) => doc._id.toString() === i.matches[1])?.uname || i.matches[1]}`, - }, - })), - dispose: () => { }, - }; - }, - resolveCodeLens(model, codeLens) { - return codeLens; - }, -}); +class CodelensProvider implements vscode.CodeLensProvider { + private regex: RegExp; + private _onDidChangeCodeLenses: vscode.EventEmitter = new vscode.EventEmitter(); + public readonly onDidChangeCodeLenses: vscode.Event = this._onDidChangeCodeLenses.event; -monaco.languages.registerCompletionItemProvider('markdown', { - async provideCompletionItems(model, position) { - const word = model.getWordAtPosition(position); - if (word.word.length < 2) return { suggestions: [] }; - const prefix = model.getValueInRange({ - startLineNumber: position.lineNumber, - endLineNumber: position.lineNumber, - startColumn: word.startColumn - 1, - endColumn: word.startColumn, + constructor() { + this.regex = /\[\]\(\/user\/(\d+)\)/g; + console.log('aaa', vscode); + + vscode.workspace.onDidChangeConfiguration((_) => { + this._onDidChangeCodeLenses.fire(); }); - if (![':', '/', '@'].includes(prefix)) return { suggestions: [] }; - const range = { - startLineNumber: position.lineNumber, - endLineNumber: position.lineNumber, - startColumn: word.startColumn - 1, - endColumn: word.endColumn, - }; - if (prefix === '@') { - const users = await api(gql` - users(search: ${word.word}) { - _id - uname - avatarUrl - priv - } - `, ['data', 'users']); - return { - suggestions: users.map((i) => ({ - label: { label: `@${i.uname}`, description: `UID=${i._id}` }, - kind: monaco.languages.CompletionItemKind.Property, - documentation: { value: `[](#loader) ![avatar](${new URL(i.avatarUrl, window.location.href)})`, isTrusted: true }, - insertText: `@[](/user/${i._id}) `, - range, - sortText: i.priv === 0 ? '0' : '1', - tags: i.priv === 0 ? [1] : [], - })), - }; + } + + public async provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken) { + const codeLenses: vscode.CodeLens[] = []; + console.log('cl', 123); + const regex = new RegExp(this.regex); + const text = document.getText(); + let matches; + const users = []; + while ((matches = regex.exec(text)) !== null) { + const line = document.lineAt(document.positionAt(matches.index).line); + const indexOf = line.text.indexOf(matches[0]); + const position = new vscode.Position(line.lineNumber, indexOf); + const range = document.getWordRangeAtPosition(position, new RegExp(this.regex)); + if (range) users.push({ matches, range }); } - return { - suggestions: prefix === ':' ? emoji(range) : qqEmoji(range), - }; + // const { data } = await api(gql` + // users(ids: ${users.map((i) => +i.matches[1])}) { + // _id + // uname + // } + // `); + for (const { matches, range } of users) { + codeLenses.push(new vscode.CodeLens(range, { + command: 'hydro.openUserPage', + arguments: [matches[1]], + title: `@${matches[1]}`, + })); + } + + return codeLenses; + } + + public resolveCodeLens(codeLens: vscode.CodeLens, token: vscode.CancellationToken) { + return codeLens; + } +} +vscode.languages.registerCodeLensProvider({ language: 'markdown' }, new CodelensProvider()); + +vscode.languages.registerCompletionItemProvider({ language: 'markdown' }, { + async provideCompletionItems(document, position, token, context) { + const range = new vscode.Range(new vscode.Position(position.line, position.character - 1), position); + return context.triggerCharacter === ':' + ? keyword.map((i, index) => { + const c = new vscode.CompletionItem(`${list[index]} ${i}`, vscode.CompletionItemKind.Keyword); + c.insertText = list[index]; + c.documentation = i; + c.range = range; + return c; + }) + : qface.data.flatMap((i) => { + const url = qface.getUrl(i.QSid, 'https://qq-face.vercel.app'); + return [i.QDes.substring(1), ...(i.Input || [])].map((input) => { + const c = new vscode.CompletionItem(`/${input}`, vscode.CompletionItemKind.Keyword); + c.insertText = `![${i.Input ? i.Input[0] : i.QDes.substring(1)}](${url} =32x32) `; + c.documentation = new vscode.MarkdownString(`![${i.QDes}](${url})`); + c.range = range; + return c; + }); + }); }, -}); +}, ':', '/'); + +vscode.languages.registerCompletionItemProvider('*', { + async provideCompletionItems(document, position, token, context) { + return []; + const range = document.getWordRangeAtPosition(position); + const word = document.getText(range); + if (word.length < 2) return []; + // const prefix = document.getValueInRange({ + // startLineNumber: position.lineNumber, + // endLineNumber: position.lineNumber, + // startColumn: word.startColumn - 1, + // endColumn: word.startColumn, + // }); + // if (![':', '/', '@'].includes(prefix)) return { suggestions: [] }; + // const range = { + // startLineNumber: position.lineNumber, + // endLineNumber: position.lineNumber, + // startColumn: word.startColumn - 1, + // endColumn: word.endColumn, + // }; + // if (prefix === '@') { + // const users = await api(gql` + // users(search: ${word.word}) { + // _id + // uname + // avatarUrl + // priv + // } + // `, ['data', 'users']); + // return { + // suggestions: users.map((i) => ({ + // label: { label: `@${i.uname}`, description: `UID=${i._id}` }, + // kind: monaco.languages.CompletionItemKind.Property, + // documentation: { value: `[](#loader) ![avatar](${new URL(i.avatarUrl, window.location.href)})`, isTrusted: true }, + // insertText: `@[](/user/${i._id}) `, + // range, + // sortText: i.priv === 0 ? '0' : '1', + // tags: i.priv === 0 ? [1] : [], + // })), + // }; + // } + // return { + // suggestions: prefix === ':' ? emoji(range) : qqEmoji(range), + // }; + }, +}, '@'); diff --git a/packages/ui-default/components/monaco/languages/typescript.ts b/packages/ui-default/components/monaco/languages/typescript.ts deleted file mode 100644 index 3125bed82..000000000 --- a/packages/ui-default/components/monaco/languages/typescript.ts +++ /dev/null @@ -1,45 +0,0 @@ -import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; - -const types = require.context('@types/node/', true, /\.d\.ts$/, 'lazy-once'); - -const diagnosticsOptions: monaco.languages.typescript.DiagnosticsOptions = { - noSemanticValidation: false, - noSyntaxValidation: false, - noSuggestionDiagnostics: true, -}; -const compilerOptions: monaco.languages.typescript.CompilerOptions = { - target: monaco.languages.typescript.ScriptTarget.ES2020, - module: monaco.languages.typescript.ModuleKind.ESNext, - allowNonTsExtensions: true, -}; -monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions(diagnosticsOptions); -monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions(diagnosticsOptions); -monaco.languages.typescript.javascriptDefaults.setCompilerOptions(compilerOptions); -monaco.languages.typescript.typescriptDefaults.setCompilerOptions(compilerOptions); -const libSource = [ - 'declare function readline(): string;', - 'declare function print(content: string): void;', -].join('\n'); -const libUri = 'ts:filename/basic.d.ts'; -monaco.languages.typescript.javascriptDefaults.addExtraLib(libSource, libUri); -monaco.editor.createModel(libSource, 'typescript', monaco.Uri.parse(libUri)); -const modules = []; - -export async function loadTypes() { - for (const key of types.keys()) { - if (!key.startsWith('.')) continue; - const m = await types(key); - const val = m.replace('declare var require: NodeRequire;', ''); - if (val.includes('declare module ')) { - modules.push(val.toString().split('declare module \'')[1].split('\'')[0]); - } - const uri = `ts:node/${key.split('./')[1]}`; - monaco.languages.typescript.javascriptDefaults.addExtraLib(val, uri); - monaco.editor.createModel(val, 'typescript', monaco.Uri.parse(uri)); - } - let val = 'declare var require:'; - for (const m of modules) val += `((id:'${m}')=>(typeof import('${m}')))&`; - val += '((id:string)=>any)'; - monaco.languages.typescript.javascriptDefaults.addExtraLib(val, 'ts:node/require.d.ts'); - monaco.editor.createModel(val, 'typescript', monaco.Uri.parse('ts:node/require.d.ts')); -} diff --git a/packages/ui-default/components/monaco/languages/yaml.ts b/packages/ui-default/components/monaco/languages/yaml.ts deleted file mode 100644 index 55ae952ee..000000000 --- a/packages/ui-default/components/monaco/languages/yaml.ts +++ /dev/null @@ -1,22 +0,0 @@ -import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; -import { configureMonacoYaml } from 'monaco-yaml'; -import problemConfigSchema from '../schema/problemconfig'; - -configureMonacoYaml(monaco, { - validate: true, - enableSchemaRequest: true, - hover: true, - completion: true, - format: false, - schemas: [ - { - uri: 'https://hydro.js.org/schema/problemConfig.json', - fileMatch: ['hydro://problem/file/config.yaml'], - schema: problemConfigSchema as any, - }, - { - uri: new URL('/manage/config/schema.json', window.location.href).toString(), - fileMatch: ['hydro://system/setting.yaml'], - }, - ], -}); diff --git a/packages/ui-default/components/monaco/loader.ts b/packages/ui-default/components/monaco/loader.ts index 3a84b4fbe..fd8ee9ffa 100644 --- a/packages/ui-default/components/monaco/loader.ts +++ b/packages/ui-default/components/monaco/loader.ts @@ -1,10 +1,25 @@ import { getFeatures, load as loadModule } from '../../lazyload'; +/* eslint-disable no-await-in-loop */ + let loaded; +const localeLoader: Partial Promise>> = { + ko: async () => { + await import('@codingame/monaco-vscode-language-pack-ko'); + }, + zh: async () => { + await import('@codingame/monaco-vscode-language-pack-zh-hans'); + }, + // eslint-disable-next-line @typescript-eslint/naming-convention + 'zh_TW': async () => { + await import('@codingame/monaco-vscode-language-pack-zh-hant'); + }, +}; + const val: Record = {}; /** @deprecated */ -export async function legacyLoadExternalModule(target: string) { +async function legacyLoadExternalModule(target: string) { if (val[target]) return val[target]; const ele = document.createElement('script'); ele.src = target; @@ -18,22 +33,26 @@ export async function legacyLoadExternalModule(target: string) { } const loaders = { - i18n: async () => { - const { setLocaleData } = await import('./nls'); - let resource; - const lang = UserContext.viewLang; - if (lang === 'zh') { - resource = await import('monaco-editor-nls/locale/zh-hans.json'); - } else if (lang === 'zh_TW') { - resource = await import('monaco-editor-nls/locale/zh-hant.json'); - } else if (lang === 'ko') { - resource = await import('monaco-editor-nls/locale/ko.json'); - } - if (resource) setLocaleData(resource); + workbench: async () => { + await Promise.all([ + import('@codingame/monaco-vscode-theme-seti-default-extension'), + import('@codingame/monaco-vscode-media-preview-default-extension'), + import('@codingame/monaco-vscode-markdown-language-features-default-extension'), + import('@codingame/monaco-vscode-markdown-math-default-extension'), + import('@codingame/monaco-vscode-configuration-editing-default-extension'), + import('@codingame/monaco-editor-wrapper/features/viewPanels'), + import('@codingame/monaco-editor-wrapper/features/search'), + ]); }, - markdown: () => import('./languages/markdown'), - typescript: () => import('./languages/typescript').then((m) => m.loadTypes()), - yaml: () => import('./languages/yaml'), + markdown: async () => await Promise.all([ + import('./languages/markdown'), + import('@codingame/monaco-vscode-markdown-basics-default-extension'), + import('@codingame/monaco-vscode-markdown-language-features-default-extension'), + import('@codingame/monaco-vscode-markdown-math-default-extension'), + ]), + typescript: async () => await import('@codingame/monaco-vscode-typescript-language-features-default-extension'), + cpp: async () => await import('@codingame/monaco-vscode-cpp-default-extension'), + yaml: async () => await import('@codingame/monaco-vscode-yaml-default-extension'), external: async (monaco, feat) => { for (const item of await getFeatures(`monaco-${feat}`)) { let apply = typeof item === 'function' @@ -47,19 +66,28 @@ const loaders = { }, }; -let loadPromise = Promise.resolve(); +let loadPromise = localeLoader['zh-hans'](); +let config = false; + +async function initConfigManager(t: typeof import('./monaco.ts')) { + t.registerFile(new t.RegisteredMemoryFile(t.default.Uri.parse('/.vscode/settings.json'), localStorage.getItem('monaco.settings') || '{}')); + const model = await t.default.editor.createModelReference(t.default.Uri.parse('/.vscode/settings.json')); + model.object.textEditorModel?.onDidChangeContent(() => { + const value = model.object.textEditorModel?.getValue() || '{}'; + localStorage.setItem('monaco.settings', value); + t.updateUserConfiguration(value); + }); +} export async function load(features = ['markdown']) { let s = Date.now(); await loadPromise; let resolve; loadPromise = new Promise((r) => { resolve = r; }); + if (!loaded) console.log('Loading monaco editor'); + const res = await import('./monaco'); if (!loaded) { - await loaders.i18n(); - console.log('Loading monaco editor'); - } - const res = await import('./index'); - if (!loaded) { + await res.init(features.includes('workbench')); console.log('Loaded monaco editor in', Date.now() - s, 'ms'); loaded = []; } @@ -83,9 +111,12 @@ export async function load(features = ['markdown']) { console.log('Monaco feat', feat, 'failed to load:', e); } } - await res.loadThemePromise; + if (!config) { + config = true; + await initConfigManager(res); + } resolve(); - return { monaco: res.default, registerAction: res.registerAction, customOptions: res.customOptions }; + return { ...res, monaco: res.default }; } export default load; diff --git a/packages/ui-default/components/monaco/monaco.ts b/packages/ui-default/components/monaco/monaco.ts new file mode 100644 index 000000000..edcabd188 --- /dev/null +++ b/packages/ui-default/components/monaco/monaco.ts @@ -0,0 +1,56 @@ +import '@codingame/monaco-editor-wrapper/features/extensionHostWorker'; +import '@codingame/monaco-editor-wrapper/features/notifications'; +import '@codingame/monaco-vscode-theme-defaults-default-extension'; +import '@codingame/monaco-vscode-json-default-extension'; + +import { + createEditor, initialize, registerFile, registerServices, +} from '@codingame/monaco-editor-wrapper'; +import { RegisteredMemoryFile } from '@codingame/monaco-vscode-files-service-override'; +import * as monaco from 'monaco-editor'; +import { Uri } from 'vscode'; + +export default monaco; +export { createEditor, registerFile, RegisteredMemoryFile }; +export { + getUserConfiguration, getConfiguration, + onUserConfigurationChange, onConfigurationChanged, + updateUserConfiguration, +} from '@codingame/monaco-editor-wrapper'; + +export const init = async (workbench = false) => { + if (workbench) { + const [ + { default: getWorkbenchServiceOverride }, + { default: getMultiDiffEditorServiceOverride }, + { default: getQuickAccessServiceOverride }, + ] = await Promise.all([ + import('@codingame/monaco-vscode-workbench-service-override'), + import('@codingame/monaco-vscode-multi-diff-editor-service-override'), + import('@codingame/monaco-vscode-quickaccess-service-override'), + ]); + registerServices({ + ...getWorkbenchServiceOverride(), + ...getMultiDiffEditorServiceOverride(), + ...getQuickAccessServiceOverride({ + shouldUseGlobalPicker() { + return true; + }, + isKeybindingConfigurationVisible() { + return true; + }, + }), + }); + } + return initialize({ + workspaceProvider: { + trusted: true, + workspace: { + workspaceUri: Uri.file('/'), + }, + async open() { + return false; + }, + }, + }); +}; diff --git a/packages/ui-default/components/monaco/nls.js b/packages/ui-default/components/monaco/nls.js deleted file mode 100644 index f9f3aab8a..000000000 --- a/packages/ui-default/components/monaco/nls.js +++ /dev/null @@ -1,39 +0,0 @@ -function format(message, args) { - let result; - if (!args.length) result = message; - else { - result = String(message).replace(/\{(\d+)\}/g, (match, rest) => { - const index = rest[0]; - return typeof args[index] !== 'undefined' ? args[index] : match; - }); - } - return result; -} - -export const getConfiguredDefaultLocale = () => 'zh'; - -let CURRENT_LOCALE_DATA = {}; // eslint-disable-line @typescript-eslint/naming-convention - -export function localize(path, message, ...args) { - return format(CURRENT_LOCALE_DATA[path.key || path] || message, args); -} - -export function localize2(data, message, ...args) { - const original = localize(message, args); - return { - value: original, - original, - }; -} - -export function setLocaleData(data) { - CURRENT_LOCALE_DATA = Object.assign(...Object.values(data)); -} - -export function loadMessageBundle() { - return localize; -} - -export function config() { - return loadMessageBundle; -} diff --git a/packages/ui-default/components/scratchpad/ScratchpadEditorContainer.tsx b/packages/ui-default/components/scratchpad/ScratchpadEditorContainer.tsx index 6e2256092..c01ea44b7 100644 --- a/packages/ui-default/components/scratchpad/ScratchpadEditorContainer.tsx +++ b/packages/ui-default/components/scratchpad/ScratchpadEditorContainer.tsx @@ -41,22 +41,21 @@ export default connect((state: any) => ({ async componentDidMount() { const value = this.props.value || ''; const { language } = this.props; - const { monaco, registerAction, customOptions } = await load([language]); - const uri = monaco.Uri.parse(`hydro:${UiContext.pdoc.pid || UiContext.pdoc.docId}.${language}`); + const { monaco, createEditor } = await load([language]); + const uri = monaco.Uri.parse(`/${UiContext.pdoc.pid || UiContext.pdoc.docId}.${language}`); this.model = monaco.editor.getModel(uri) || monaco.editor.createModel(value, language, uri); if (this.containerElement) { const config: monaco.editor.IStandaloneEditorConstructionOptions = { theme: 'vs-light', fontFamily: UserContext.codeFontFamily, - ...customOptions, lineNumbers: 'on', glyphMargin: true, lightbulb: { enabled: monaco.editor.ShowLightbulbIconMode.On }, model: this.model, fontLigatures: '', + language, }; - this.editor = monaco.editor.create(this.containerElement, config); - registerAction(this.editor, this.model); + this.editor = createEditor(this.containerElement, config); this.disposable.push( this.editor.onDidChangeModelContent((event) => { if (!this.__prevent_trigger_change_event) { @@ -96,7 +95,7 @@ export default connect((state: any) => ({ if (model && editor && prevProps.language !== language) { const val = model.getValue(LF, false); model.dispose(); - const uri = monaco.Uri.parse(`hydro:${UiContext.pdoc.pid || UiContext.pdoc.docId}.${language}`); + const uri = monaco.Uri.parse(`file:///${UiContext.pdoc.pid || UiContext.pdoc.docId}.${language}`); this.model = monaco.editor.getModel(uri) || monaco.editor.createModel(val, language, uri); editor.setModel(this.model); } diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index 077d83cdc..b01328f46 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -15,6 +15,11 @@ }, "devDependencies": { "@blueprintjs/core": "^5.10.3", + "@codingame/monaco-editor-wrapper": "^6.6.10", + "@codingame/monaco-vscode-language-pack-ko": "5.3.0", + "@codingame/monaco-vscode-language-pack-zh-hans": "5.3.0", + "@codingame/monaco-vscode-language-pack-zh-hant": "5.3.0", + "@codingame/monaco-vscode-typescript-language-features-default-extension": "5.3.0", "@cordisjs/core": "^3.16.2", "@fontsource/dm-mono": "^5.0.20", "@fontsource/fira-code": "^5.0.18", @@ -40,6 +45,7 @@ "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@types/redux-logger": "^3.0.13", + "@types/vscode": "^1.89.0", "@types/webpack-env": "^1.18.5", "@vscode/codicons": "^0.0.36", "ajv": "^8.16.0", @@ -73,9 +79,8 @@ "matchmedia-polyfill": "^0.3.2", "mini-css-extract-plugin": "^2.9.0", "moment": "^2.30.1", - "monaco-editor": "0.46.0", - "monaco-editor-nls": "^3.1.0", - "monaco-editor-webpack-plugin": "^7.1.0", + "monaco-editor": "npm:@codingame/monaco-vscode-editor-api@5.3.0", + "monaco-languageclient": "^8.4.0", "monaco-themes": "^0.4.4", "monaco-yaml": "^5.1.1", "nanoid": "^5.0.7", @@ -117,8 +122,12 @@ "timeago-react": "^3.0.6", "timeago.js": "^4.0.2", "ts-loader": "^9.5.1", + "url": "^0.11.3", + "util": "^0.12.5", "vditor": "^3.10.4", "vinyl-buffer": "^1.0.1", + "vscode": "npm:@codingame/monaco-vscode-api@^5.3.0", + "vscode-languageclient": "^9.0.1", "web-streams-polyfill": "^4.0.0", "webpack": "^5.91.0", "webpack-bundle-analyzer": "^4.10.2",