gradio/frontend
1442k
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 