Skip to content

Commit

Permalink
refactor: editor options interface
Browse files Browse the repository at this point in the history
  • Loading branch information
magic-akari committed Apr 18, 2024
1 parent 383d549 commit 1c78fde
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 90 deletions.
62 changes: 31 additions & 31 deletions src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<EditorOptions, "language" | "theme"> {
readonly value?: string;
readonly language?: "text" | BundledLanguage;
readonly theme?: "none" | BundledTheme;
}

export interface UpdateOptions extends Partial<EditorOptions> {}

interface EditorOptionsWithValue extends EditorOptions {
readonly value: string;
}

interface ShikiCodeFactory {
create(domElement: HTMLElement, highlighter: Highlighter, options: InitOptions): ShikiCode;
withOptions(options: Readonly<UpdateOptions>): ShikiCodeFactory;
withPlugins(...plugins: EditorPlugin[]): ShikiCodeFactory;
withOptions(options: UpdateOptions): ShikiCodeFactory;
withPlugins(...plugins: readonly EditorPlugin[]): ShikiCodeFactory;
}

export interface ShikiCode {
Expand Down Expand Up @@ -74,8 +77,6 @@ export interface ShikiCode {
dispose(): void;
}

type FullOptions = Required<UpdateOptions>;

const defaultOptions = {
lineNumbers: "on",
readOnly: false,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
},

Expand All @@ -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;
}

Expand All @@ -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) ||
Expand Down Expand Up @@ -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)
);
}
19 changes: 19 additions & 0 deletions src/plugins/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
12 changes: 3 additions & 9 deletions src/plugins/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
83 changes: 33 additions & 50 deletions src/plugins/tab.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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 {
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
Expand All @@ -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);
}
}
}
Expand All @@ -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;
Expand All @@ -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: {
Expand Down Expand Up @@ -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"));
Expand All @@ -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();
}
Expand All @@ -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"));
Expand Down

0 comments on commit 1c78fde

Please sign in to comment.