Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/store/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
import { registerCollectionIntegration } from "./integrations/register-collection-integration"
import { registerGitSyncWorkspaceIntegration } from "./integrations/register-git-sync-workspace-integration"
import { registerIndexingIntegration } from "./integrations/register-indexing-integration"
import { registerTabPathIntegration } from "./integrations/register-tab-path-integration"
import { createStoreEventHub } from "./integrations/store-events"
import type { TabSlice } from "./tab/tab-slice"
import { prepareTabSlice, type TabSliceDependencies } from "./tab/tab-slice"
Expand Down Expand Up @@ -95,6 +96,7 @@ export const createMditStore = (
registerCollectionIntegration(store, events)
registerIndexingIntegration(store, events)
registerGitSyncWorkspaceIntegration(store, events)
registerTabPathIntegration(store, events)

return store
}
131 changes: 131 additions & 0 deletions packages/store/src/integrations/integrations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@ import { describe, expect, it, vi } from "vitest"
import { createStore } from "zustand/vanilla"
import type { MditStore } from ".."
import { prepareCollectionSlice } from "../collection/collection-slice"
import {
prepareTabSlice,
type TabSlice,
type TabSliceDependencies,
} from "../tab/tab-slice"
import { registerCollectionIntegration } from "./register-collection-integration"
import { registerGitSyncWorkspaceIntegration } from "./register-git-sync-workspace-integration"
import { registerIndexingIntegration } from "./register-indexing-integration"
import { registerTabPathIntegration } from "./register-tab-path-integration"
import { createStoreEventHub } from "./store-events"

type EntryLike = {
Expand Down Expand Up @@ -41,6 +47,34 @@ const createCollectionIntegrationStore = () => {
}))
}

type TabIntegrationState = TabSlice & {
workspacePath: string | null
}

const createTabIntegrationStore = (
overrides: Partial<TabSliceDependencies> = {},
) => {
const readTextFile =
overrides.readTextFile ?? vi.fn(async (path: string) => `content:${path}`)
const renameFile = overrides.renameFile ?? vi.fn(async () => undefined)
const saveSettings = overrides.saveSettings ?? vi.fn(async () => undefined)
const createSlice = prepareTabSlice({
readTextFile,
renameFile,
saveSettings,
}) as any

const store = createStore<TabIntegrationState>()((set, get, api) => ({
workspacePath: "/ws",
...createSlice(set, get, api),
}))

return {
store,
readTextFile,
}
}

