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