mirror of
https://github.com/go-gitea/gitea.git
synced 2026-02-14 18:47:20 +01:00
Currently, pressing the space key in the Monaco editor scrolls the page instead of inserting a space if the editor is focused. This PR stops the space key event from propagating to parent elements, which prevents unwanted page scrolling while still allowing Monaco to handle space input normally. Changes: - disable Monaco editContext No changes to default editor behavior are needed; Monaco automatically inserts the space character. --------- Signed-off-by: silverwind <me@silverwind.io> Co-authored-by: silverwind <me@silverwind.io>
256 lines
9.8 KiB
TypeScript
256 lines
9.8 KiB
TypeScript
import tinycolor from 'tinycolor2';
|
|
import {basename, extname, isObject, isDarkTheme} from '../utils.ts';
|
|
import {onInputDebounce} from '../utils/dom.ts';
|
|
import type MonacoNamespace from 'monaco-editor';
|
|
|
|
type Monaco = typeof MonacoNamespace;
|
|
type IStandaloneCodeEditor = MonacoNamespace.editor.IStandaloneCodeEditor;
|
|
type IEditorOptions = MonacoNamespace.editor.IEditorOptions;
|
|
type IGlobalEditorOptions = MonacoNamespace.editor.IGlobalEditorOptions;
|
|
type ITextModelUpdateOptions = MonacoNamespace.editor.ITextModelUpdateOptions;
|
|
type MonacoOpts = IEditorOptions & IGlobalEditorOptions & ITextModelUpdateOptions;
|
|
|
|
type CodeEditorConfig = {
|
|
indent_style?: 'tab' | 'space',
|
|
indent_size?: number,
|
|
tab_width?: string | number, // backend emits this as string
|
|
trim_trailing_whitespace?: boolean,
|
|
};
|
|
|
|
const languagesByFilename: Record<string, string> = {};
|
|
const languagesByExt: Record<string, string> = {};
|
|
|
|
const baseOptions: MonacoOpts = {
|
|
fontFamily: 'var(--fonts-monospace)',
|
|
fontSize: 14, // https://github.com/microsoft/monaco-editor/issues/2242
|
|
guides: {bracketPairs: false, indentation: false},
|
|
links: false,
|
|
minimap: {enabled: false},
|
|
occurrencesHighlight: 'off',
|
|
overviewRulerLanes: 0,
|
|
renderLineHighlight: 'all',
|
|
renderLineHighlightOnlyWhenFocus: true,
|
|
rulers: [],
|
|
scrollbar: {horizontalScrollbarSize: 6, verticalScrollbarSize: 6, alwaysConsumeMouseWheel: false},
|
|
scrollBeyondLastLine: false,
|
|
automaticLayout: true,
|
|
indentSize: 'tabSize',
|
|
wrappingIndent: 'none',
|
|
wordWrapBreakAfterCharacters: '',
|
|
wordWrapBreakBeforeCharacters: '',
|
|
matchBrackets: 'never',
|
|
editContext: false, // https://github.com/microsoft/monaco-editor/issues/5081
|
|
};
|
|
|
|
function getCodeEditorConfig(input: HTMLInputElement): CodeEditorConfig | null {
|
|
const json = input.getAttribute('data-code-editor-config');
|
|
if (!json) return null;
|
|
try {
|
|
return JSON.parse(json);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function initLanguages(monaco: Monaco): void {
|
|
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;
|
|
}
|
|
if (id === 'typescript') {
|
|
monaco.typescript.typescriptDefaults.setCompilerOptions({
|
|
// this is needed to suppress error annotations in tsx regarding missing --jsx flag.
|
|
jsx: monaco.typescript.JsxEmit.Preserve,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function getLanguage(filename: string): string {
|
|
return languagesByFilename[filename] || languagesByExt[extname(filename)] || 'plaintext';
|
|
}
|
|
|
|
function updateEditor(monaco: Monaco, editor: IStandaloneCodeEditor, filename: string, lineWrapExts: string[]): void {
|
|
editor.updateOptions(getFileBasedOptions(filename, lineWrapExts));
|
|
const model = editor.getModel();
|
|
if (!model) return;
|
|
const language = model.getLanguageId();
|
|
const newLanguage = getLanguage(filename);
|
|
if (language !== newLanguage) monaco.editor.setModelLanguage(model, newLanguage);
|
|
// TODO: Need to update the model uri with the new filename, but there is no easy way currently, see
|
|
// https://github.com/microsoft/monaco-editor/discussions/3751
|
|
}
|
|
|
|
// export editor for customization - https://github.com/go-gitea/gitea/issues/10409
|
|
function exportEditor(editor: IStandaloneCodeEditor): void {
|
|
if (!window.codeEditors) window.codeEditors = [];
|
|
if (!window.codeEditors.includes(editor)) window.codeEditors.push(editor);
|
|
}
|
|
|
|
function updateTheme(monaco: Monaco): void {
|
|
// https://github.com/microsoft/monaco-editor/issues/2427
|
|
// also, monaco can only parse 6-digit hex colors, so we convert the colors to that format
|
|
const styles = window.getComputedStyle(document.documentElement);
|
|
const getColor = (name: string) => tinycolor(styles.getPropertyValue(name).trim()).toString('hex6');
|
|
|
|
monaco.editor.defineTheme('gitea', {
|
|
base: isDarkTheme() ? 'vs-dark' : 'vs',
|
|
inherit: true,
|
|
rules: [
|
|
{
|
|
background: getColor('--color-code-bg'),
|
|
token: '',
|
|
},
|
|
],
|
|
colors: {
|
|
'editor.background': getColor('--color-code-bg'),
|
|
'editor.foreground': getColor('--color-text'),
|
|
'editor.inactiveSelectionBackground': getColor('--color-primary-light-4'),
|
|
'editor.lineHighlightBackground': getColor('--color-editor-line-highlight'),
|
|
'editor.selectionBackground': getColor('--color-primary-light-3'),
|
|
'editor.selectionForeground': getColor('--color-primary-light-3'),
|
|
'editorLineNumber.background': getColor('--color-code-bg'),
|
|
'editorLineNumber.foreground': getColor('--color-secondary-dark-6'),
|
|
'editorWidget.background': getColor('--color-body'),
|
|
'editorWidget.border': getColor('--color-secondary'),
|
|
'input.background': getColor('--color-input-background'),
|
|
'input.border': getColor('--color-input-border'),
|
|
'input.foreground': getColor('--color-input-text'),
|
|
'scrollbar.shadow': getColor('--color-shadow-opaque'),
|
|
'progressBar.background': getColor('--color-primary'),
|
|
'focusBorder': '#0000', // prevent blue border
|
|
},
|
|
});
|
|
}
|
|
|
|
type CreateMonacoOpts = MonacoOpts & {language?: string};
|
|
|
|
export async function createMonaco(textarea: HTMLTextAreaElement, filename: string, opts: CreateMonacoOpts): Promise<{monaco: Monaco, editor: IStandaloneCodeEditor}> {
|
|
const monaco = await import(/* webpackChunkName: "monaco" */'monaco-editor');
|
|
|
|
initLanguages(monaco);
|
|
let {language, ...other} = opts;
|
|
if (!language) language = getLanguage(filename);
|
|
|
|
const container = document.createElement('div');
|
|
container.className = 'monaco-editor-container';
|
|
if (!textarea.parentNode) throw new Error('Parent node absent');
|
|
textarea.parentNode.append(container);
|
|
|
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
|
updateTheme(monaco);
|
|
});
|
|
updateTheme(monaco);
|
|
|
|
const model = monaco.editor.createModel(textarea.value, language, monaco.Uri.file(filename));
|
|
|
|
const editor = monaco.editor.create(container, {
|
|
model,
|
|
theme: 'gitea',
|
|
...baseOptions,
|
|
...other,
|
|
});
|
|
|
|
monaco.editor.addKeybindingRules([
|
|
{keybinding: monaco.KeyCode.Enter, command: null}, // disable enter from accepting code completion
|
|
]);
|
|
|
|
model.onDidChangeContent(() => {
|
|
textarea.value = editor.getValue({
|
|
preserveBOM: true,
|
|
lineEnding: '',
|
|
});
|
|
textarea.dispatchEvent(new Event('change')); // seems to be needed for jquery-are-you-sure
|
|
});
|
|
|
|
const elEditorOptions = textarea.closest('form')?.querySelector('.code-editor-options');
|
|
if (elEditorOptions) {
|
|
elEditorOptions.querySelector<HTMLSelectElement>('.js-indent-style-select')!.addEventListener('change', (e) => {
|
|
const insertSpaces = (e.target as HTMLSelectElement).value === 'space';
|
|
editor.updateOptions({insertSpaces, useTabStops: !insertSpaces});
|
|
});
|
|
elEditorOptions.querySelector<HTMLSelectElement>('.js-indent-size-select')!.addEventListener('change', (e) => {
|
|
const tabSize = Number((e.target as HTMLSelectElement).value);
|
|
editor.updateOptions({tabSize});
|
|
});
|
|
elEditorOptions.querySelector<HTMLSelectElement>('.js-line-wrap-select')!.addEventListener('change', (e) => {
|
|
const wordWrap = (e.target as HTMLSelectElement).value as IEditorOptions['wordWrap'];
|
|
editor.updateOptions({wordWrap});
|
|
});
|
|
}
|
|
|
|
exportEditor(editor);
|
|
|
|
const loading = document.querySelector('.editor-loading');
|
|
if (loading) loading.remove();
|
|
|
|
return {monaco, editor};
|
|
}
|
|
|
|
function getFileBasedOptions(filename: string, lineWrapExts: string[]): MonacoOpts {
|
|
return {
|
|
wordWrap: (lineWrapExts || []).includes(extname(filename)) ? 'on' : 'off',
|
|
};
|
|
}
|
|
|
|
function togglePreviewDisplay(previewable: boolean): void {
|
|
const previewTab = document.querySelector<HTMLElement>('a[data-tab="preview"]');
|
|
if (!previewTab) return;
|
|
|
|
if (previewable) {
|
|
previewTab.style.display = '';
|
|
} else {
|
|
previewTab.style.display = 'none';
|
|
// If the "preview" tab was active, user changes the filename to a non-previewable one,
|
|
// then the "preview" tab becomes inactive (hidden), so the "write" tab should become active
|
|
if (previewTab.classList.contains('active')) {
|
|
const writeTab = document.querySelector<HTMLElement>('a[data-tab="write"]');
|
|
writeTab?.click();
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function createCodeEditor(textarea: HTMLTextAreaElement, filenameInput: HTMLInputElement): Promise<IStandaloneCodeEditor> {
|
|
const filename = basename(filenameInput.value);
|
|
const previewableExts = new Set((textarea.getAttribute('data-previewable-extensions') || '').split(','));
|
|
const lineWrapExts = (textarea.getAttribute('data-line-wrap-extensions') || '').split(',');
|
|
const isPreviewable = previewableExts.has(extname(filename));
|
|
const editorConfig = getCodeEditorConfig(filenameInput);
|
|
|
|
togglePreviewDisplay(isPreviewable);
|
|
|
|
const {monaco, editor} = await createMonaco(textarea, filename, {
|
|
...getFileBasedOptions(filenameInput.value, lineWrapExts),
|
|
...getMonacoOptsByCodeEditorConfig(editorConfig),
|
|
});
|
|
|
|
filenameInput.addEventListener('input', onInputDebounce(() => {
|
|
const filename = filenameInput.value;
|
|
const previewable = previewableExts.has(extname(filename));
|
|
togglePreviewDisplay(previewable);
|
|
updateEditor(monaco, editor, filename, lineWrapExts);
|
|
}));
|
|
|
|
return editor;
|
|
}
|
|
|
|
function getMonacoOptsByCodeEditorConfig(ec: CodeEditorConfig | null): MonacoOpts {
|
|
if (!ec || !isObject(ec)) return {};
|
|
|
|
const opts: MonacoOpts = {};
|
|
opts.detectIndentation = !ec.indent_style || !ec.indent_size;
|
|
|
|
// with indentSize='tabSize', this also controls the `indentSize` option
|
|
if (!opts.detectIndentation) {
|
|
opts.tabSize = Number(ec.tab_width) || Number(ec.indent_size) || 4;
|
|
}
|
|
|
|
opts.trimAutoWhitespace = ec.trim_trailing_whitespace === true;
|
|
opts.insertSpaces = ec.indent_style === 'space';
|
|
opts.useTabStops = ec.indent_style === 'tab';
|
|
return opts;
|
|
}
|