Skip to content

Commit b25833d

Browse files
authored
Merge branch 'main' into main
2 parents 4c33c1b + 17af9ac commit b25833d

32 files changed

Lines changed: 1346 additions & 491 deletions

apps/desktop/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@zennotes/desktop",
33
"productName": "ZenNotes",
4-
"version": "2.35.0",
4+
"version": "2.36.0",
55
"description": "ZenNotes desktop shell",
66
"private": true,
77
"main": "./out/main/index.js",

apps/server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@zennotes/server",
33
"private": true,
4-
"version": "2.35.0",
4+
"version": "2.36.0",
55
"scripts": {
66
"dev": "node ../../tooling/scripts/run-go-server-dev.mjs",
77
"prepare-web": "node ../../tooling/scripts/prepare-server-web-dist.mjs",

apps/web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@zennotes/web",
33
"private": true,
4-
"version": "2.35.0",
4+
"version": "2.36.0",
55
"type": "module",
66
"description": "ZenNotes web client for self-hosted and hosted deployments",
77
"homepage": "https://zennotes.org",

package-lock.json

Lines changed: 265 additions & 322 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "zennotes-monorepo",
33
"private": true,
4-
"version": "2.35.0",
4+
"version": "2.36.0",
55
"description": "ZenNotes monorepo for desktop, web, and self-hosted server builds",
66
"packageManager": "npm@10.9.2",
77
"engines": {

packages/app-core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@zennotes/app-core",
33
"private": true,
4-
"version": "2.35.0",
4+
"version": "2.36.0",
55
"type": "module",
66
"exports": {
77
"./main": "./src/main.tsx"

packages/app-core/src/components/EditorPane.tsx

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ import {
132132
} from '../lib/cm-wikilinks'
133133
import { linkRangeAtCursor, markdownLinkAt } from '../lib/internal-links'
134134
import { setBlockType, toggleWrap, wrapLink } from '../lib/cm-format'
135+
import { shouldShowSelectionToolbar } from '../lib/cm-selection-toolbar'
136+
import { editorCursorPosition } from '../lib/editor-cursor-position'
135137
import { EditorSelectionToolbar } from './EditorSelectionToolbar'
136138
import { appMarkdownSnippetExtension } from '../lib/markdown-snippets-config'
137139
import { LazyDiagramTabView, LazyPreview as Preview } from './LazyPreview'
@@ -197,7 +199,7 @@ import {
197199
recallTabScroll,
198200
type TabScrollPosition
199201
} from '../lib/tab-scroll-memory'
200-
import { parseOutline } from '../lib/outline'
202+
import { activeOutlineLineForCursor, parseOutline } from '../lib/outline'
201203
import {
202204
findRenderedHeadingForOutlineLine,
203205
nextOutlinePreviewSyncLockUntil,
@@ -750,6 +752,7 @@ function selectionEdgeCoords(view: EditorView): {
750752
}
751753

752754
function getSelectionCommentAction(view: EditorView): SelectionCommentAction {
755+
if (!shouldShowSelectionToolbar(view, useStore.getState().vimMode)) return null
753756
const sel = view.state.selection.main
754757
const active = document.activeElement
755758
// Keep the toolbar up while the editor holds the selection OR while the user
@@ -842,6 +845,9 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element {
842845
const setActiveCommentId = useStore((s) => s.setActiveCommentId)
843846

844847
const setEditorViewRef = useStore((s) => s.setEditorViewRef)
848+
const activeEditorCursorPosition = useStore((s) =>
849+
isActive ? s.editorCursorPosition : null
850+
)
845851
const sidebarOpen = useStore((s) => s.sidebarOpen)
846852
const zenMode = useStore((s) => s.zenMode)
847853
const toggleSidebar = useStore((s) => s.toggleSidebar)
@@ -1627,23 +1633,8 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element {
16271633
setActiveOutlineLineSafely(null)
16281634
return
16291635
}
1630-
// Probe ~25% down the viewport (capped) so a heading is considered
1631-
// active once it scrolls into the upper portion of the visible area
1632-
// — not only after it has scrolled past the very top edge.
1633-
const rect = view.scrollDOM.getBoundingClientRect()
1634-
const probeY = rect.top + Math.min(140, rect.height * 0.25)
1635-
const pos = view.posAtCoords({ x: rect.left + 8, y: probeY })
1636-
if (pos == null) {
1637-
setActiveOutlineLineSafely(null)
1638-
return
1639-
}
1640-
const probeLine = view.state.doc.lineAt(pos).number
1641-
let activeLine: number | null = null
1642-
for (const item of outlineItems) {
1643-
if (item.line <= probeLine) activeLine = item.line
1644-
else break
1645-
}
1646-
setActiveOutlineLineSafely(activeLine)
1636+
const cursorLine = view.state.doc.lineAt(view.state.selection.main.head).number
1637+
setActiveOutlineLineSafely(activeOutlineLineForCursor(outlineItems, cursorLine))
16471638
}, [outlineItems, setActiveOutlineLineSafely])
16481639

16491640
const computeActiveFromPreview = useCallback(() => {
@@ -1974,6 +1965,12 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element {
19741965
if (upd.viewportChanged || upd.geometryChanged) {
19751966
schedulePreviewSyncFromEditorViewport()
19761967
}
1968+
if (
1969+
(upd.selectionSet || upd.docChanged) &&
1970+
useStore.getState().editorViewRef === upd.view
1971+
) {
1972+
useStore.getState().setEditorCursorPosition(editorCursorPosition(upd.state))
1973+
}
19771974
if (!upd.docChanged) return
19781975
if (upd.transactions.some((tr: Transaction) => tr.annotation(programmatic))) return
19791976
const path = viewPathRef.current
@@ -3488,10 +3485,8 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element {
34883485
focusEditorNormalMode()
34893486
}, [applyPaneMode, mode])
34903487

3491-
// Track the topmost-visible heading and surface it as the active
3492-
// outline item. We listen on whichever surface is the user's scroll
3493-
// target for the current mode — split mode follows the editor since
3494-
// that's where typing happens.
3488+
// Editing follows the cursor so keyboard motion updates the Outline even
3489+
// when the viewport barely moves. Preview mode remains scroll-driven.
34953490
useEffect(() => {
34963491
if (!outlineOpen) {
34973492
setActiveOutlineLineSafely(null)
@@ -3512,6 +3507,7 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element {
35123507
}
35133508
}
35143509
}, [
3510+
activeEditorCursorPosition?.line,
35153511
computeActiveFromEditor,
35163512
content?.path,
35173513
mode,

packages/app-core/src/components/StatusBar.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { describe, expect, it } from "vitest";
66
import { formatRelativeSyncTime } from "../lib/cloud-auto-sync";
77
import { useCloudSyncStatusStore } from "../lib/cloud-auto-sync";
88
import { StatusBar } from "./StatusBar";
9+
import { useStore } from "../store";
10+
import type { NoteContent } from "@shared/ipc";
911

1012
(
1113
globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
@@ -95,4 +97,39 @@ describe("cloud sync status time", () => {
9597
act(() => root.unmount());
9698
host.remove();
9799
});
100+
101+
it("shows the active editor line and column on the right (discussion #597)", () => {
102+
useCloudSyncStatusStore.setState({ phase: "hidden" });
103+
useStore.setState({
104+
notes: [],
105+
editorCursorPosition: { line: 3, column: 5 },
106+
});
107+
const note = {
108+
path: "inbox/editor-position.md",
109+
title: "Editor position",
110+
folder: "inbox",
111+
siblingOrder: 0,
112+
createdAt: 0,
113+
updatedAt: 0,
114+
size: 16,
115+
tags: [],
116+
wikilinks: [],
117+
assetEmbeds: [],
118+
hasAttachments: false,
119+
excerpt: "alpha",
120+
body: "alpha\nbeta\ngamma",
121+
} as NoteContent;
122+
const host = document.createElement("div");
123+
document.body.append(host);
124+
const root = createRoot(host);
125+
126+
act(() => root.render(createElement(StatusBar, { note })));
127+
128+
const position = host.querySelector<HTMLElement>("[data-editor-position]");
129+
expect(position?.textContent).toBe("Ln 3, Col 5");
130+
expect(position?.getAttribute("aria-live")).toBeNull();
131+
132+
act(() => root.unmount());
133+
host.remove();
134+
});
98135
});

packages/app-core/src/components/StatusBar.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { requestSettingsTarget } from "../lib/settings-navigation";
2424
*/
2525
export function StatusBar({ note }: { note: NoteContent | null }): JSX.Element {
2626
const notes = useStore((s) => s.notes);
27+
const cursorPosition = useStore((s) => s.editorCursorPosition);
2728

2829
const { words, characters, minutes } = useMemo(() => {
2930
const body = note?.body ?? "";
@@ -69,6 +70,15 @@ export function StatusBar({ note }: { note: NoteContent | null }): JSX.Element {
6970
</Stat>
7071
<Stat>{characters.toLocaleString()} characters</Stat>
7172
<Stat>{minutes} min read</Stat>
73+
{cursorPosition && (
74+
<span
75+
data-editor-position
76+
className="tabular-nums"
77+
title={`Line ${cursorPosition.line}, column ${cursorPosition.column}`}
78+
>
79+
Ln {cursorPosition.line}, Col {cursorPosition.column}
80+
</span>
81+
)}
7282
</>
7383
)}
7484
</div>
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// @vitest-environment jsdom
2+
3+
import { afterEach, describe, expect, it } from 'vitest'
4+
import { EditorState } from '@codemirror/state'
5+
import { EditorView } from '@codemirror/view'
6+
import { getCM, vim } from '@replit/codemirror-vim'
7+
import { shouldShowSelectionToolbar } from './cm-selection-toolbar'
8+
9+
describe('selection toolbar in Vim mode (discussion #597)', () => {
10+
const views: EditorView[] = []
11+
12+
afterEach(() => {
13+
views.splice(0).forEach((view) => view.destroy())
14+
})
15+
16+
function mount(): EditorView {
17+
const view = new EditorView({
18+
state: EditorState.create({
19+
doc: 'alpha\nbeta\ngamma',
20+
extensions: [vim()]
21+
}),
22+
parent: document.body
23+
})
24+
views.push(view)
25+
view.focus()
26+
return view
27+
}
28+
29+
function press(view: EditorView, key: string, modifiers: KeyboardEventInit = {}): void {
30+
view.contentDOM.dispatchEvent(
31+
new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true, ...modifiers })
32+
)
33+
}
34+
35+
it('keeps the formatting toolbar out of normal and visual-block mode', () => {
36+
const view = mount()
37+
38+
expect(shouldShowSelectionToolbar(view, true)).toBe(false)
39+
40+
press(view, 'v', { ctrlKey: true })
41+
expect(getCM(view)?.state.vim?.visualBlock).toBe(true)
42+
expect(shouldShowSelectionToolbar(view, true)).toBe(false)
43+
})
44+
45+
it('still offers the toolbar for ordinary and Vim insert-mode selections', () => {
46+
const view = mount()
47+
48+
expect(shouldShowSelectionToolbar(view, false)).toBe(true)
49+
50+
press(view, 'i')
51+
expect(getCM(view)?.state.vim?.insertMode).toBe(true)
52+
expect(shouldShowSelectionToolbar(view, true)).toBe(true)
53+
})
54+
})

0 commit comments

Comments
 (0)