ui: monaco: use monaco-vscode-api

This commit is contained in:
undefined committed 2024-06-08 17:42:40 +08:00
1 parent c05accf343
commit 5be19fffd4
11 files changed
+280 -461

No files matched your search

@@ -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 }));
}
@@ -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<void> = new vscode.EventEmitter<void>();
public readonly onDidChangeCodeLenses: vscode.Event<void> = 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),
// };
},
}, '@');
@@ -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'));
}
@@ -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'],
},
],
});
+55 -24
View File
@@ -1,10 +1,25 @@
import { getFeatures, load as loadModule } from '../../lazyload';
/* eslint-disable no-await-in-loop */
let loaded;
const localeLoader: Partial<Record<string, () => Promise<void>>> = {
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<string, any> = {};
/** @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;
@@ -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;
},
},
});
};
@@ -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;
}