diff --git a/package.json b/package.json
index f3befb288..544e2f2b6 100644
--- a/package.json
+++ b/package.json
@@ -82,7 +82,6 @@
"bencode": "^4.0.0",
"chardet": "^2.1.1",
"clsx": "^2.1.1",
- "copy-to-clipboard": "^3.3.3",
"crypto-js": "^4.2.0",
"docx-preview": "^0.3.7",
"handlebars": "^4.7.9",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ad0444211..238b059c4 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -71,9 +71,6 @@ importers:
clsx:
specifier: ^2.1.1
version: 2.1.1
- copy-to-clipboard:
- specifier: ^3.3.3
- version: 3.3.3
crypto-js:
specifier: ^4.2.0
version: 4.2.0
@@ -1962,9 +1959,6 @@ packages:
convert-string@0.1.0:
resolution: {integrity: sha512-1KX9ESmtl8xpT2LN2tFnKSbV4NiarbVi8DVb39ZriijvtTklyrT+4dT1wsGMHKD3CJUjXgvJzstm9qL9ICojGA==}
- copy-to-clipboard@3.3.3:
- resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==}
-
core-js-compat@3.48.0:
resolution: {integrity: sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==}
@@ -3389,9 +3383,6 @@ packages:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
- toggle-selection@1.0.6:
- resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==}
-
tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
@@ -5595,10 +5586,6 @@ snapshots:
convert-string@0.1.0: {}
- copy-to-clipboard@3.3.3:
- dependencies:
- toggle-selection: 1.0.6
-
core-js-compat@3.48.0:
dependencies:
browserslist: 4.28.1
@@ -7278,8 +7265,6 @@ snapshots:
dependencies:
is-number: 7.0.0
- toggle-selection@1.0.6: {}
-
tr46@0.0.3: {}
trim-lines@3.0.1: {}
diff --git a/src/components/EncodingSelect.tsx b/src/components/EncodingSelect.tsx
index ae79f1ccb..d4d85c4b9 100644
--- a/src/components/EncodingSelect.tsx
+++ b/src/components/EncodingSelect.tsx
@@ -3,70 +3,114 @@ import { SelectWrapper } from "./Base"
import chardet from "chardet"
import { createEffect } from "solid-js"
+const MIN_CONFIDENCE = 30
+
+const encodingLabels = [
+ "utf-8",
+ "gbk",
+ "gb18030",
+ "ibm866",
+ "iso-8859-2",
+ "iso-8859-3",
+ "iso-8859-4",
+ "iso-8859-5",
+ "iso-8859-6",
+ "iso-8859-7",
+ "iso-8859-8",
+ "iso-8859-8i",
+ "iso-8859-10",
+ "iso-8859-13",
+ "iso-8859-14",
+ "iso-8859-15",
+ "iso-8859-16",
+ "koi8-r",
+ "koi8-u",
+ "macintosh",
+ "windows-874",
+ "windows-1250",
+ "windows-1251",
+ "windows-1252",
+ "windows-1253",
+ "windows-1254",
+ "windows-1255",
+ "windows-1256",
+ "windows-1257",
+ "windows-1258",
+ "x-mac-cyrillic",
+ "big5",
+ "euc-jp",
+ "iso-2022-jp",
+ "shift_jis",
+ "euc-kr",
+ "iso-2022-kr",
+ "utf-16be",
+ "utf-16le",
+ "x-user-defined",
+ "iso-2022-cn",
+]
+
+const encodingLabelSet = new Set(encodingLabels)
+
+/**
+ * Detect encoding from BOM (Byte Order Mark).
+ * Returns the detected encoding or undefined if no BOM found.
+ */
+function detectBOM(buffer: Uint8Array): string | undefined {
+ if (
+ buffer.length >= 3 &&
+ buffer[0] === 0xef &&
+ buffer[1] === 0xbb &&
+ buffer[2] === 0xbf
+ ) {
+ return "utf-8"
+ }
+ if (buffer.length >= 2) {
+ if (buffer[0] === 0xff && buffer[1] === 0xfe) return "utf-16le"
+ if (buffer[0] === 0xfe && buffer[1] === 0xff) return "utf-16be"
+ }
+ return undefined
+}
+
+/**
+ * Detect encoding using chardet with confidence filtering.
+ * Returns the best match or undefined if no confident match found.
+ */
+function detectByChardet(buffer: Uint8Array): string | undefined {
+ const results = chardet.analyse(buffer)
+ for (const result of results) {
+ if (result.confidence >= MIN_CONFIDENCE) {
+ const label = result.name.toLowerCase()
+ if (encodingLabelSet.has(label)) {
+ return label
+ }
+ }
+ }
+ return undefined
+}
+
export function EncodingSelect(props: {
encoding: string
setEncoding: (encoding: string) => void
referenceText?: string | ArrayBuffer
}) {
- const encodingLabels = [
- "utf-8",
- "gbk",
- "gb18030",
- "ibm866",
- "iso-8859-2",
- "iso-8859-3",
- "iso-8859-4",
- "iso-8859-5",
- "iso-8859-6",
- "iso-8859-7",
- "iso-8859-8",
- "iso-8859-8i",
- "iso-8859-10",
- "iso-8859-13",
- "iso-8859-14",
- "iso-8859-15",
- "iso-8859-16",
- "koi8-r",
- "koi8-u",
- "macintosh",
- "windows-874",
- "windows-1250",
- "windows-1251",
- "windows-1252",
- "windows-1253",
- "windows-1254",
- "windows-1255",
- "windows-1256",
- "windows-1257",
- "windows-1258",
- "x-mac-cyrillic",
- "big5",
- "euc-jp",
- "iso-2022-jp",
- "shift_jis",
- "euc-kr",
- "iso-2022-kr",
- "utf-16be",
- "utf-16le",
- "x-user-defined",
- "iso-2022-cn",
- ]
-
createEffect(() => {
- if (props.referenceText) {
- let buffer: Uint8Array
- if (typeof props.referenceText === "string") {
- buffer = new TextEncoder().encode(props.referenceText)
- } else {
- buffer = new Uint8Array(props.referenceText)
- }
- for (let encoding of chardet.analyse(buffer)) {
- const encodingLabel = encoding.name.toLowerCase()
- if (encodingLabels.includes(encodingLabel)) {
- props.setEncoding(encodingLabel)
- return
- }
- }
+ const data = props.referenceText
+ // Skip detection for strings - they are already decoded (TextEncoder always produces UTF-8)
+ if (!data || typeof data === "string") return
+
+ const buffer = new Uint8Array(data)
+
+ // 1. Try BOM detection first (fast and accurate)
+ const bomEncoding = detectBOM(buffer)
+ if (bomEncoding) {
+ props.setEncoding(bomEncoding)
+ return
+ }
+
+ // 2. Fall back to chardet with confidence filtering
+ const detected = detectByChardet(buffer)
+ if (detected) {
+ props.setEncoding(detected)
}
})
diff --git a/src/components/MonacoEditor.tsx b/src/components/MonacoEditor.tsx
index eeb9cc1df..d704a359f 100644
--- a/src/components/MonacoEditor.tsx
+++ b/src/components/MonacoEditor.tsx
@@ -1,5 +1,5 @@
import { Box } from "@hope-ui/solid"
-import { createEffect, createSignal, onCleanup, onMount } from "solid-js"
+import { createEffect, createSignal, on, onCleanup, onMount } from "solid-js"
import { MaybeLoading } from "./FullLoading"
import loader from "@monaco-editor/loader"
import { useCDN } from "~/hooks"
@@ -9,11 +9,12 @@ import { local } from "~/store"
export interface MonacoEditorProps {
value: string
onChange?: (value: string) => void
- theme: "vs" | "vs-dark"
path?: string
language?: string
+ options?: monacoType.editor.IStandaloneEditorConstructionOptions
+ onEditorReady?: (editor: monacoType.editor.IStandaloneCodeEditor) => void
}
-let monaco: typeof monacoType
+export let monaco: typeof monacoType
export const MonacoEditorLoader = (props: MonacoEditorProps) => {
const { monacoPath } = useCDN()
@@ -40,11 +41,13 @@ export const MonacoEditor = (props: MonacoEditorProps) => {
let model: monacoType.editor.ITextModel
onMount(() => {
- monacoEditor = monaco.editor.create(monacoEditorDiv!, {
+ const constructionOptions = {
+ ...props.options,
value: props.value,
- theme: props.theme,
fontSize: parseInt(local.editor_font_size),
- })
+ automaticLayout: true,
+ }
+ monacoEditor = monaco.editor.create(monacoEditorDiv!, constructionOptions)
model = monaco.editor.createModel(
props.value,
props.language,
@@ -54,18 +57,38 @@ export const MonacoEditor = (props: MonacoEditorProps) => {
monacoEditor.onDidChangeModelContent(() => {
props.onChange?.(monacoEditor.getValue())
})
+ props.onEditorReady?.(monacoEditor)
})
- createEffect(() => {
- monacoEditor.setValue(props.value)
- })
+ createEffect(
+ on(
+ () => props.value,
+ (value) => {
+ monacoEditor.setValue(value)
+ },
+ { defer: true },
+ ),
+ )
createEffect(() => {
- monaco.editor.setTheme(props.theme)
+ monaco.editor.setTheme(props.options?.theme ?? "vs")
})
+ createEffect(
+ on(
+ () => props.language,
+ (lang) => {
+ if (lang && model) {
+ monaco.editor.setModelLanguage(model, lang)
+ }
+ },
+ { defer: true },
+ ),
+ )
+
createEffect(() => {
monacoEditor?.updateOptions({
fontSize: parseInt(local.editor_font_size),
+ ...props.options,
})
})
@@ -73,5 +96,5 @@ export const MonacoEditor = (props: MonacoEditorProps) => {
model && model.dispose()
monacoEditor && monacoEditor.dispose()
})
- return
+ return
}
diff --git a/src/hooks/useUtil.ts b/src/hooks/useUtil.ts
index 696073f93..7c60c6500 100644
--- a/src/hooks/useUtil.ts
+++ b/src/hooks/useUtil.ts
@@ -1,18 +1,58 @@
-import copy from "copy-to-clipboard"
import { createResource } from "solid-js"
import { getHideFiles, objStore } from "~/store"
import { Obj } from "~/types"
import { decodeText, fetchText, notify, pathJoin } from "~/utils"
import { useT, useLink, useRouter } from "."
+async function checkClipboardPermission(
+ mode: "clipboard-read" | "clipboard-write",
+): Promise {
+ try {
+ const status = await navigator.permissions.query({
+ name: mode as PermissionName,
+ })
+ return status.state === "granted" || status.state === "prompt"
+ } catch {
+ // Safari does not support permissions.query for clipboard;
+ // assume permission is available if navigator.clipboard exists.
+ return !!navigator.clipboard
+ }
+}
+
export const useUtil = () => {
const t = useT()
const { pathname } = useRouter()
return {
- copy: (text: string) => {
- copy(text)
+ copy: async (text: string) => {
+ try {
+ if (await checkClipboardPermission("clipboard-write")) {
+ await navigator.clipboard.writeText(text)
+ } else {
+ throw new Error("permission denied")
+ }
+ } catch {
+ const ta = document.createElement("textarea")
+ ta.value = text
+ ta.style.position = "fixed"
+ ta.style.opacity = "0"
+ document.body.appendChild(ta)
+ ta.select()
+ document.execCommand("copy")
+ document.body.removeChild(ta)
+ }
notify.success(t("global.copied"))
},
+ paste: async (): Promise => {
+ try {
+ if (!(await checkClipboardPermission("clipboard-read"))) {
+ throw new Error(t("global.clipboard_denied"))
+ }
+ return navigator.clipboard.readText()
+ } catch (e: any) {
+ notify.error(e.message || t("global.clipboard_denied"))
+ return ""
+ }
+ },
isHide: (obj: Obj) => {
const hideFiles = getHideFiles()
for (const reg of hideFiles) {
diff --git a/src/lang/en/global.json b/src/lang/en/global.json
index 7561b2da5..a622d2a21 100644
--- a/src/lang/en/global.json
+++ b/src/lang/en/global.json
@@ -5,7 +5,14 @@
"delete": "Delete",
"save": "Save",
"update": "Update",
+ "undo": "Undo",
+ "redo": "Redo",
+ "increase": "Increase",
+ "decrease": "Decrease",
+ "copy": "Copy",
+ "paste": "Paste",
"copied": "Copied",
+ "clipboard_denied": "Clipboard access denied. Please allow clipboard permission in your browser settings.",
"delete_success": "Deleted Successfully",
"save_success": "Saved Successfully",
"update_success": "Updated Successfully",
@@ -28,6 +35,7 @@
"close": "Close",
"no_support_now": "Not currently supported",
"empty_input": "Please enter",
+ "unsaved_changes_confirm": "You have unsaved changes. Are you sure you want to leave?",
"invalid_filename_chars": "Filename cannot contain: / \\ ? < > * : | \"",
"invalid_regex": "Invalid regular expression",
"name": "Name",
diff --git a/src/lang/en/home.json b/src/lang/en/home.json
index 104cc2e83..cb4fab2e8 100644
--- a/src/lang/en/home.json
+++ b/src/lang/en/home.json
@@ -229,6 +229,17 @@
"dblclick": "Double click",
"disable_while_checked": "Disable while checked"
},
+ "editor_font_size": "Editor font size",
+ "editor_word_wrap": "Editor word wrap",
+ "editor_word_wrap_options": {
+ "false": "Off",
+ "true": "On"
+ },
+ "editor_minimap": "Editor minimap",
+ "editor_minimap_options": {
+ "false": "Off",
+ "true": "On"
+ },
"show_gallery_thumbnails": "Show gallery thumbnails",
"show_gallery_thumbnails_options": {
"none": "None",
diff --git a/src/pages/home/previews/text-editor.tsx b/src/pages/home/previews/text-editor.tsx
index 1074bebf3..6ec762671 100644
--- a/src/pages/home/previews/text-editor.tsx
+++ b/src/pages/home/previews/text-editor.tsx
@@ -1,12 +1,54 @@
-import { Button, useColorMode, VStack } from "@hope-ui/solid"
-import { createEffect, createMemo, createSignal, on, Show } from "solid-js"
-import { EncodingSelect, MaybeLoading } from "~/components"
-import { MonacoEditorLoader } from "~/components/MonacoEditor"
-import { useFetch, useFetchText, useParseText, useRouter, useT } from "~/hooks"
-import { objStore, userCan } from "~/store"
-import { PEmptyResp } from "~/types"
-import { handleResp, notify, r } from "~/utils"
+import {
+ Box,
+ Button,
+ ButtonGroup,
+ HStack,
+ IconButton,
+ Input,
+ Popover,
+ PopoverBody,
+ PopoverContent,
+ PopoverTrigger,
+ Tooltip,
+ useColorMode,
+ VStack,
+} from "@hope-ui/solid"
+import {
+ createEffect,
+ createMemo,
+ createSignal,
+ For,
+ on,
+ onCleanup,
+ Show,
+} from "solid-js"
+import { useBeforeLeave } from "@solidjs/router"
+import { BoxWithFullScreen, EncodingSelect, MaybeLoading } from "~/components"
+import { MonacoEditorLoader, monaco } from "~/components/MonacoEditor"
+import { useFetchText, useParseText, useRouter, useT, useUtil } from "~/hooks"
+import { objStore, setLocal, userCan } from "~/store"
+import { local } from "~/store"
+import { notify } from "~/utils"
+import { StreamUpload } from "~/pages/home/uploads/stream"
import { createShortcut } from "@solid-primitives/keyboard"
+import { BiRegularRedo, BiRegularUndo } from "solid-icons/bi"
+import {
+ TbBraces,
+ TbClipboardText,
+ TbCopy,
+ TbDeviceFloppy,
+ TbTextWrap,
+ TbTextWrapDisabled,
+ TbMap,
+ TbMapOff,
+} from "solid-icons/tb"
+import { FaSolidMinus, FaSolidPlus } from "solid-icons/fa"
+import type * as monacoType from "monaco-editor/esm/vs/editor/editor.api.js"
+
+interface LanguageOption {
+ id: string
+ aliases?: string[]
+}
function Editor(props: { data?: string | ArrayBuffer; contentType?: string }) {
const { colorMode } = useColorMode()
@@ -18,68 +60,463 @@ function Editor(props: { data?: string | ArrayBuffer; contentType?: string }) {
const [encoding, setEncoding] = createSignal("utf-8")
const [value, setValue] = createSignal(text(encoding()))
const t = useT()
- const [loading, save] = useFetch(
- (): PEmptyResp =>
- r.put("/fs/put", value(), {
- headers: {
- "File-Path": encodeURIComponent(pathname()),
- "Content-Type": props.contentType || "text/plain",
- },
- }),
+ const { copy, paste } = useUtil()
+
+ // Editor instance reference
+ const [editor, setEditor] =
+ createSignal()
+
+ // Track modified state
+ const [modified, setModified] = createSignal(false)
+ const [cursorLine, setCursorLine] = createSignal(1)
+ const [cursorColumn, setCursorColumn] = createSignal(1)
+ const [wordCount, setWordCount] = createSignal(0)
+ const [language, setLanguage] = createSignal("")
+ const [languageOptions, setLanguageOptions] = createSignal(
+ [],
+ )
+ const [wordWrap, setWordWrap] = createSignal(
+ local.editor_word_wrap === "true",
+ )
+ const [minimap, setMinimap] = createSignal(local.editor_minimap !== "false")
+ const [saving, setSaving] = createSignal(false)
+ const [langSearch, setLangSearch] = createSignal("")
+ const filteredLanguages = createMemo(() => {
+ const s = langSearch().toLowerCase()
+ if (!s) return languageOptions()
+ return languageOptions().filter(
+ (l) =>
+ l.id.toLowerCase().includes(s) ||
+ l.aliases?.some((a) => a.toLowerCase().includes(s)),
+ )
+ })
+ const languageDisplayName = createMemo(() => {
+ const lang = languageOptions().find((l) => l.id === language())
+ return lang?.aliases?.[0] || language()
+ })
+ const canWrite = createMemo(
+ () =>
+ // objStore.write is only set from folder listing (FsListResp),
+ // not from file detail (FsGetResp). When directly entering a file,
+ // write is undefined, so fall back to permission check only.
+ (userCan("write_content") || objStore.write_content_bypass) &&
+ objStore.write !== false,
)
+
+ if (canWrite()) {
+ // Warn on browser close/refresh when there are unsaved changes
+ const beforeUnloadHandler = (e: BeforeUnloadEvent) => {
+ if (modified()) {
+ e.preventDefault()
+ }
+ }
+ window.addEventListener("beforeunload", beforeUnloadHandler)
+ onCleanup(() =>
+ window.removeEventListener("beforeunload", beforeUnloadHandler),
+ )
+
+ // Warn on in-app navigation when there are unsaved changes
+ useBeforeLeave((e) => {
+ if (modified()) {
+ if (!window.confirm(t("global.unsaved_changes_confirm"))) {
+ e.preventDefault()
+ }
+ }
+ })
+
+ // Save on Ctrl+S / Cmd+S
+ createShortcut(["Control", "S"], (e: KeyboardEvent | null) => {
+ e?.preventDefault()
+ onSave()
+ })
+ createShortcut(["Meta", "S"], (e: KeyboardEvent | null) => {
+ e?.preventDefault()
+ onSave()
+ })
+ }
+
createEffect(
on(encoding, (v) => {
setValue(text(v))
+ setModified(false)
}),
)
async function onSave() {
- const resp = await save()
- handleResp(resp, () => {
+ setSaving(true)
+ try {
+ const file = new File([value()], objStore.obj.name, {
+ type: props.contentType || "text/plain",
+ })
+ await StreamUpload(pathname(), file, () => {}, false, true, false)
notify.success(t("global.save_success"))
+ setModified(false)
+ } catch (e: any) {
+ notify.error(e.message)
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ function onEditorReady(ed: monacoType.editor.IStandaloneCodeEditor) {
+ setEditor(ed)
+
+ // Track cursor position
+ ed.onDidChangeCursorPosition((e) => {
+ setCursorLine(e.position.lineNumber)
+ setCursorColumn(e.position.column)
})
+
+ // Track content changes for modified state
+ let savedVersionId = ed.getModel()?.getAlternativeVersionId() ?? 0
+ ed.onDidChangeModelContent(() => {
+ const currentVersionId = ed.getModel()?.getAlternativeVersionId() ?? 0
+ setModified(currentVersionId !== savedVersionId)
+ updateWordCount(ed)
+ })
+
+ // Initial word count
+ updateWordCount(ed)
+
+ // Detect language from model
+ const lang = ed.getModel()?.getLanguageId() ?? ""
+ setLanguage(lang)
+
+ // Populate language options from Monaco
+ if (monaco?.languages?.getLanguages) {
+ const langs = monaco.languages.getLanguages() as LanguageOption[]
+ setLanguageOptions(langs.sort((a, b) => a.id.localeCompare(b.id)))
+ }
+ }
+
+ function updateWordCount(ed: monacoType.editor.IStandaloneCodeEditor) {
+ const content = ed.getValue()
+ if (!content) {
+ setWordCount(0)
+ return
+ }
+ const trimmed = content.trim()
+ setWordCount(trimmed ? trimmed.split(/\s+/).length : 0)
}
- createShortcut(["Control", "S"], onSave)
+ function undo() {
+ editor()?.trigger("toolbar", "undo", null)
+ }
+
+ function redo() {
+ editor()?.trigger("toolbar", "redo", null)
+ }
+
+ async function copyToClipboard() {
+ const ed = editor()
+ if (!ed) return
+ const sel = ed.getSelection()
+ const selection = sel ? (ed.getModel()?.getValueInRange(sel) ?? "") : ""
+ const text = selection || ed.getValue()
+ await copy(text)
+ }
+
+ async function pasteFromClipboard() {
+ const ed = editor()
+ if (!ed) return
+
+ const text = await paste()
+ if (!text) return
+ const selection = ed.getSelection()
+ if (selection) {
+ ed.executeEdits("paste", [
+ { range: selection, text, forceMoveMarkers: true },
+ ])
+ } else {
+ ed.trigger("keyboard", "paste", { text })
+ }
+ ed.focus()
+ }
+
+ function toggleWordWrap() {
+ const next = !wordWrap()
+ setWordWrap(next)
+ setLocal("editor_word_wrap", String(next))
+ }
+
+ function changeFontSize(delta: number) {
+ const current = parseInt(local.editor_font_size) || 14
+ const next = Math.max(8, Math.min(40, current + delta))
+ setLocal("editor_font_size", String(next))
+ }
return (
-
-
-
-
+
+ {/* Toolbar */}
+
+
+
+ }
+ size="sm"
+ variant="solid"
+ loading={saving()}
+ onClick={onSave}
+ >
+ {t("global.save")}
+
+
+
+
+ }
+ size="sm"
+ variant="ghost"
+ onClick={undo}
+ />
+
+
+ }
+ size="sm"
+ variant="ghost"
+ onClick={redo}
+ />
+
+
+
+
+
+
+ }
+ size="sm"
+ variant="ghost"
+ onClick={copyToClipboard}
+ />
+
+
+
+ }
+ size="sm"
+ variant="ghost"
+ onClick={pasteFromClipboard}
+ />
+
+
+
+
+
+
+ : }
+ size="sm"
+ variant="ghost"
+ onClick={toggleWordWrap}
+ color={wordWrap() ? "$info11" : undefined}
+ />
+
+
+
+ : }
+ size="sm"
+ variant="ghost"
+ onClick={() => {
+ const next = !minimap()
+ setMinimap(next)
+ setLocal("editor_minimap", String(next))
+ }}
+ color={minimap() ? "$info11" : undefined}
+ />
+
+
+
+
+
+
+ }
+ onClick={() => changeFontSize(-1)}
+ />
+
+
+
+ }
+ onClick={() => changeFontSize(1)}
+ />
+
+
+
+
+
+
+
+
+
+
+ {/* Editor */}
{
- setValue(value)
+ options={{
+ theme: theme(),
+ wordWrap: wordWrap() ? "on" : "off",
+ minimap: { enabled: minimap() },
+ readOnly: !canWrite(),
}}
+ onChange={(val) => setValue(val)}
+ onEditorReady={onEditorReady}
/>
-
-
-
+
+ setLangSearch("")}>
+
+
+
+ {languageDisplayName()}
+
+
+
+
+ setLangSearch(e.currentTarget.value)}
+ mb="$2"
+ autofocus
+ />
+
+
+ {(lang) => (
+ {
+ setLanguage(lang.id)
+ setLangSearch("")
+ }}
+ >
+ {lang.aliases?.[0] || lang.id}
+
+
+ ({lang.id})
+
+
+
+ )}
+
+
+
+
+
+
+
+
+ Ln {cursorLine()}, Col {cursorColumn()}
+
+ {wordCount()} words
+
+
+ ●
+
+
+
)
}
-// TODO add encoding select
const TextEditor = () => {
const [content] = useFetchText()
return (
-
-
-
+
+
+
+
+
)
}
diff --git a/src/pages/home/toolbar/LocalSettings.tsx b/src/pages/home/toolbar/LocalSettings.tsx
index 4779ce50b..84bebce37 100644
--- a/src/pages/home/toolbar/LocalSettings.tsx
+++ b/src/pages/home/toolbar/LocalSettings.tsx
@@ -24,6 +24,7 @@ import {
VStack,
Switch as HopeSwitch,
IconButton,
+ ButtonGroup,
} from "@hope-ui/solid"
import { For, Match, onCleanup, Switch } from "solid-js"
import { FaSolidMinus, FaSolidPlus } from "solid-icons/fa"
@@ -83,7 +84,7 @@ function LocalSettingEdit(props: LocalSetting) {
/>
-
+
}
@@ -100,6 +101,7 @@ function LocalSettingEdit(props: LocalSetting) {
onInput={(e) => {
setLocal(props.key, e.currentTarget.value)
}}
+ borderRadius="$none"
style={{
"-moz-appearance": "textfield",
// @ts-ignore
@@ -115,7 +117,7 @@ function LocalSettingEdit(props: LocalSetting) {
setLocal(props.key, (parseInt(local[props.key]) + 1).toString())
}}
/>
-
+
diff --git a/src/store/local_settings.ts b/src/store/local_settings.ts
index d02d0d915..cfa2e3407 100644
--- a/src/store/local_settings.ts
+++ b/src/store/local_settings.ts
@@ -71,6 +71,18 @@ export const initialLocalSettings = [
default: "14",
type: "number",
},
+ {
+ key: "editor_word_wrap",
+ default: "false",
+ type: "select",
+ options: ["false", "true"],
+ },
+ {
+ key: "editor_minimap",
+ default: "true",
+ type: "select",
+ options: ["false", "true"],
+ },
{
key: "show_gallery_thumbnails",
default: "visible",