mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-02-03 20:51:07 -05:00
125 lines
4.8 KiB
TypeScript
125 lines
4.8 KiB
TypeScript
|
|
import {basename, extname} from '../utils.js';
|
||
|
|
import {hideElem, onInputDebounce, showElem} from '../utils/dom.js';
|
||
|
|
import {createCodemirror, type CodemirrorEditor, type EditorOptions} from './codemirror.ts';
|
||
|
|
import {EditorView} from '@codemirror/view';
|
||
|
|
import type {LanguageSupport} from '@codemirror/language';
|
||
|
|
|
||
|
|
interface EditorConfig {
|
||
|
|
indent_style: string;
|
||
|
|
indent_size: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export class SettableEditorView extends EditorView {
|
||
|
|
public setValue(value: string) {
|
||
|
|
// Replace \n with the actual newline character and unescape escaped \n
|
||
|
|
value = value.replaceAll(/(?<!\\)\\n/g, '\n').replaceAll(/\\\\n/g, '\\n');
|
||
|
|
this.dispatch({changes: {from: 0, to: this.state.doc.length, insert: value}});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function getEditorconfig(input: HTMLInputElement): null | EditorConfig {
|
||
|
|
try {
|
||
|
|
return JSON.parse(input.getAttribute('data-editorconfig'));
|
||
|
|
} catch {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function updateEditor(editor: CodemirrorEditor, filename: string, lineWrapExts: string[]) {
|
||
|
|
const fileOption = getFileBasedOptions(filename, lineWrapExts);
|
||
|
|
editor.view.dispatch({
|
||
|
|
effects: editor.compartments.wordWrap.reconfigure(fileOption.wordWrap ? editor.codemirrorView.EditorView.lineWrapping : []),
|
||
|
|
});
|
||
|
|
|
||
|
|
const currentLanguage = editor.compartments.language.get(editor.view.state) as Array<unknown> | LanguageSupport;
|
||
|
|
const newLanguage = editor.codemirrorLanguage.LanguageDescription.matchFilename(editor.languages, filename);
|
||
|
|
if (!currentLanguage || (currentLanguage as Array<unknown>).length === 0 || !newLanguage || (currentLanguage as LanguageSupport).language.name.toLowerCase() !== newLanguage.name.toLowerCase()) {
|
||
|
|
editor.view.dispatch({
|
||
|
|
effects: editor.compartments.language.reconfigure(newLanguage ? await newLanguage.load() : []),
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function getFileBasedOptions(filename: string, lineWrapExts: string[]): Pick<EditorOptions, 'wordWrap'> {
|
||
|
|
return {
|
||
|
|
wordWrap: (lineWrapExts || []).includes(extname(filename)),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function togglePreviewDisplay(previewable: boolean) {
|
||
|
|
const previewTab = document.querySelector('.item[data-tab="preview"]') as HTMLAnchorElement;
|
||
|
|
if (!previewTab) return;
|
||
|
|
|
||
|
|
if (previewable) {
|
||
|
|
const newUrl = (previewTab.getAttribute('data-url') || '').replace(/(.*)\/.*/, `$1/markup`);
|
||
|
|
previewTab.setAttribute('data-url', newUrl);
|
||
|
|
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('.item[data-tab="write"]') as HTMLAnchorElement;
|
||
|
|
writeTab.click();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function createCodeEditor(textarea: HTMLTextAreaElement, filenameInput: HTMLInputElement): Promise<SettableEditorView> {
|
||
|
|
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 previewable = previewableExts.has(extname(filename));
|
||
|
|
const editorConfig = getEditorconfig(filenameInput);
|
||
|
|
|
||
|
|
togglePreviewDisplay(previewable);
|
||
|
|
|
||
|
|
const editor = await createCodemirror(textarea, filename, {
|
||
|
|
...getFileBasedOptions(filenameInput.value, lineWrapExts),
|
||
|
|
...getEditorConfigOptions(editorConfig),
|
||
|
|
});
|
||
|
|
|
||
|
|
filenameInput.addEventListener('input', onInputDebounce(async () => {
|
||
|
|
const filename = filenameInput.value;
|
||
|
|
const previewable = previewableExts.has(extname(filename));
|
||
|
|
togglePreviewDisplay(previewable);
|
||
|
|
await updateEditor(editor, filename, lineWrapExts);
|
||
|
|
}));
|
||
|
|
|
||
|
|
const searchButton = document.querySelector('#editor-find');
|
||
|
|
searchButton.addEventListener('click', () => {
|
||
|
|
const search = editor.codemirrorSearch;
|
||
|
|
const view = editor.view;
|
||
|
|
|
||
|
|
if (search.searchPanelOpen(view.state)) {
|
||
|
|
search.closeSearchPanel(view);
|
||
|
|
} else {
|
||
|
|
search.openSearchPanel(view);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
const writeTab = document.querySelector('#editor-bar .switch .item[data-tab="write"]');
|
||
|
|
document.querySelector('#editor-bar .switch').addEventListener('click', () => {
|
||
|
|
if (writeTab.classList.contains('active')) {
|
||
|
|
showElem(searchButton);
|
||
|
|
} else {
|
||
|
|
hideElem(searchButton);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
return Object.setPrototypeOf(editor.view, SettableEditorView.prototype);
|
||
|
|
}
|
||
|
|
|
||
|
|
function getEditorConfigOptions(ec: null | EditorConfig): Pick<EditorOptions, 'indentSize' | 'tabSize' | 'indentStyle'> {
|
||
|
|
if (ec === null) {
|
||
|
|
return {indentStyle: 'space'};
|
||
|
|
}
|
||
|
|
|
||
|
|
const opts: ReturnType<typeof getEditorConfigOptions> = {
|
||
|
|
indentStyle: ec.indent_style,
|
||
|
|
};
|
||
|
|
if ('indent_size' in ec) opts.indentSize = Number(ec.indent_size);
|
||
|
|
if ('tab_width' in ec) opts.tabSize = Number(ec.tab_width) || opts.indentSize;
|
||
|
|
return opts;
|
||
|
|
}
|