describe("store integrations", () => {
it("refreshes collection entries when workspace entries are replaced", async () => {
const events = createStoreEventHub()
Expand Down Expand Up @@ -202,6 +236,103 @@ describe("store integrations", () => {
expect(store.getState().collectionEntries).toEqual([])
})

it("removes deleted paths from tab history", async () => {
const events = createStoreEventHub()
const { store } = createTabIntegrationStore()

await store.getState().openTab("/ws/a.md")
await store.getState().openTab("/ws/folder/note.md")
await store.getState().openTab("/ws/keep.md")

registerTabPathIntegration(store as unknown as MditStore, events)
await events.emit({
type: "workspace/tab-paths-removed",
workspacePath: "/ws",
paths: ["/ws/a.md", "/ws/folder"],
})

expect(store.getState().tabs.map((tab) => tab.path)).toEqual([
"/ws/keep.md",
])
expect(store.getState().history.map((entry) => entry.path)).toEqual([
"/ws/keep.md",
])
expect(store.getState().historyIndex).toBe(0)
})

it("renames open tabs and history paths when a tab path is renamed", async () => {
const events = createStoreEventHub()
const { store } = createTabIntegrationStore()

await store.getState().openTab("/ws/old/note.md")

registerTabPathIntegration(store as unknown as MditStore, events)
await events.emit({
type: "workspace/tab-path-renamed",
workspacePath: "/ws",
oldPath: "/ws/old",
newPath: "/ws/new",
clearSyncedName: true,
})

expect(store.getState().tabs.map((tab) => tab.path)).toEqual([
"/ws/new/note.md",
])
expect(store.getState().history.map((entry) => entry.path)).toEqual([
"/ws/new/note.md",
])
})

it("moves open tabs and history paths when a tab path is moved", async () => {
const events = createStoreEventHub()
const { store, readTextFile } = createTabIntegrationStore()
const readTextFileMock = vi.mocked(readTextFile)

await store.getState().openTab("/ws/folder/note.md")
readTextFileMock.mockClear()

registerTabPathIntegration(store as unknown as MditStore, events)
await events.emit({
type: "workspace/tab-path-moved",
workspacePath: "/ws",
sourcePath: "/ws/folder",
newPath: "/ws/archive/folder",
refreshContent: true,
})

expect(store.getState().tabs.map((tab) => tab.path)).toEqual([
"/ws/archive/folder/note.md",
])
expect(store.getState().history.map((entry) => entry.path)).toEqual([
"/ws/archive/folder/note.md",
])
expect(readTextFileMock).not.toHaveBeenCalled()
})

it("ignores tab path events for a different workspace", async () => {
const events = createStoreEventHub()
const { store } = createTabIntegrationStore()

await store.getState().openTab("/other/a.md")
store.setState({ workspacePath: "/other" })

registerTabPathIntegration(store as unknown as MditStore, events)
await events.emit({
type: "workspace/tab-path-renamed",
workspacePath: "/ws",
oldPath: "/other/a.md",
newPath: "/other/b.md",
clearSyncedName: false,
})

expect(store.getState().tabs.map((tab) => tab.path)).toEqual([
"/other/a.md",
])
expect(store.getState().history.map((entry) => entry.path)).toEqual([
"/other/a.md",
])
})

it("resets indexing state when workspace resets", async () => {
const events = createStoreEventHub()
const state = {
Expand Down
40 changes: 40 additions & 0 deletions packages/store/src/integrations/register-tab-path-integration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { MditStore } from ".."
import type { StoreEventHub } from "./store-events"

export const registerTabPathIntegration = (
store: MditStore,
events: StoreEventHub,
) =>
events.subscribe(async (event) => {
const state = store.getState()

if (
state.workspacePath == null ||
state.workspacePath !== event.workspacePath
) {
return
}

switch (event.type) {
case "workspace/tab-paths-removed": {
state.removePathsFromHistory(event.paths)
return
}
case "workspace/tab-path-renamed": {
await state.renameTab(event.oldPath, event.newPath, {
clearSyncedName: event.clearSyncedName,
})
state.updateHistoryPath(event.oldPath, event.newPath)
return
}
case "workspace/tab-path-moved": {
await state.renameTab(event.sourcePath, event.newPath, {
refreshContent: event.refreshContent,
})
state.updateHistoryPath(event.sourcePath, event.newPath)
return
}
default:
return
}
})
19 changes: 19 additions & 0 deletions packages/store/src/integrations/store-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,25 @@ export type StoreEvent =
| { type: "workspace/reset"; workspacePath: string | null }
| { type: "workspace/loaded"; workspacePath: string }
| { type: "workspace/entries-replaced"; workspacePath: string }
| {
type: "workspace/tab-paths-removed"
workspacePath: string
paths: string[]
}
| {
type: "workspace/tab-path-renamed"
workspacePath: string
oldPath: string
newPath: string
clearSyncedName: boolean
}
| {
type: "workspace/tab-path-moved"
workspacePath: string
sourcePath: string
newPath: string
refreshContent: boolean
}
| {
type: "workspace/entry-created"
workspacePath: string
Expand Down
45 changes: 41 additions & 4 deletions packages/store/src/workspace/fs/structure-actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,11 +215,11 @@ describe("fs-structure-actions", () => {
})

it("renameEntry updates tab path in edit mode without entryRenamed", async () => {
const { context, deps, ports, setState, getState } =
const { context, deps, events, ports, setState, getState } =
createActionTestContext()
const actions = createFsStructureActions(context)
getState().entryRenamed = vi.fn().mockResolvedValue(undefined)
setState({ isEditMode: true })
setState({ isEditMode: true, workspacePath: "/ws" })

const renamedPath = await actions.renameEntry(
{
Expand All @@ -235,13 +235,50 @@ describe("fs-structure-actions", () => {
"/ws/old.md",
"/ws/new.md",
)
expect(ports.tab.renameTab).not.toHaveBeenCalled()
expect(events.emit).toHaveBeenCalledWith({
type: "workspace/tab-path-renamed",
workspacePath: "/ws",
oldPath: "/ws/old.md",
newPath: "/ws/new.md",
clearSyncedName: true,
})
expect(getState().entryRenamed).not.toHaveBeenCalled()
})

it("renameEntry falls back to direct tab rename in edit mode without workspace", async () => {
const { context, deps, events, ports, setState, getState } =
createActionTestContext()
const actions = createFsStructureActions(context)
getState().entryRenamed = vi.fn().mockResolvedValue(undefined)
setState({ isEditMode: true, workspacePath: null })

const renamedPath = await actions.renameEntry(
{
path: "/tmp/old.md",
name: "old.md",
isDirectory: false,
},
"new.md",
)

expect(renamedPath).toBe("/tmp/new.md")
expect(deps.fileSystemRepository.rename).toHaveBeenCalledWith(
"/tmp/old.md",
"/tmp/new.md",
)
expect(ports.tab.renameTab).toHaveBeenCalledWith(
"/ws/old.md",
"/ws/new.md",
"/tmp/old.md",
"/tmp/new.md",
{
clearSyncedName: true,
},
)
expect(ports.tab.updateHistoryPath).toHaveBeenCalledWith(
"/tmp/old.md",
"/tmp/new.md",
)
expect(events.emit).not.toHaveBeenCalled()
expect(getState().entryRenamed).not.toHaveBeenCalled()
})

Expand Down
17 changes: 16 additions & 1 deletion packages/store/src/workspace/fs/structure-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,22 @@ export const createFsStructureActions = (
!entry.isDirectory && !options?.preserveActiveTabSyncedName

if (ctx.get().isEditMode) {
await ctx.ports.tab.renameTab(entry.path, nextPath, { clearSyncedName })
const { workspacePath } = ctx.get()
if (!workspacePath) {
await ctx.ports.tab.renameTab(entry.path, nextPath, {
clearSyncedName,
})
ctx.ports.tab.updateHistoryPath(entry.path, nextPath)
return nextPath
Comment thread
hhhjin marked this conversation as resolved.
}

await ctx.runtime.events.emit({
type: "workspace/tab-path-renamed",
workspacePath,
oldPath: entry.path,
newPath: nextPath,
clearSyncedName,
})
return nextPath
}

Expand Down
Loading
Loading