CoolFace
Datasetpublic

gradio/frontend

sourceHugging Faceupdated 21h agoView on Hugging Face
1likes442kdownloads
Code.svelte346 linesDownload Raw Back to shared
1<script lang="ts">2	import { onMount } from "svelte";3	import {4		EditorView,5		ViewUpdate,6		keymap,7		placeholder as placeholderExt,8		lineNumbers9	} from "@codemirror/view";10	import { StateEffect, EditorState, type Extension } from "@codemirror/state";11	import { indentWithTab } from "@codemirror/commands";12	import { autocompletion, acceptCompletion } from "@codemirror/autocomplete";13 14	import { basicDark } from "cm6-theme-basic-dark";15	import { basicLight } from "cm6-theme-basic-light";16	import { basicSetup } from "./extensions";17	import { getLanguageExtension } from "./language";18 19	interface Props {20		class_names?: string;21		value?: string;22		dark_mode: boolean;23		basic?: boolean;24		language: string;25		lines?: number;26		max_lines?: number | null;27		extensions?: Extension[];28		use_tab?: boolean;29		readonly?: boolean;30		placeholder?: string | HTMLElement | null | undefined;31		wrap_lines?: boolean;32		show_line_numbers?: boolean;33		autocomplete?: boolean;34		onchange?: (value: string) => void;35		onblur?: () => void;36		onfocus?: () => void;37		oninput?: () => void;38	}39 40	let {41		class_names = "",42		value = $bindable(),43		dark_mode,44		basic = true,45		language,46		lines = 5,47		max_lines = null,48		extensions = [],49		use_tab = true,50		readonly = false,51		placeholder = undefined,52		wrap_lines = false,53		show_line_numbers = true,54		autocomplete = false,55		onchange,56		onblur,57		onfocus,58		oninput59	}: Props = $props();60 61	let lang_extension: Extension | undefined = $state();62	let element: HTMLDivElement;63	let view: EditorView;64 65	async function get_lang(val: string): Promise<void> {66		const ext = await getLanguageExtension(val);67		lang_extension = ext;68	}69 70	$effect(() => {71		get_lang(language);72	});73 74	$effect(() => {75		lang_extension;76		readonly;77		reconfigure();78	});79 80	$effect(() => {81		set_doc(value);82	});83 84	update_lines();85 86	function set_doc(new_doc: string): void {87		if (view && new_doc !== view.state.doc.toString()) {88			view.dispatch({89				changes: {90					from: 0,91					to: view.state.doc.length,92					insert: new_doc93				}94			});95		}96	}97 98	function update_lines(): void {99		if (view) {100			view.requestMeasure({ read: resize });101		}102	}103 104	function create_editor_view(): EditorView {105		const editorView = new EditorView({106			parent: element,107			state: create_editor_state(value)108		});109		editorView.dom.addEventListener("focus", handle_focus, true);110		editorView.dom.addEventListener("blur", handle_blur, true);111		return editorView;112	}113 114	function handle_focus(): void {115		onfocus?.();116	}117 118	function handle_blur(): void {119		onblur?.();120	}121 122	function getGutterLineHeight(_view: EditorView): string | null {123		let elements = _view.dom.querySelectorAll<HTMLElement>(".cm-gutterElement");124		if (elements.length === 0) {125			return null;126		}127		for (var i = 0; i < elements.length; i++) {128			let node = elements[i];129			let height = getComputedStyle(node)?.height ?? "0px";130			if (height != "0px") {131				return height;132			}133		}134		return null;135	}136 137	function resize(_view: EditorView): any {138		let scroller = _view.dom.querySelector<HTMLElement>(".cm-scroller");139		if (!scroller) {140			return null;141		}142		const lineHeight = getGutterLineHeight(_view);143		if (!lineHeight) {144			return null;145		}146 147		const minLines = lines == 1 ? 1 : lines + 1;148		scroller.style.minHeight = `calc(${lineHeight} * ${minLines})`;149		if (max_lines)150			scroller.style.maxHeight = `calc(${lineHeight} * ${max_lines + 1})`;151	}152 153	import { Transaction } from "@codemirror/state";154 155	function is_user_input(update: ViewUpdate): boolean {156		return update.transactions.some(157			(tr) => tr.annotation(Transaction.userEvent) != null158		);159	}160 161	function handle_change(vu: ViewUpdate): void {162		if (!vu.docChanged) return;163 164		const doc = vu.state.doc;165		const text = doc.toString();166		value = text;167 168		const user_change = is_user_input(vu);169		if (user_change) {170			onchange?.(text);171			oninput?.();172		} else {173			onchange?.(text);174		}175 176		view.requestMeasure({ read: resize });177	}178 179	function get_extensions(): Extension[] {180		const stateExtensions = [181			...get_base_extensions(182				basic,183				use_tab,184				placeholder,185				readonly,186				lang_extension,187				show_line_numbers188			),189			FontTheme,190			...get_theme(),191			...extensions192		];193		return stateExtensions;194	}195 196	const FontTheme = EditorView.theme({197		"&": {198			fontSize: "var(--text-sm)",199			backgroundColor: "var(--border-color-secondary)"200		},201		".cm-content": {202			paddingTop: "5px",203			paddingBottom: "5px",204			color: "var(--body-text-color)",205			fontFamily: "var(--font-mono)",206			minHeight: "100%"207		},208		".cm-gutterElement": {209			marginRight: "var(--spacing-xs)"210		},211		".cm-gutters": {212			marginRight: "1px",213			borderRight: "1px solid var(--border-color-primary)",214			backgroundColor: "var(--block-background-fill);",215			color: "var(--body-text-color-subdued)"216		},217		".cm-focused": {218			outline: "none"219		},220		".cm-scroller": {221			height: "auto"222		},223		".cm-cursor": {224			borderLeftColor: "var(--body-text-color)"225		}226	});227 228	const AutocompleteTheme = EditorView.theme({229		".cm-tooltip-autocomplete": {230			"& > ul": {231				backgroundColor: "var(--background-fill-primary)",232				color: "var(--body-text-color)"233			},234			"& > ul > li[aria-selected]": {235				backgroundColor: "var(--color-accent-soft)",236				color: "var(--body-text-color)"237			}238		}239	});240 241	function create_editor_state(_value: string | null | undefined): EditorState {242		return EditorState.create({243			doc: _value ?? undefined,244			extensions: get_extensions()245		});246	}247 248	function get_base_extensions(249		basic: boolean,250		use_tab: boolean,251		placeholder: string | HTMLElement | null | undefined,252		readonly: boolean,253		lang: Extension | null | undefined,254		show_line_numbers: boolean255	): Extension[] {256		const extensions: Extension[] = [257			EditorView.editable.of(!readonly),258			EditorState.readOnly.of(readonly),259			EditorView.contentAttributes.of({ "aria-label": "Code input container" })260		];261 262		if (basic) {263			extensions.push(basicSetup);264		}265		if (use_tab) {266			extensions.push(267				keymap.of([{ key: "Tab", run: acceptCompletion }, indentWithTab])268			);269		}270		if (placeholder) {271			extensions.push(placeholderExt(placeholder));272		}273		if (lang) {274			extensions.push(lang);275		}276		if (show_line_numbers) {277			extensions.push(lineNumbers());278		}279		if (autocomplete) {280			extensions.push(autocompletion());281			extensions.push(AutocompleteTheme);282		}283 284		extensions.push(EditorView.updateListener.of(handle_change));285		if (wrap_lines) {286			extensions.push(EditorView.lineWrapping);287		}288 289		return extensions;290	}291 292	function get_theme(): Extension[] {293		const extensions: Extension[] = [];294 295		if (dark_mode) {296			extensions.push(basicDark);297		} else {298			extensions.push(basicLight);299		}300		return extensions;301	}302 303	function reconfigure(): void {304		view?.dispatch({305			effects: StateEffect.reconfigure.of(get_extensions())306		});307	}308 309	onMount(() => {310		view = create_editor_view();311		return () => view?.destroy();312	});313</script>314 315<div class="wrap">316	<div class="codemirror-wrapper {class_names}" bind:this={element} />317</div>318 319<style>320	.wrap {321		display: flex;322		flex-direction: column;323		flex-grow: 1;324		margin: 0;325		padding: 0;326		height: 100%;327	}328	.codemirror-wrapper {329		flex-grow: 1;330		overflow: auto;331	}332 333	:global(.cm-editor) {334		height: 100%;335	}336 337	/* Dunno why this doesn't work through the theme API -- don't remove*/338	:global(.cm-selectionBackground) {339		background-color: #b9d2ff30 !important;340	}341 342	:global(.cm-focused) {343		outline: none !important;344	}345</style>346