From 9d3966772b0423b9ab451635670a4565c0eaf262 Mon Sep 17 00:00:00 2001 From: SirBlobby Date: Sat, 5 Sep 2026 22:07:19 -0400 Subject: [PATCH] Auto-close delimiters and wrap selected text --- src/lib/components/Editor.svelte | 4 ++- src/lib/ts/editor-brackets.ts | 55 ++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 src/lib/ts/editor-brackets.ts diff --git a/src/lib/components/Editor.svelte b/src/lib/components/Editor.svelte index 1a322e5..bfd6ed3 100644 --- a/src/lib/components/Editor.svelte +++ b/src/lib/components/Editor.svelte @@ -14,6 +14,7 @@ import { page } from '$app/stores'; import { LSPClient, languageServerExtensions } from "@codemirror/lsp-client"; import { setDiagnostics, lintGutter } from '@codemirror/lint'; + import { bracketExtensions, typstBracketSettings } from '../ts/editor-brackets'; let { ytext = undefined, @@ -168,7 +169,7 @@ ); const isToml = (filePath ?? '').toLowerCase().endsWith('.toml'); - const languageExtension = isToml ? StreamLanguage.define(toml) : myLang; + const languageExtension = isToml ? StreamLanguage.define(toml) : [myLang, typstBracketSettings(myLang)]; const completionExtensions = isToml ? [] : [autocompletion({ override: [typstCompletions] })]; state = EditorState.create({ @@ -177,6 +178,7 @@ lineNumbers(), lintGutter(), history(), + ...bracketExtensions, keymap.of([...defaultKeymap, ...historyKeymap, indentWithTab] as any), languageExtension, yCollab(activeText, activeProvider.awareness), diff --git a/src/lib/ts/editor-brackets.ts b/src/lib/ts/editor-brackets.ts new file mode 100644 index 0000000..9d72d02 --- /dev/null +++ b/src/lib/ts/editor-brackets.ts @@ -0,0 +1,55 @@ +import { closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete'; +import { EditorView, keymap } from '@codemirror/view'; +import { EditorSelection, Prec } from '@codemirror/state'; +import type { Language } from '@codemirror/language'; + +const autoClosedDelimiters = ['(', '[', '{', '"', '$']; + +const charactersAllowedAfterOpening = ')]}:;>,.$'; + +const markupDelimiters: Record = { + '*': '*', + _: '_', + '`': '`', + '<': '>' +}; + +export function typstBracketSettings(language: Language) { + return language.data.of({ + closeBrackets: { + brackets: autoClosedDelimiters, + before: charactersAllowedAfterOpening + } + }); +} + +function surroundSelection(view: EditorView, typedText: string) { + const closingText = markupDelimiters[typedText]; + if (!closingText) return false; + + const state = view.state; + if (state.readOnly || state.selection.ranges.every((range) => range.empty)) return false; + + const changes = state.changeByRange((range) => { + if (range.empty) return { range }; + const shift = typedText.length; + return { + changes: [ + { from: range.from, insert: typedText }, + { from: range.to, insert: closingText } + ], + range: EditorSelection.range(range.anchor + shift, range.head + shift) + }; + }); + + view.dispatch(state.update(changes, { scrollIntoView: true, userEvent: 'input.type' })); + return true; +} + +export const bracketExtensions = [ + closeBrackets(), + Prec.high( + EditorView.inputHandler.of((view, _from, _to, typedText) => surroundSelection(view, typedText)) + ), + keymap.of(closeBracketsKeymap) +];