diff --git a/src/core.ts b/src/core.ts index e96f584..10b178d 100644 --- a/src/core.ts +++ b/src/core.ts @@ -4,47 +4,50 @@ import type { EditorPlugin } from "./plugins/index.js"; import { hookScroll } from "./scroll.js"; import { injectStyle } from "./style.js"; -export interface ShikiOptions { - /** - * Control the rendering of line numbers. - * Defaults to `on`. - */ - readonly lineNumbers?: "on" | "off"; - /** - * Should the editor be read only. - * Defaults to false. - */ - readonly readOnly?: boolean; +export interface IndentOptions { /** * The number of spaces a tab is equal to. * This setting is overridden based on the file contents when `detectIndentation` is on. * Defaults to 4. */ - readonly tabSize?: number; + readonly tabSize: number; /** * Insert spaces when pressing `Tab`. * This setting is overridden based on the file contents when `detectIndentation` is on. * Defaults to true. */ - readonly insertSpaces?: boolean; + readonly insertSpaces: boolean; } -export interface InitOptions { - readonly value?: string; +export interface EditorOptions extends IndentOptions { + /** + * Control the rendering of line numbers. + * Defaults to `on`. + */ + readonly lineNumbers: "on" | "off"; + /** + * Should the editor be read only. + * Defaults to false. + */ + readonly readOnly: boolean; readonly language: "text" | BundledLanguage; readonly theme: "none" | BundledTheme; } -export interface UpdateOptions extends ShikiOptions { +export interface InitOptions extends Pick { readonly value?: string; - readonly language?: "text" | BundledLanguage; - readonly theme?: "none" | BundledTheme; +} + +export interface UpdateOptions extends Partial {} + +interface EditorOptionsWithValue extends EditorOptions { + readonly value: string; } interface ShikiCodeFactory { create(domElement: HTMLElement, highlighter: Highlighter, options: InitOptions): ShikiCode; - withOptions(options: Readonly): ShikiCodeFactory; - withPlugins(...plugins: EditorPlugin[]): ShikiCodeFactory; + withOptions(options: UpdateOptions): ShikiCodeFactory; + withPlugins(...plugins: readonly EditorPlugin[]): ShikiCodeFactory; } export interface ShikiCode { @@ -74,8 +77,6 @@ export interface ShikiCode { dispose(): void; } -type FullOptions = Required; - const defaultOptions = { lineNumbers: "on", readOnly: false, @@ -105,7 +106,7 @@ export function shikiCode(): ShikiCodeFactory { function create( domElement: HTMLElement, highlighter: Highlighter, - editor_options: FullOptions, + editor_options: EditorOptionsWithValue, plugin_list: EditorPlugin[], ): ShikiCode { const doc = domElement.ownerDocument; @@ -172,12 +173,12 @@ function create( updateContainer(domElement, highlighter, newOptions.theme!); } - const should_rerender = shouldRerender(editor_options, newOptions, input.value); + const should_rerender = shouldRerender(editor_options, newOptions); Object.assign(editor_options, newOptions); if (should_rerender) { - forceRender(newOptions.value === void 0 ? input.value : newOptions.value); + forceRender(); } }, @@ -204,7 +205,7 @@ function initContainer(container: HTMLElement) { container.style.position = "relative"; } -function shouldUpdateContainer(config: FullOptions, newOptions: UpdateOptions) { +function shouldUpdateContainer(config: EditorOptions, newOptions: UpdateOptions) { return newOptions.theme !== void 0 && newOptions.theme !== config.theme; } @@ -224,7 +225,7 @@ function initIO(input: HTMLTextAreaElement, output: HTMLElement) { output.classList.add("shikicode", "output"); } -function shouldUpdateIO(config: FullOptions, newOptions: UpdateOptions) { +function shouldUpdateIO(config: EditorOptions, newOptions: UpdateOptions) { return ( (newOptions.lineNumbers !== void 0 && newOptions.lineNumbers !== config.lineNumbers) || (newOptions.tabSize !== void 0 && newOptions.tabSize !== config.tabSize) || @@ -264,10 +265,9 @@ function render(output: HTMLElement, highlighter: Highlighter, value: string, la }); } -function shouldRerender(config: FullOptions, newOptions: UpdateOptions, value: string) { +function shouldRerender(options: EditorOptions, newOptions: UpdateOptions) { return ( - (newOptions.theme !== void 0 && newOptions.theme !== config.theme) || - (newOptions.language !== void 0 && newOptions.language !== config.language) || - (newOptions.value !== void 0 && newOptions.value !== value) + (newOptions.theme !== void 0 && newOptions.theme !== options.theme) || + (newOptions.language !== void 0 && newOptions.language !== options.language) ); } diff --git a/src/plugins/common.ts b/src/plugins/common.ts index 705e162..65282d7 100644 --- a/src/plugins/common.ts +++ b/src/plugins/common.ts @@ -107,3 +107,22 @@ export function setRangeText( } } } + +export interface InputState { + /** + * The whole text content. + */ + value: string; + /** + * The start of the selection. + */ + selectionStart: number; + /** + * The end of the selection. + */ + selectionEnd: number; + /** + * The direction of the selection. + */ + selectionDirection?: "forward" | "backward" | "none"; +} diff --git a/src/plugins/index.ts b/src/plugins/index.ts index 866019d..089a5a2 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -1,16 +1,10 @@ -import type { BundledLanguage, BundledTheme } from "shiki"; -import type { ShikiCode, ShikiOptions } from "../core.js"; +import type { EditorOptions, ShikiCode } from "../core.js"; export type IDisposable = () => void; -export type { ShikiCode } from "../core.js"; - -export interface PluginOptions extends ShikiOptions { - readonly language: "text" | BundledLanguage; - readonly theme: "none" | BundledTheme; -} +export type { EditorOptions, IndentOptions, ShikiCode } from "../core.js"; export type EditorPlugin = { - (editor: ShikiCode, options: PluginOptions): IDisposable; + (editor: ShikiCode, options: EditorOptions): IDisposable; }; export * from "./autoload.js"; diff --git a/src/plugins/tab.ts b/src/plugins/tab.ts index e29efa8..eb2487b 100644 --- a/src/plugins/tab.ts +++ b/src/plugins/tab.ts @@ -1,29 +1,12 @@ -import { ceilTab, floorTab, setRangeText, visibleWidthFromLeft, visibleWidthLeadingSpace } from "./common.js"; -import type { IDisposable, ShikiCode } from "./index.js"; - -export interface IndentConfig { - tabSize: number; - insertSpaces: boolean; -} - -export interface State { - /** - * The whole text content. - */ - value: string; - /** - * The start of the selection. - */ - selectionStart: number; - /** - * The end of the selection. - */ - selectionEnd: number; - /** - * The direction of the selection. - */ - selectionDirection?: "forward" | "backward" | "none"; -} +import { + ceilTab, + floorTab, + setRangeText, + visibleWidthFromLeft, + visibleWidthLeadingSpace, + type InputState, +} from "./common.js"; +import type { IDisposable, IndentOptions, ShikiCode } from "./index.js"; export interface PatchAction { value: string; @@ -51,21 +34,21 @@ export interface Action { const empty_action: Action = {}; -export function indentText(input: State, config: IndentConfig): Action { +export function indentText(input: InputState, options: IndentOptions): Action { if ( input.selectionStart !== input.selectionEnd && (bothEndsSelected(input.value, input.selectionStart, input.selectionEnd) || input.value.slice(input.selectionStart, input.selectionEnd).includes("\n")) ) { - return blockIndentText(input, config); + return blockIndentText(input, options); } - return simpleIndentText(input, config); + return simpleIndentText(input, options); } -function simpleIndentText(input: State, config: IndentConfig): Action { +function simpleIndentText(input: InputState, options: IndentOptions): Action { const { value, selectionStart, selectionEnd } = input; - const { tabSize, insertSpaces } = config; + const { tabSize, insertSpaces } = options; if (!insertSpaces) { return { @@ -90,8 +73,8 @@ function simpleIndentText(input: State, config: IndentConfig): Action { }; } -function blockIndentText(input: State, config: IndentConfig): Action { - const { tabSize, insertSpaces } = config; +function blockIndentText(input: InputState, options: IndentOptions): Action { + const { tabSize, insertSpaces } = options; const { value, selectionStart, selectionEnd, selectionDirection } = input; const block_start = getLineStart(value, selectionStart); @@ -171,8 +154,8 @@ function blockIndentText(input: State, config: IndentConfig): Action { } satisfies Action; } -export function outdentText(input: State, config: IndentConfig): Action { - const { tabSize, insertSpaces } = config; +export function outdentText(input: InputState, options: IndentOptions): Action { + const { tabSize, insertSpaces } = options; const { value, selectionStart, selectionEnd, selectionDirection } = input; const block_start = getLineStart(value, selectionStart); @@ -253,7 +236,7 @@ export function outdentText(input: State, config: IndentConfig): Action { } satisfies Action; } -function enter(input: State, config: IndentConfig): Action { +function enter(input: InputState, options: IndentOptions): Action { if (input.selectionStart !== input.selectionEnd) { return empty_action; } @@ -264,23 +247,23 @@ function enter(input: State, config: IndentConfig): Action { } const line = value.slice(line_start, selectionStart); - let [leading_space] = visibleWidthLeadingSpace(line, config.tabSize); - leading_space = floorTab(leading_space, config.tabSize); + let [leading_space] = visibleWidthLeadingSpace(line, options.tabSize); + leading_space = floorTab(leading_space, options.tabSize); let indent_space = leading_space; switch (value[selectionStart - 1]) { case "(": case "[": case "{": { - indent_space += config.tabSize; + indent_space += options.tabSize; } } let replacement = "\n"; - if (config.insertSpaces) { + if (options.insertSpaces) { replacement += " ".repeat(indent_space); } else { - replacement += "\t".repeat(indent_space / config.tabSize); + replacement += "\t".repeat(indent_space / options.tabSize); } let select: SelectAction | undefined; @@ -295,10 +278,10 @@ function enter(input: State, config: IndentConfig): Action { direction: "none", }; - if (config.insertSpaces) { + if (options.insertSpaces) { replacement += "\n" + " ".repeat(leading_space); } else { - replacement += "\n" + "\t".repeat(leading_space / config.tabSize); + replacement += "\n" + "\t".repeat(leading_space / options.tabSize); } } } @@ -314,7 +297,7 @@ function enter(input: State, config: IndentConfig): Action { }; } -function backspace(input: State, config: IndentConfig): Action { +function backspace(input: InputState, options: IndentOptions): Action { const { value, selectionStart, selectionEnd } = input; if (selectionStart !== selectionEnd) { return empty_action; @@ -331,14 +314,14 @@ function backspace(input: State, config: IndentConfig): Action { switch (value[i]) { case " ": { width++; - if (width % config.tabSize === 0) { + if (width % options.tabSize === 0) { last_tab_stop = i; } break; } case "\t": { last_tab_stop = i; - width = ceilTab(width + 1, config.tabSize); + width = ceilTab(width + 1, options.tabSize); break; } default: { @@ -398,14 +381,14 @@ function getLineEnd(text: string, index: number): number { /** * A plugin that automatically inserts or removes indentation. */ -export function hookTab({ input }: ShikiCode, config: IndentConfig): IDisposable { +export function hookTab({ input }: ShikiCode, options: IndentOptions): IDisposable { const onKeydown = (e: KeyboardEvent) => { switch (e.key) { case "Tab": { e.preventDefault(); const action = e.shiftKey ? outdentText : indentText; - const { patch, select } = action(e.target as HTMLTextAreaElement, config); + const { patch, select } = action(e.target as HTMLTextAreaElement, options); if (patch) { setRangeText(input, patch.value, patch.start, patch.end, patch.mode); input.dispatchEvent(new Event("input")); @@ -419,7 +402,7 @@ export function hookTab({ input }: ShikiCode, config: IndentConfig): IDisposable } case "Enter": { - const { patch, select } = enter(e.target as HTMLTextAreaElement, config); + const { patch, select } = enter(e.target as HTMLTextAreaElement, options); if (patch || select) { e.preventDefault(); } @@ -436,7 +419,7 @@ export function hookTab({ input }: ShikiCode, config: IndentConfig): IDisposable } case "Backspace": { - const { select } = backspace(e.target as HTMLTextAreaElement, config); + const { select } = backspace(e.target as HTMLTextAreaElement, options); if (select) { input.setSelectionRange(select.start, select.end, select.direction); input.dispatchEvent(new Event("selectionchange"));