mirror of
https://github.com/stablyai/orca
synced 2026-04-21 14:17:16 +00:00
WIP: uncommitted changes before rebase (#168)
This commit is contained in:
parent
cf0565ab97
commit
e699bec46e
11 changed files with 1455 additions and 16 deletions
|
|
@ -48,6 +48,10 @@
|
|||
"monaco-editor": "^0.55.1",
|
||||
"node-pty": "^1.1.0",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react-markdown": "^10.1.0",
|
||||
"rehype-highlight": "^7.0.2",
|
||||
"remark-frontmatter": "^5.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"shadcn": "^4.1.0",
|
||||
"simple-git": "^3.33.0",
|
||||
"sonner": "^2.0.7",
|
||||
|
|
|
|||
962
pnpm-lock.yaml
962
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -424,6 +424,141 @@
|
|||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* ── Markdown Preview ────────────────────────────────── */
|
||||
|
||||
.markdown-preview {
|
||||
padding: 24px 32px;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.markdown-body h1,
|
||||
.markdown-body h2,
|
||||
.markdown-body h3,
|
||||
.markdown-body h4,
|
||||
.markdown-body h5,
|
||||
.markdown-body h6 {
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 0.5em;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.markdown-body h1 {
|
||||
font-size: 1.75em;
|
||||
}
|
||||
.markdown-body h2 {
|
||||
font-size: 1.4em;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding-bottom: 0.3em;
|
||||
}
|
||||
.markdown-body h3 {
|
||||
font-size: 1.15em;
|
||||
}
|
||||
|
||||
.markdown-body p {
|
||||
margin: 0.75em 0;
|
||||
}
|
||||
|
||||
.markdown-body a {
|
||||
color: var(--primary);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.markdown-body code {
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9em;
|
||||
font-family: var(--font-mono, monospace);
|
||||
}
|
||||
|
||||
.markdown-dark .markdown-body code {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.markdown-light .markdown-body code {
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.markdown-body pre {
|
||||
margin: 1em 0;
|
||||
padding: 12px 16px;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.markdown-dark .markdown-body pre {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.markdown-light .markdown-body pre {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.markdown-body pre code {
|
||||
padding: 0;
|
||||
background: none;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.markdown-body ul,
|
||||
.markdown-body ol {
|
||||
margin: 0.75em 0;
|
||||
padding-left: 1.5em;
|
||||
}
|
||||
|
||||
.markdown-body li {
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
|
||||
.markdown-body li > input[type='checkbox'] {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.markdown-body blockquote {
|
||||
margin: 0.75em 0;
|
||||
padding: 0.25em 1em;
|
||||
border-left: 3px solid var(--border);
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.markdown-body table {
|
||||
margin: 1em 0;
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.markdown-body th,
|
||||
.markdown-body td {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid var(--border);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.markdown-body th {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown-dark .markdown-body th {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.markdown-light .markdown-body th {
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.markdown-body hr {
|
||||
margin: 1.5em 0;
|
||||
border: none;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.markdown-body img {
|
||||
max-width: 100%;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* ── Content ─────────────────────────────────────────── */
|
||||
|
||||
.content-area {
|
||||
|
|
|
|||
|
|
@ -4,10 +4,13 @@ import { useAppStore } from '@/store'
|
|||
import { detectLanguage } from '@/lib/language-detect'
|
||||
import { getEditorHeaderCopyState } from './editor-header'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import type { MarkdownViewMode } from '@/store/slices/editor'
|
||||
import MarkdownViewToggle from './MarkdownViewToggle'
|
||||
|
||||
const MonacoEditor = lazy(() => import('./MonacoEditor'))
|
||||
const DiffViewer = lazy(() => import('./DiffViewer'))
|
||||
const CombinedDiffViewer = lazy(() => import('./CombinedDiffViewer'))
|
||||
const MarkdownPreview = lazy(() => import('./MarkdownPreview'))
|
||||
|
||||
type FileContent = {
|
||||
content: string
|
||||
|
|
@ -25,6 +28,9 @@ export default function EditorPanel(): React.JSX.Element | null {
|
|||
const markFileDirty = useAppStore((s) => s.markFileDirty)
|
||||
const pendingEditorReveal = useAppStore((s) => s.pendingEditorReveal)
|
||||
|
||||
const markdownViewMode = useAppStore((s) => s.markdownViewMode)
|
||||
const setMarkdownViewMode = useAppStore((s) => s.setMarkdownViewMode)
|
||||
|
||||
const activeFile = openFiles.find((f) => f.id === activeFileId) ?? null
|
||||
|
||||
const [fileContents, setFileContents] = useState<Record<string, FileContent>>({})
|
||||
|
|
@ -244,12 +250,42 @@ export default function EditorPanel(): React.JSX.Element | null {
|
|||
? detectLanguage(activeFile.relativePath)
|
||||
: detectLanguage(activeFile.filePath)
|
||||
|
||||
const isMarkdown = resolvedLanguage === 'markdown'
|
||||
const mdViewMode: MarkdownViewMode =
|
||||
isMarkdown && activeFile.mode === 'edit'
|
||||
? (markdownViewMode[activeFile.id] ?? 'source')
|
||||
: 'source'
|
||||
|
||||
const loadingFallback = (
|
||||
<div className="flex items-center justify-center h-full text-muted-foreground text-sm">
|
||||
Loading editor...
|
||||
</div>
|
||||
)
|
||||
|
||||
const renderMonacoEditor = (fc: FileContent): React.JSX.Element => (
|
||||
<MonacoEditor
|
||||
filePath={activeFile.filePath}
|
||||
relativePath={activeFile.relativePath}
|
||||
content={editBuffers[activeFile.id] ?? fc.content}
|
||||
language={resolvedLanguage}
|
||||
onContentChange={handleContentChange}
|
||||
onSave={handleSave}
|
||||
revealLine={pendingEditorReveal?.line}
|
||||
revealColumn={pendingEditorReveal?.column}
|
||||
revealMatchLength={pendingEditorReveal?.matchLength}
|
||||
/>
|
||||
)
|
||||
|
||||
const renderMarkdownContent = (fc: FileContent): React.JSX.Element => {
|
||||
const currentContent = editBuffers[activeFile.id] ?? fc.content
|
||||
|
||||
if (mdViewMode === 'preview') {
|
||||
return <MarkdownPreview content={currentContent} filePath={activeFile.filePath} />
|
||||
}
|
||||
|
||||
return renderMonacoEditor(fc)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-1 min-w-0 min-h-0">
|
||||
<div className="editor-header">
|
||||
|
|
@ -288,6 +324,12 @@ export default function EditorPanel(): React.JSX.Element | null {
|
|||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
{isMarkdown && activeFile.mode === 'edit' && (
|
||||
<MarkdownViewToggle
|
||||
mode={mdViewMode}
|
||||
onChange={(mode) => setMarkdownViewMode(activeFile.id, mode)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Suspense fallback={loadingFallback}>
|
||||
{isCombinedDiff ? (
|
||||
|
|
@ -309,19 +351,7 @@ export default function EditorPanel(): React.JSX.Element | null {
|
|||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<MonacoEditor
|
||||
filePath={activeFile.filePath}
|
||||
relativePath={activeFile.relativePath}
|
||||
content={editBuffers[activeFile.id] ?? fc.content}
|
||||
language={resolvedLanguage}
|
||||
onContentChange={handleContentChange}
|
||||
onSave={handleSave}
|
||||
revealLine={pendingEditorReveal?.line}
|
||||
revealColumn={pendingEditorReveal?.column}
|
||||
revealMatchLength={pendingEditorReveal?.matchLength}
|
||||
/>
|
||||
)
|
||||
return isMarkdown ? renderMarkdownContent(fc) : renderMonacoEditor(fc)
|
||||
})()
|
||||
) : (
|
||||
(() => {
|
||||
|
|
|
|||
81
src/renderer/src/components/editor/MarkdownPreview.tsx
Normal file
81
src/renderer/src/components/editor/MarkdownPreview.tsx
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import React from 'react'
|
||||
import Markdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import remarkFrontmatter from 'remark-frontmatter'
|
||||
import rehypeHighlight from 'rehype-highlight'
|
||||
import type { Components } from 'react-markdown'
|
||||
import { useAppStore } from '@/store'
|
||||
import { getMarkdownPreviewImageSrc, getMarkdownPreviewLinkTarget } from './markdown-preview-links'
|
||||
|
||||
type MarkdownPreviewProps = {
|
||||
content: string
|
||||
filePath: string
|
||||
}
|
||||
|
||||
export default function MarkdownPreview({
|
||||
content,
|
||||
filePath
|
||||
}: MarkdownPreviewProps): React.JSX.Element {
|
||||
const settings = useAppStore((s) => s.settings)
|
||||
const isDark =
|
||||
settings?.theme === 'dark' ||
|
||||
(settings?.theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||
|
||||
const components: Components = {
|
||||
a: ({ href, children, ...props }) => {
|
||||
const handleClick = (event: React.MouseEvent<HTMLAnchorElement>): void => {
|
||||
if (!href || href.startsWith('#')) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
|
||||
const target = getMarkdownPreviewLinkTarget(href, filePath)
|
||||
if (!target) {
|
||||
return
|
||||
}
|
||||
|
||||
let parsed: URL
|
||||
try {
|
||||
parsed = new URL(target)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
|
||||
void window.api.shell.openUrl(parsed.toString())
|
||||
return
|
||||
}
|
||||
|
||||
if (parsed.protocol === 'file:') {
|
||||
void window.api.shell.openFileUri(parsed.toString())
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<a {...props} href={href} onClick={handleClick}>
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
},
|
||||
img: ({ src, alt, ...props }) => (
|
||||
<img {...props} src={getMarkdownPreviewImageSrc(src, filePath)} alt={alt ?? ''} />
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`markdown-preview flex-1 min-h-0 overflow-auto ${isDark ? 'markdown-dark' : 'markdown-light'}`}
|
||||
>
|
||||
<div className="markdown-body">
|
||||
<Markdown
|
||||
components={components}
|
||||
remarkPlugins={[remarkGfm, remarkFrontmatter]}
|
||||
rehypePlugins={[rehypeHighlight]}
|
||||
>
|
||||
{content}
|
||||
</Markdown>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
36
src/renderer/src/components/editor/MarkdownViewToggle.tsx
Normal file
36
src/renderer/src/components/editor/MarkdownViewToggle.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import React from 'react'
|
||||
import { Code, BookOpen } from 'lucide-react'
|
||||
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
|
||||
import type { MarkdownViewMode } from '@/store/slices/editor'
|
||||
|
||||
type MarkdownViewToggleProps = {
|
||||
mode: MarkdownViewMode
|
||||
onChange: (mode: MarkdownViewMode) => void
|
||||
}
|
||||
|
||||
export default function MarkdownViewToggle({
|
||||
mode,
|
||||
onChange
|
||||
}: MarkdownViewToggleProps): React.JSX.Element {
|
||||
return (
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
size="sm"
|
||||
className="h-6 [&_[data-slot=toggle-group-item]]:h-7 [&_[data-slot=toggle-group-item]]:min-w-5 [&_[data-slot=toggle-group-item]]:px-2.5"
|
||||
variant="outline"
|
||||
value={mode}
|
||||
onValueChange={(v) => {
|
||||
if (v) {
|
||||
onChange(v as MarkdownViewMode)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ToggleGroupItem value="source" aria-label="Source" title="Source">
|
||||
<Code className="h-2 w-2" />
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="preview" aria-label="Preview" title="Preview">
|
||||
<BookOpen className="h-2 w-2" />
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
)
|
||||
}
|
||||
|
|
@ -158,7 +158,7 @@ export default function MonacoEditor({
|
|||
onChange={handleChange}
|
||||
onMount={handleMount}
|
||||
options={{
|
||||
minimap: { enabled: true },
|
||||
minimap: { enabled: false },
|
||||
scrollBeyondLastLine: false,
|
||||
wordWrap: 'on',
|
||||
fontSize: settings?.terminalFontSize ?? 13,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { getMarkdownPreviewImageSrc, getMarkdownPreviewLinkTarget } from './markdown-preview-links'
|
||||
|
||||
describe('getMarkdownPreviewLinkTarget', () => {
|
||||
it('resolves relative markdown links against the current file', () => {
|
||||
expect(getMarkdownPreviewLinkTarget('./guide/setup.md', '/repo/docs/README.md')).toBe(
|
||||
'file:///repo/docs/guide/setup.md'
|
||||
)
|
||||
})
|
||||
|
||||
it('preserves external links', () => {
|
||||
expect(getMarkdownPreviewLinkTarget('https://example.com/docs', '/repo/docs/README.md')).toBe(
|
||||
'https://example.com/docs'
|
||||
)
|
||||
})
|
||||
|
||||
it('does not hijack hash-only anchors', () => {
|
||||
expect(getMarkdownPreviewLinkTarget('#overview', '/repo/docs/README.md')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getMarkdownPreviewImageSrc', () => {
|
||||
it('resolves relative image paths against the current file', () => {
|
||||
expect(getMarkdownPreviewImageSrc('../assets/diagram.png', '/repo/docs/guides/README.md')).toBe(
|
||||
'file:///repo/docs/assets/diagram.png'
|
||||
)
|
||||
})
|
||||
|
||||
it('resolves relative paths for Windows markdown files', () => {
|
||||
expect(getMarkdownPreviewImageSrc('./diagram.png', 'C:\\repo\\docs\\README.md')).toBe(
|
||||
'file:///C:/repo/docs/diagram.png'
|
||||
)
|
||||
})
|
||||
|
||||
it('leaves unsupported schemes unchanged', () => {
|
||||
expect(getMarkdownPreviewImageSrc('data:image/png;base64,abc', '/repo/docs/README.md')).toBe(
|
||||
'data:image/png;base64,abc'
|
||||
)
|
||||
})
|
||||
})
|
||||
75
src/renderer/src/components/editor/markdown-preview-links.ts
Normal file
75
src/renderer/src/components/editor/markdown-preview-links.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
function toFileUrl(filePath: string): string {
|
||||
const normalizedPath = filePath.replaceAll('\\', '/')
|
||||
const segments = normalizedPath.split('/').map((segment, index) => {
|
||||
if (index === 0 && /^[A-Za-z]:$/.test(segment)) {
|
||||
return segment
|
||||
}
|
||||
return encodeURIComponent(segment)
|
||||
})
|
||||
|
||||
if (normalizedPath.startsWith('/')) {
|
||||
return `file://${segments.join('/')}`
|
||||
}
|
||||
|
||||
return `file:///${segments.join('/')}`
|
||||
}
|
||||
|
||||
function resolveMarkdownUrl(rawUrl: string, filePath: string): URL | null {
|
||||
if (!rawUrl || rawUrl.startsWith('#')) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
return new URL(rawUrl, toFileUrl(filePath))
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function getMarkdownPreviewLinkTarget(
|
||||
rawHref: string | undefined,
|
||||
filePath: string
|
||||
): string | null {
|
||||
if (!rawHref) {
|
||||
return null
|
||||
}
|
||||
|
||||
const resolved = resolveMarkdownUrl(rawHref, filePath)
|
||||
if (!resolved) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (
|
||||
resolved.protocol === 'http:' ||
|
||||
resolved.protocol === 'https:' ||
|
||||
resolved.protocol === 'file:'
|
||||
) {
|
||||
return resolved.toString()
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function getMarkdownPreviewImageSrc(
|
||||
rawSrc: string | undefined,
|
||||
filePath: string
|
||||
): string | undefined {
|
||||
if (!rawSrc) {
|
||||
return rawSrc
|
||||
}
|
||||
|
||||
const resolved = resolveMarkdownUrl(rawSrc, filePath)
|
||||
if (!resolved) {
|
||||
return rawSrc
|
||||
}
|
||||
|
||||
if (
|
||||
resolved.protocol === 'http:' ||
|
||||
resolved.protocol === 'https:' ||
|
||||
resolved.protocol === 'file:'
|
||||
) {
|
||||
return resolved.toString()
|
||||
}
|
||||
|
||||
return rawSrc
|
||||
}
|
||||
|
|
@ -87,3 +87,40 @@ describe('createEditorSlice openDiff', () => {
|
|||
expect(store.getState().activeFileId).toBe('/repo/file.ts::staged')
|
||||
})
|
||||
})
|
||||
|
||||
describe('createEditorSlice markdown preview state', () => {
|
||||
it('drops markdown view mode for a replaced preview tab', () => {
|
||||
const store = createEditorStore()
|
||||
|
||||
store.getState().openFile(
|
||||
{
|
||||
filePath: '/repo/docs/README.md',
|
||||
relativePath: 'docs/README.md',
|
||||
worktreeId: 'wt-1',
|
||||
language: 'markdown',
|
||||
mode: 'edit'
|
||||
},
|
||||
{ preview: true }
|
||||
)
|
||||
store.getState().setMarkdownViewMode('/repo/docs/README.md', 'preview')
|
||||
|
||||
store.getState().openFile(
|
||||
{
|
||||
filePath: '/repo/docs/guide.md',
|
||||
relativePath: 'docs/guide.md',
|
||||
worktreeId: 'wt-1',
|
||||
language: 'markdown',
|
||||
mode: 'edit'
|
||||
},
|
||||
{ preview: true }
|
||||
)
|
||||
|
||||
expect(store.getState().markdownViewMode).toEqual({})
|
||||
expect(store.getState().openFiles).toEqual([
|
||||
expect.objectContaining({
|
||||
id: '/repo/docs/guide.md',
|
||||
isPreview: true
|
||||
})
|
||||
])
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -18,7 +18,13 @@ export type OpenFile = {
|
|||
export type RightSidebarTab = 'explorer' | 'search' | 'source-control' | 'checks'
|
||||
export type ActivityBarPosition = 'top' | 'side'
|
||||
|
||||
export type MarkdownViewMode = 'source' | 'preview'
|
||||
|
||||
export type EditorSlice = {
|
||||
// Markdown view mode per file (fileId -> mode)
|
||||
markdownViewMode: Record<string, MarkdownViewMode>
|
||||
setMarkdownViewMode: (fileId: string, mode: MarkdownViewMode) => void
|
||||
|
||||
// Right sidebar
|
||||
rightSidebarOpen: boolean
|
||||
rightSidebarWidth: number
|
||||
|
|
@ -94,6 +100,13 @@ export type EditorSlice = {
|
|||
}
|
||||
|
||||
export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (set) => ({
|
||||
// Markdown view mode
|
||||
markdownViewMode: {},
|
||||
setMarkdownViewMode: (fileId, mode) =>
|
||||
set((s) => ({
|
||||
markdownViewMode: { ...s.markdownViewMode, [fileId]: mode }
|
||||
})),
|
||||
|
||||
// Right sidebar
|
||||
rightSidebarOpen: false,
|
||||
rightSidebarWidth: 280,
|
||||
|
|
@ -177,11 +190,24 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (s
|
|||
(f) => f.worktreeId === worktreeId && f.isPreview
|
||||
)
|
||||
if (existingPreviewIdx !== -1) {
|
||||
const replacedPreview = s.openFiles[existingPreviewIdx]
|
||||
const nextMarkdownViewMode =
|
||||
replacedPreview.id === id
|
||||
? s.markdownViewMode
|
||||
: Object.fromEntries(
|
||||
Object.entries(s.markdownViewMode).filter(
|
||||
([fileId]) => fileId !== replacedPreview.id
|
||||
)
|
||||
)
|
||||
// Replace in-place to preserve tab position
|
||||
newFiles = s.openFiles.map((f, i) =>
|
||||
i === existingPreviewIdx ? { ...file, id, isDirty: false, isPreview: true } : f
|
||||
)
|
||||
return { openFiles: newFiles, ...activeResult }
|
||||
return {
|
||||
openFiles: newFiles,
|
||||
markdownViewMode: nextMarkdownViewMode,
|
||||
...activeResult
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -210,6 +236,8 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (s
|
|||
const closedFile = s.openFiles.find((f) => f.id === fileId)
|
||||
const idx = s.openFiles.findIndex((f) => f.id === fileId)
|
||||
const newFiles = s.openFiles.filter((f) => f.id !== fileId)
|
||||
const newMarkdownViewMode = { ...s.markdownViewMode }
|
||||
delete newMarkdownViewMode[fileId]
|
||||
let newActiveId = s.activeFileId
|
||||
const newActiveFileIdByWorktree = { ...s.activeFileIdByWorktree }
|
||||
|
||||
|
|
@ -255,6 +283,7 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (s
|
|||
activeTabType: newActiveTabType,
|
||||
activeFileIdByWorktree: newActiveFileIdByWorktree,
|
||||
activeTabTypeByWorktree: newActiveTabTypeByWorktree,
|
||||
markdownViewMode: newMarkdownViewMode,
|
||||
pendingEditorReveal: null
|
||||
}
|
||||
}),
|
||||
|
|
@ -263,10 +292,19 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (s
|
|||
set((s) => {
|
||||
const activeWorktreeId = s.activeWorktreeId
|
||||
if (!activeWorktreeId) {
|
||||
return { openFiles: [], activeFileId: null, activeTabType: 'terminal' }
|
||||
return {
|
||||
openFiles: [],
|
||||
activeFileId: null,
|
||||
activeTabType: 'terminal',
|
||||
markdownViewMode: {}
|
||||
}
|
||||
}
|
||||
// Only close files for the current worktree
|
||||
const newFiles = s.openFiles.filter((f) => f.worktreeId !== activeWorktreeId)
|
||||
const remainingFileIds = new Set(newFiles.map((f) => f.id))
|
||||
const newMarkdownViewMode = Object.fromEntries(
|
||||
Object.entries(s.markdownViewMode).filter(([fileId]) => remainingFileIds.has(fileId))
|
||||
)
|
||||
const newActiveFileIdByWorktree = { ...s.activeFileIdByWorktree }
|
||||
delete newActiveFileIdByWorktree[activeWorktreeId]
|
||||
const newActiveTabTypeByWorktree = { ...s.activeTabTypeByWorktree }
|
||||
|
|
@ -275,6 +313,7 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (s
|
|||
openFiles: newFiles,
|
||||
activeFileId: null,
|
||||
activeTabType: 'terminal',
|
||||
markdownViewMode: newMarkdownViewMode,
|
||||
activeFileIdByWorktree: newActiveFileIdByWorktree,
|
||||
activeTabTypeByWorktree: newActiveTabTypeByWorktree
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue