Add markdown alert parsing, fix buffer issue when switching files (#988)

Adds the GitHub alert syntax parsing to the markdown element, fixes an
issue where the file edit buffer was not getting unset when the file
path changed, continues my crusade on star imports
This commit is contained in:
Evan Simkowitz 2024-10-08 12:25:41 -04:00 committed by GitHub
parent 2e27296677
commit d2b2491211
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 123 additions and 116 deletions

View File

@ -2,4 +2,4 @@
To report vulnerabilities or security concerns, please email us at: [security@commandline.dev](mailto:security@commandline.dev) To report vulnerabilities or security concerns, please email us at: [security@commandline.dev](mailto:security@commandline.dev)
** Please do not report security vulnerabilities through public github issues. ** **Please do not report security vulnerabilities through public github issues.**

View File

@ -17,6 +17,7 @@ import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
import rehypeSlug from "rehype-slug"; import rehypeSlug from "rehype-slug";
import RemarkFlexibleToc, { TocItem } from "remark-flexible-toc"; import RemarkFlexibleToc, { TocItem } from "remark-flexible-toc";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
import { remarkAlert } from "remark-github-blockquote-alert";
import { openLink } from "../store/global"; import { openLink } from "../store/global";
import { IconButton } from "./iconbutton"; import { IconButton } from "./iconbutton";
import "./markdown.less"; import "./markdown.less";
@ -258,7 +259,7 @@ const Markdown = ({
options={{ scrollbars: { autoHide: "leave" } }} options={{ scrollbars: { autoHide: "leave" } }}
> >
<ReactMarkdown <ReactMarkdown
remarkPlugins={[remarkGfm, [RemarkFlexibleToc, { tocRef: tocRef.current }]]} remarkPlugins={[remarkGfm, remarkAlert, [RemarkFlexibleToc, { tocRef: tocRef.current }]]}
rehypePlugins={[ rehypePlugins={[
rehypeRaw, rehypeRaw,
rehypeHighlight, rehypeHighlight,

View File

@ -1,30 +1,28 @@
// Copyright 2024, Command Line Inc. // Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { CenteredDiv } from "@/app/element/quickelems";
import { TypeAheadModal } from "@/app/modals/typeaheadmodal"; import { TypeAheadModal } from "@/app/modals/typeaheadmodal";
import { ContextMenuModel } from "@/app/store/contextmenu"; import { ContextMenuModel } from "@/app/store/contextmenu";
import { tryReinjectKey } from "@/app/store/keymodel"; import { tryReinjectKey } from "@/app/store/keymodel";
import { RpcApi } from "@/app/store/wshclientapi"; import { RpcApi } from "@/app/store/wshclientapi";
import { WindowRpcClient } from "@/app/store/wshrpcutil"; import { WindowRpcClient } from "@/app/store/wshrpcutil";
import { CodeEditor } from "@/app/view/codeeditor/codeeditor";
import { Markdown } from "@/element/markdown"; import { Markdown } from "@/element/markdown";
import { NodeModel } from "@/layout/index"; import { NodeModel } from "@/layout/index";
import { atoms, createBlock, getConnStatusAtom, getSettingsKeyAtom, globalStore, refocusNode } from "@/store/global"; import { atoms, createBlock, getConnStatusAtom, getSettingsKeyAtom, globalStore, refocusNode } from "@/store/global";
import * as services from "@/store/services"; import * as services from "@/store/services";
import * as WOS from "@/store/wos"; import * as WOS from "@/store/wos";
import { getWebServerEndpoint } from "@/util/endpoints"; import { getWebServerEndpoint } from "@/util/endpoints";
import * as historyutil from "@/util/historyutil"; import { goHistory, goHistoryBack, goHistoryForward } from "@/util/historyutil";
import * as keyutil from "@/util/keyutil"; import { adaptFromReactOrNativeKeyEvent, checkKeyPressed, keydownWrapper } from "@/util/keyutil";
import * as util from "@/util/util"; import { base64ToString, isBlank, jotaiLoadableValue, makeConnRoute, stringToBase64 } from "@/util/util";
import { makeConnRoute } from "@/util/util";
import { Monaco } from "@monaco-editor/react"; import { Monaco } from "@monaco-editor/react";
import clsx from "clsx"; import clsx from "clsx";
import * as jotai from "jotai"; import { Atom, atom, Getter, PrimitiveAtom, useAtomValue, useSetAtom, WritableAtom } from "jotai";
import { loadable } from "jotai/utils"; import { loadable } from "jotai/utils";
import type * as MonacoTypes from "monaco-editor/esm/vs/editor/editor.api"; import type * as MonacoTypes from "monaco-editor/esm/vs/editor/editor.api";
import * as React from "react"; import { createRef, memo, useCallback, useEffect, useMemo, useState } from "react";
import { createRef, useCallback, useState } from "react";
import { CenteredDiv } from "../../element/quickelems";
import { CodeEditor } from "../codeeditor/codeeditor";
import { CSVView } from "./csvview"; import { CSVView } from "./csvview";
import { DirectoryPreview } from "./directorypreview"; import { DirectoryPreview } from "./directorypreview";
import "./preview.less"; import "./preview.less";
@ -101,70 +99,64 @@ export class PreviewModel implements ViewModel {
viewType: string; viewType: string;
blockId: string; blockId: string;
nodeModel: NodeModel; nodeModel: NodeModel;
blockAtom: jotai.Atom<Block>; blockAtom: Atom<Block>;
viewIcon: jotai.Atom<string | IconButtonDecl>; viewIcon: Atom<string | IconButtonDecl>;
viewName: jotai.Atom<string>; viewName: Atom<string>;
viewText: jotai.Atom<HeaderElem[]>; viewText: Atom<HeaderElem[]>;
preIconButton: jotai.Atom<IconButtonDecl>; preIconButton: Atom<IconButtonDecl>;
endIconButtons: jotai.Atom<IconButtonDecl[]>; endIconButtons: Atom<IconButtonDecl[]>;
previewTextRef: React.RefObject<HTMLDivElement>; previewTextRef: React.RefObject<HTMLDivElement>;
editMode: jotai.Atom<boolean>; editMode: Atom<boolean>;
canPreview: jotai.PrimitiveAtom<boolean>; canPreview: PrimitiveAtom<boolean>;
specializedView: jotai.Atom<Promise<{ specializedView?: string; errorStr?: string }>>; specializedView: Atom<Promise<{ specializedView?: string; errorStr?: string }>>;
loadableSpecializedView: jotai.Atom<Loadable<{ specializedView?: string; errorStr?: string }>>; loadableSpecializedView: Atom<Loadable<{ specializedView?: string; errorStr?: string }>>;
manageConnection: jotai.Atom<boolean>; manageConnection: Atom<boolean>;
connStatus: jotai.Atom<ConnStatus>; connStatus: Atom<ConnStatus>;
metaFilePath: jotai.Atom<string>; metaFilePath: Atom<string>;
statFilePath: jotai.Atom<Promise<string>>; statFilePath: Atom<Promise<string>>;
normFilePath: jotai.Atom<Promise<string>>; normFilePath: Atom<Promise<string>>;
loadableStatFilePath: jotai.Atom<Loadable<string>>; loadableStatFilePath: Atom<Loadable<string>>;
loadableFileInfo: jotai.Atom<Loadable<FileInfo>>; loadableFileInfo: Atom<Loadable<FileInfo>>;
connection: jotai.Atom<string>; connection: Atom<string>;
statFile: jotai.Atom<Promise<FileInfo>>; statFile: Atom<Promise<FileInfo>>;
fullFile: jotai.Atom<Promise<FullFile>>; fullFile: Atom<Promise<FullFile>>;
fileMimeType: jotai.Atom<Promise<string>>; fileMimeType: Atom<Promise<string>>;
fileMimeTypeLoadable: jotai.Atom<Loadable<string>>; fileMimeTypeLoadable: Atom<Loadable<string>>;
fileContentSaved: jotai.PrimitiveAtom<string | null>; fileContentSaved: PrimitiveAtom<string | null>;
fileContent: jotai.WritableAtom<Promise<string>, [string], void>; fileContent: WritableAtom<Promise<string>, [string], void>;
newFileContent: jotai.PrimitiveAtom<string | null>; newFileContent: PrimitiveAtom<string | null>;
openFileModal: jotai.PrimitiveAtom<boolean>; openFileModal: PrimitiveAtom<boolean>;
openFileError: jotai.PrimitiveAtom<string>; openFileError: PrimitiveAtom<string>;
openFileModalGiveFocusRef: React.MutableRefObject<() => boolean>; openFileModalGiveFocusRef: React.MutableRefObject<() => boolean>;
markdownShowToc: jotai.PrimitiveAtom<boolean>; markdownShowToc: PrimitiveAtom<boolean>;
monacoRef: React.MutableRefObject<MonacoTypes.editor.IStandaloneCodeEditor>; monacoRef: React.MutableRefObject<MonacoTypes.editor.IStandaloneCodeEditor>;
showHiddenFiles: jotai.PrimitiveAtom<boolean>; showHiddenFiles: PrimitiveAtom<boolean>;
refreshVersion: jotai.PrimitiveAtom<number>; refreshVersion: PrimitiveAtom<number>;
refreshCallback: () => void; refreshCallback: () => void;
directoryKeyDownHandler: (waveEvent: WaveKeyboardEvent) => boolean; directoryKeyDownHandler: (waveEvent: WaveKeyboardEvent) => boolean;
codeEditKeyDownHandler: (waveEvent: WaveKeyboardEvent) => boolean; codeEditKeyDownHandler: (waveEvent: WaveKeyboardEvent) => boolean;
setPreviewFileName(fileName: string) {
globalStore.set(this.fileContentSaved, null);
globalStore.set(this.newFileContent, null);
services.ObjectService.UpdateObjectMeta(`block:${this.blockId}`, { file: fileName });
}
constructor(blockId: string, nodeModel: NodeModel) { constructor(blockId: string, nodeModel: NodeModel) {
this.viewType = "preview"; this.viewType = "preview";
this.blockId = blockId; this.blockId = blockId;
this.nodeModel = nodeModel; this.nodeModel = nodeModel;
let showHiddenFiles = globalStore.get(getSettingsKeyAtom("preview:showhiddenfiles")) ?? true; let showHiddenFiles = globalStore.get(getSettingsKeyAtom("preview:showhiddenfiles")) ?? true;
this.showHiddenFiles = jotai.atom<boolean>(showHiddenFiles); this.showHiddenFiles = atom<boolean>(showHiddenFiles);
this.refreshVersion = jotai.atom(0); this.refreshVersion = atom(0);
this.previewTextRef = createRef(); this.previewTextRef = createRef();
this.openFileModal = jotai.atom(false); this.openFileModal = atom(false);
this.openFileError = jotai.atom(null) as jotai.PrimitiveAtom<string>; this.openFileError = atom(null) as PrimitiveAtom<string>;
this.openFileModalGiveFocusRef = createRef(); this.openFileModalGiveFocusRef = createRef();
this.manageConnection = jotai.atom(true); this.manageConnection = atom(true);
this.blockAtom = WOS.getWaveObjectAtom<Block>(`block:${blockId}`); this.blockAtom = WOS.getWaveObjectAtom<Block>(`block:${blockId}`);
this.markdownShowToc = jotai.atom(false); this.markdownShowToc = atom(false);
this.monacoRef = createRef(); this.monacoRef = createRef();
this.viewIcon = jotai.atom((get) => { this.viewIcon = atom((get) => {
const blockData = get(this.blockAtom); const blockData = get(this.blockAtom);
if (blockData?.meta?.icon) { if (blockData?.meta?.icon) {
return blockData.meta.icon; return blockData.meta.icon;
@ -173,9 +165,8 @@ export class PreviewModel implements ViewModel {
if (connStatus?.status != "connected") { if (connStatus?.status != "connected") {
return null; return null;
} }
const fileName = get(this.metaFilePath);
const mimeTypeLoadable = get(this.fileMimeTypeLoadable); const mimeTypeLoadable = get(this.fileMimeTypeLoadable);
const mimeType = util.jotaiLoadableValue(mimeTypeLoadable, ""); const mimeType = jotaiLoadableValue(mimeTypeLoadable, "");
if (mimeType == "directory") { if (mimeType == "directory") {
return { return {
elemtype: "iconbutton", elemtype: "iconbutton",
@ -208,12 +199,12 @@ export class PreviewModel implements ViewModel {
} }
return iconForFile(mimeType); return iconForFile(mimeType);
}); });
this.editMode = jotai.atom((get) => { this.editMode = atom((get) => {
const blockData = get(this.blockAtom); const blockData = get(this.blockAtom);
return blockData?.meta?.edit ?? false; return blockData?.meta?.edit ?? false;
}); });
this.viewName = jotai.atom("Preview"); this.viewName = atom("Preview");
this.viewText = jotai.atom((get) => { this.viewText = atom((get) => {
let headerPath = get(this.metaFilePath); let headerPath = get(this.metaFilePath);
const connStatus = get(this.connStatus); const connStatus = get(this.connStatus);
if (connStatus?.status != "connected") { if (connStatus?.status != "connected") {
@ -282,12 +273,12 @@ export class PreviewModel implements ViewModel {
}, },
] as HeaderElem[]; ] as HeaderElem[];
}); });
this.preIconButton = jotai.atom((get) => { this.preIconButton = atom((get) => {
const connStatus = get(this.connStatus); const connStatus = get(this.connStatus);
if (connStatus?.status != "connected") { if (connStatus?.status != "connected") {
return null; return null;
} }
const mimeType = util.jotaiLoadableValue(get(this.fileMimeTypeLoadable), ""); const mimeType = jotaiLoadableValue(get(this.fileMimeTypeLoadable), "");
const metaPath = get(this.metaFilePath); const metaPath = get(this.metaFilePath);
if (mimeType == "directory" && metaPath == "/") { if (mimeType == "directory" && metaPath == "/") {
return null; return null;
@ -298,12 +289,12 @@ export class PreviewModel implements ViewModel {
click: this.goParentDirectory.bind(this), click: this.goParentDirectory.bind(this),
}; };
}); });
this.endIconButtons = jotai.atom((get) => { this.endIconButtons = atom((get) => {
const connStatus = get(this.connStatus); const connStatus = get(this.connStatus);
if (connStatus?.status != "connected") { if (connStatus?.status != "connected") {
return null; return null;
} }
const mimeType = util.jotaiLoadableValue(get(this.fileMimeTypeLoadable), ""); const mimeType = jotaiLoadableValue(get(this.fileMimeTypeLoadable), "");
const loadableSV = get(this.loadableSpecializedView); const loadableSV = get(this.loadableSpecializedView);
const isCeView = loadableSV.state == "hasData" && loadableSV.data.specializedView == "codeedit"; const isCeView = loadableSV.state == "hasData" && loadableSV.data.specializedView == "codeedit";
if (mimeType == "directory") { if (mimeType == "directory") {
@ -335,18 +326,18 @@ export class PreviewModel implements ViewModel {
} }
return null; return null;
}); });
this.metaFilePath = jotai.atom<string>((get) => { this.metaFilePath = atom<string>((get) => {
const file = get(this.blockAtom)?.meta?.file; const file = get(this.blockAtom)?.meta?.file;
if (util.isBlank(file)) { if (isBlank(file)) {
return "~"; return "~";
} }
return file; return file;
}); });
this.statFilePath = jotai.atom<Promise<string>>(async (get) => { this.statFilePath = atom<Promise<string>>(async (get) => {
const fileInfo = await get(this.statFile); const fileInfo = await get(this.statFile);
return fileInfo?.path; return fileInfo?.path;
}); });
this.normFilePath = jotai.atom<Promise<string>>(async (get) => { this.normFilePath = atom<Promise<string>>(async (get) => {
const fileInfo = await get(this.statFile); const fileInfo = await get(this.statFile);
if (fileInfo == null) { if (fileInfo == null) {
return null; return null;
@ -357,10 +348,10 @@ export class PreviewModel implements ViewModel {
return fileInfo.dir + "/" + fileInfo.name; return fileInfo.dir + "/" + fileInfo.name;
}); });
this.loadableStatFilePath = loadable(this.statFilePath); this.loadableStatFilePath = loadable(this.statFilePath);
this.connection = jotai.atom<string>((get) => { this.connection = atom<string>((get) => {
return get(this.blockAtom)?.meta?.connection; return get(this.blockAtom)?.meta?.connection;
}); });
this.statFile = jotai.atom<Promise<FileInfo>>(async (get) => { this.statFile = atom<Promise<FileInfo>>(async (get) => {
const fileName = get(this.metaFilePath); const fileName = get(this.metaFilePath);
if (fileName == null) { if (fileName == null) {
return null; return null;
@ -369,15 +360,15 @@ export class PreviewModel implements ViewModel {
const statFile = await services.FileService.StatFile(conn, fileName); const statFile = await services.FileService.StatFile(conn, fileName);
return statFile; return statFile;
}); });
this.fileMimeType = jotai.atom<Promise<string>>(async (get) => { this.fileMimeType = atom<Promise<string>>(async (get) => {
const fileInfo = await get(this.statFile); const fileInfo = await get(this.statFile);
return fileInfo?.mimetype; return fileInfo?.mimetype;
}); });
this.fileMimeTypeLoadable = loadable(this.fileMimeType); this.fileMimeTypeLoadable = loadable(this.fileMimeType);
this.newFileContent = jotai.atom(null) as jotai.PrimitiveAtom<string | null>; this.newFileContent = atom(null) as PrimitiveAtom<string | null>;
this.goParentDirectory = this.goParentDirectory.bind(this); this.goParentDirectory = this.goParentDirectory.bind(this);
const fullFileAtom = jotai.atom<Promise<FullFile>>(async (get) => { const fullFileAtom = atom<Promise<FullFile>>(async (get) => {
const fileName = get(this.metaFilePath); const fileName = get(this.metaFilePath);
if (fileName == null) { if (fileName == null) {
return null; return null;
@ -387,8 +378,8 @@ export class PreviewModel implements ViewModel {
return file; return file;
}); });
this.fileContentSaved = jotai.atom(null) as jotai.PrimitiveAtom<string | null>; this.fileContentSaved = atom(null) as PrimitiveAtom<string | null>;
const fileContentAtom = jotai.atom( const fileContentAtom = atom(
async (get) => { async (get) => {
const _ = get(this.metaFilePath); const _ = get(this.metaFilePath);
const newContent = get(this.newFileContent); const newContent = get(this.newFileContent);
@ -400,7 +391,7 @@ export class PreviewModel implements ViewModel {
return savedContent; return savedContent;
} }
const fullFile = await get(fullFileAtom); const fullFile = await get(fullFileAtom);
return util.base64ToString(fullFile?.data64); return base64ToString(fullFile?.data64);
}, },
(get, set, update: string) => { (get, set, update: string) => {
set(this.fileContentSaved, update); set(this.fileContentSaved, update);
@ -410,13 +401,13 @@ export class PreviewModel implements ViewModel {
this.fullFile = fullFileAtom; this.fullFile = fullFileAtom;
this.fileContent = fileContentAtom; this.fileContent = fileContentAtom;
this.specializedView = jotai.atom<Promise<{ specializedView?: string; errorStr?: string }>>(async (get) => { this.specializedView = atom<Promise<{ specializedView?: string; errorStr?: string }>>(async (get) => {
return this.getSpecializedView(get); return this.getSpecializedView(get);
}); });
this.loadableSpecializedView = loadable(this.specializedView); this.loadableSpecializedView = loadable(this.specializedView);
this.canPreview = jotai.atom(false); this.canPreview = atom(false);
this.loadableFileInfo = loadable(this.statFile); this.loadableFileInfo = loadable(this.statFile);
this.connStatus = jotai.atom((get) => { this.connStatus = atom((get) => {
const blockData = get(this.blockAtom); const blockData = get(this.blockAtom);
const connName = blockData?.meta?.connection; const connName = blockData?.meta?.connection;
const connAtom = getConnStatusAtom(connName); const connAtom = getConnStatusAtom(connName);
@ -428,7 +419,7 @@ export class PreviewModel implements ViewModel {
globalStore.set(this.markdownShowToc, !globalStore.get(this.markdownShowToc)); globalStore.set(this.markdownShowToc, !globalStore.get(this.markdownShowToc));
} }
async getSpecializedView(getFn: jotai.Getter): Promise<{ specializedView?: string; errorStr?: string }> { async getSpecializedView(getFn: Getter): Promise<{ specializedView?: string; errorStr?: string }> {
const mimeType = await getFn(this.fileMimeType); const mimeType = await getFn(this.fileMimeType);
const fileInfo = await getFn(this.statFile); const fileInfo = await getFn(this.statFile);
const fileName = await getFn(this.statFilePath); const fileName = await getFn(this.statFilePath);
@ -490,12 +481,16 @@ export class PreviewModel implements ViewModel {
fileName = ""; fileName = "";
} }
const blockMeta = globalStore.get(this.blockAtom)?.meta; const blockMeta = globalStore.get(this.blockAtom)?.meta;
const updateMeta = historyutil.goHistory("file", fileName, newPath, blockMeta); const updateMeta = goHistory("file", fileName, newPath, blockMeta);
if (updateMeta == null) { if (updateMeta == null) {
return; return;
} }
const blockOref = WOS.makeORef("block", this.blockId); const blockOref = WOS.makeORef("block", this.blockId);
services.ObjectService.UpdateObjectMeta(blockOref, updateMeta); services.ObjectService.UpdateObjectMeta(blockOref, updateMeta);
// Clear the saved file buffers
globalStore.set(this.fileContentSaved, null);
globalStore.set(this.newFileContent, null);
} }
async getParentInfo(fileInfo: FileInfo): Promise<FileInfo | undefined> { async getParentInfo(fileInfo: FileInfo): Promise<FileInfo | undefined> {
@ -543,7 +538,7 @@ export class PreviewModel implements ViewModel {
goHistoryBack() { goHistoryBack() {
const blockMeta = globalStore.get(this.blockAtom)?.meta; const blockMeta = globalStore.get(this.blockAtom)?.meta;
const curPath = globalStore.get(this.metaFilePath); const curPath = globalStore.get(this.metaFilePath);
const updateMeta = historyutil.goHistoryBack("file", curPath, blockMeta, true); const updateMeta = goHistoryBack("file", curPath, blockMeta, true);
if (updateMeta == null) { if (updateMeta == null) {
return; return;
} }
@ -555,7 +550,7 @@ export class PreviewModel implements ViewModel {
goHistoryForward() { goHistoryForward() {
const blockMeta = globalStore.get(this.blockAtom)?.meta; const blockMeta = globalStore.get(this.blockAtom)?.meta;
const curPath = globalStore.get(this.metaFilePath); const curPath = globalStore.get(this.metaFilePath);
const updateMeta = historyutil.goHistoryForward("file", curPath, blockMeta); const updateMeta = goHistoryForward("file", curPath, blockMeta);
if (updateMeta == null) { if (updateMeta == null) {
return; return;
} }
@ -582,7 +577,7 @@ export class PreviewModel implements ViewModel {
} }
const conn = globalStore.get(this.connection) ?? ""; const conn = globalStore.get(this.connection) ?? "";
try { try {
services.FileService.SaveFile(conn, filePath, util.stringToBase64(newFileContent)); services.FileService.SaveFile(conn, filePath, stringToBase64(newFileContent));
globalStore.set(this.fileContent, newFileContent); globalStore.set(this.fileContent, newFileContent);
globalStore.set(this.newFileContent, null); globalStore.set(this.newFileContent, null);
console.log("saved file", filePath); console.log("saved file", filePath);
@ -644,7 +639,7 @@ export class PreviewModel implements ViewModel {
navigator.clipboard.writeText(fileInfo.name); navigator.clipboard.writeText(fileInfo.name);
}, },
}); });
const mimeType = util.jotaiLoadableValue(globalStore.get(this.fileMimeTypeLoadable), ""); const mimeType = jotaiLoadableValue(globalStore.get(this.fileMimeTypeLoadable), "");
if (mimeType == "directory") { if (mimeType == "directory") {
menuItems.push({ menuItems.push({
label: "Open Terminal in New Block", label: "Open Terminal in New Block",
@ -694,29 +689,29 @@ export class PreviewModel implements ViewModel {
} }
keyDownHandler(e: WaveKeyboardEvent): boolean { keyDownHandler(e: WaveKeyboardEvent): boolean {
if (keyutil.checkKeyPressed(e, "Cmd:ArrowLeft")) { if (checkKeyPressed(e, "Cmd:ArrowLeft")) {
this.goHistoryBack(); this.goHistoryBack();
return true; return true;
} }
if (keyutil.checkKeyPressed(e, "Cmd:ArrowRight")) { if (checkKeyPressed(e, "Cmd:ArrowRight")) {
this.goHistoryForward(); this.goHistoryForward();
return true; return true;
} }
if (keyutil.checkKeyPressed(e, "Cmd:ArrowUp")) { if (checkKeyPressed(e, "Cmd:ArrowUp")) {
// handle up directory // handle up directory
this.goParentDirectory({}); this.goParentDirectory({});
return true; return true;
} }
const openModalOpen = globalStore.get(this.openFileModal); const openModalOpen = globalStore.get(this.openFileModal);
if (!openModalOpen) { if (!openModalOpen) {
if (keyutil.checkKeyPressed(e, "Cmd:o")) { if (checkKeyPressed(e, "Cmd:o")) {
this.updateOpenFileModalAndError(true); this.updateOpenFileModalAndError(true);
return true; return true;
} }
} }
const canPreview = globalStore.get(this.canPreview); const canPreview = globalStore.get(this.canPreview);
if (canPreview) { if (canPreview) {
if (keyutil.checkKeyPressed(e, "Cmd:e")) { if (checkKeyPressed(e, "Cmd:e")) {
const editMode = globalStore.get(this.editMode); const editMode = globalStore.get(this.editMode);
this.setEditMode(!editMode); this.setEditMode(!editMode);
return true; return true;
@ -744,9 +739,9 @@ function makePreviewModel(blockId: string, nodeModel: NodeModel): PreviewModel {
} }
function MarkdownPreview({ model }: SpecializedViewProps) { function MarkdownPreview({ model }: SpecializedViewProps) {
const connName = jotai.useAtomValue(model.connection); const connName = useAtomValue(model.connection);
const fileInfo = jotai.useAtomValue(model.statFile); const fileInfo = useAtomValue(model.statFile);
const resolveOpts: MarkdownResolveOpts = React.useMemo<MarkdownResolveOpts>(() => { const resolveOpts: MarkdownResolveOpts = useMemo<MarkdownResolveOpts>(() => {
return { return {
connName: connName, connName: connName,
baseDir: fileInfo.dir, baseDir: fileInfo.dir,
@ -760,8 +755,8 @@ function MarkdownPreview({ model }: SpecializedViewProps) {
} }
function StreamingPreview({ model }: SpecializedViewProps) { function StreamingPreview({ model }: SpecializedViewProps) {
const conn = jotai.useAtomValue(model.connection); const conn = useAtomValue(model.connection);
const fileInfo = jotai.useAtomValue(model.statFile); const fileInfo = useAtomValue(model.statFile);
const filePath = fileInfo.path; const filePath = fileInfo.path;
const usp = new URLSearchParams(); const usp = new URLSearchParams();
usp.set("path", filePath); usp.set("path", filePath);
@ -805,27 +800,27 @@ function StreamingPreview({ model }: SpecializedViewProps) {
} }
function CodeEditPreview({ model }: SpecializedViewProps) { function CodeEditPreview({ model }: SpecializedViewProps) {
const fileContent = jotai.useAtomValue(model.fileContent); const fileContent = useAtomValue(model.fileContent);
const setNewFileContent = jotai.useSetAtom(model.newFileContent); const setNewFileContent = useSetAtom(model.newFileContent);
const fileName = jotai.useAtomValue(model.statFilePath); const fileName = useAtomValue(model.statFilePath);
function codeEditKeyDownHandler(e: WaveKeyboardEvent): boolean { function codeEditKeyDownHandler(e: WaveKeyboardEvent): boolean {
if (keyutil.checkKeyPressed(e, "Cmd:e")) { if (checkKeyPressed(e, "Cmd:e")) {
model.setEditMode(false); model.setEditMode(false);
return true; return true;
} }
if (keyutil.checkKeyPressed(e, "Cmd:s")) { if (checkKeyPressed(e, "Cmd:s")) {
model.handleFileSave(); model.handleFileSave();
return true; return true;
} }
if (keyutil.checkKeyPressed(e, "Cmd:r")) { if (checkKeyPressed(e, "Cmd:r")) {
model.handleFileRevert(); model.handleFileRevert();
return true; return true;
} }
return false; return false;
} }
React.useEffect(() => { useEffect(() => {
model.codeEditKeyDownHandler = codeEditKeyDownHandler; model.codeEditKeyDownHandler = codeEditKeyDownHandler;
return () => { return () => {
model.codeEditKeyDownHandler = null; model.codeEditKeyDownHandler = null;
@ -837,7 +832,7 @@ function CodeEditPreview({ model }: SpecializedViewProps) {
model.monacoRef.current = editor; model.monacoRef.current = editor;
editor.onKeyDown((e: MonacoTypes.IKeyboardEvent) => { editor.onKeyDown((e: MonacoTypes.IKeyboardEvent) => {
const waveEvent = keyutil.adaptFromReactOrNativeKeyEvent(e.browserEvent); const waveEvent = adaptFromReactOrNativeKeyEvent(e.browserEvent);
const handled = tryReinjectKey(waveEvent); const handled = tryReinjectKey(waveEvent);
if (handled) { if (handled) {
e.stopPropagation(); e.stopPropagation();
@ -864,8 +859,8 @@ function CodeEditPreview({ model }: SpecializedViewProps) {
} }
function CSVViewPreview({ model, parentRef }: SpecializedViewProps) { function CSVViewPreview({ model, parentRef }: SpecializedViewProps) {
const fileContent = jotai.useAtomValue(model.fileContent); const fileContent = useAtomValue(model.fileContent);
const fileName = jotai.useAtomValue(model.statFilePath); const fileName = useAtomValue(model.statFilePath);
return <CSVView parentRef={parentRef} readonly={true} content={fileContent} filename={fileName} />; return <CSVView parentRef={parentRef} readonly={true} content={fileContent} filename={fileName} />;
} }
@ -898,11 +893,11 @@ function iconForFile(mimeType: string): string {
} }
function SpecializedView({ parentRef, model }: SpecializedViewProps) { function SpecializedView({ parentRef, model }: SpecializedViewProps) {
const specializedView = jotai.useAtomValue(model.specializedView); const specializedView = useAtomValue(model.specializedView);
const mimeType = jotai.useAtomValue(model.fileMimeType); const mimeType = useAtomValue(model.fileMimeType);
const setCanPreview = jotai.useSetAtom(model.canPreview); const setCanPreview = useSetAtom(model.canPreview);
React.useEffect(() => { useEffect(() => {
setCanPreview(canPreview(mimeType)); setCanPreview(canPreview(mimeType));
}, [mimeType, setCanPreview]); }, [mimeType, setCanPreview]);
@ -927,7 +922,7 @@ function PreviewView({
contentRef: React.RefObject<HTMLDivElement>; contentRef: React.RefObject<HTMLDivElement>;
model: PreviewModel; model: PreviewModel;
}) { }) {
const connStatus = jotai.useAtomValue(model.connStatus); const connStatus = useAtomValue(model.connStatus);
if (connStatus?.status != "connected") { if (connStatus?.status != "connected") {
return null; return null;
} }
@ -943,7 +938,7 @@ function PreviewView({
); );
} }
const OpenFileModal = React.memo( const OpenFileModal = memo(
({ ({
model, model,
blockRef, blockRef,
@ -953,19 +948,19 @@ const OpenFileModal = React.memo(
blockRef: React.RefObject<HTMLDivElement>; blockRef: React.RefObject<HTMLDivElement>;
blockId: string; blockId: string;
}) => { }) => {
const openFileModal = jotai.useAtomValue(model.openFileModal); const openFileModal = useAtomValue(model.openFileModal);
const curFileName = jotai.useAtomValue(model.metaFilePath); const curFileName = useAtomValue(model.metaFilePath);
const [filePath, setFilePath] = useState(""); const [filePath, setFilePath] = useState("");
const isNodeFocused = jotai.useAtomValue(model.nodeModel.isFocused); const isNodeFocused = useAtomValue(model.nodeModel.isFocused);
const handleKeyDown = useCallback( const handleKeyDown = useCallback(
keyutil.keydownWrapper((waveEvent: WaveKeyboardEvent): boolean => { keydownWrapper((waveEvent: WaveKeyboardEvent): boolean => {
if (keyutil.checkKeyPressed(waveEvent, "Escape")) { if (checkKeyPressed(waveEvent, "Escape")) {
model.updateOpenFileModalAndError(false); model.updateOpenFileModalAndError(false);
return true; return true;
} }
const handleCommandOperations = async () => { const handleCommandOperations = async () => {
if (keyutil.checkKeyPressed(waveEvent, "Enter")) { if (checkKeyPressed(waveEvent, "Enter")) {
model.handleOpenFile(filePath); model.handleOpenFile(filePath);
return true; return true;
} }

View File

@ -124,6 +124,7 @@
"rehype-slug": "^6.0.0", "rehype-slug": "^6.0.0",
"remark-flexible-toc": "^1.1.1", "remark-flexible-toc": "^1.1.1",
"remark-gfm": "^4.0.0", "remark-gfm": "^4.0.0",
"remark-github-blockquote-alert": "^1.2.1",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"sharp": "^0.33.5", "sharp": "^0.33.5",
"shell-quote": "^1.8.1", "shell-quote": "^1.8.1",

View File

@ -9632,6 +9632,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"remark-github-blockquote-alert@npm:^1.2.1":
version: 1.2.1
resolution: "remark-github-blockquote-alert@npm:1.2.1"
dependencies:
unist-util-visit: "npm:^5.0.0"
checksum: 10c0/48f70a56347ba6d2649ec647f9b126fe2e22ee4efcbc4962e1645967ac0994859a930a1af3a8b75c2989d55ed5b7ce2df6fc86a4b0f2c0aa39d5ed2162c857ca
languageName: node
linkType: hard
"remark-parse@npm:^11.0.0": "remark-parse@npm:^11.0.0":
version: 11.0.0 version: 11.0.0
resolution: "remark-parse@npm:11.0.0" resolution: "remark-parse@npm:11.0.0"
@ -11496,6 +11505,7 @@ __metadata:
rehype-slug: "npm:^6.0.0" rehype-slug: "npm:^6.0.0"
remark-flexible-toc: "npm:^1.1.1" remark-flexible-toc: "npm:^1.1.1"
remark-gfm: "npm:^4.0.0" remark-gfm: "npm:^4.0.0"
remark-github-blockquote-alert: "npm:^1.2.1"
rollup-plugin-flow: "npm:^1.1.1" rollup-plugin-flow: "npm:^1.1.1"
rxjs: "npm:^7.8.1" rxjs: "npm:^7.8.1"
semver: "npm:^7.6.3" semver: "npm:^7.6.3"