Skip to content
Open
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
6 changes: 3 additions & 3 deletions packages/app-core/src/components/EditorPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ import { forwardOnCheckboxArrow } from '../lib/cm-forward-task'
import { markerHopCommands } from '../lib/cm-marker-hop'
import { isInMarkdownCode } from '../lib/cm-auto-pairs'
import { toggleCheckbox } from '../lib/cm-toggle-checkbox'
import { completionKeymapForEditor, completionNavKeymap } from '../lib/cm-completion-nav'
import { completionKeymapExtension, completionNavKeymap } from '../lib/cm-completion-nav'
import { vimAwareDefaultKeymap, vimAwareMarkdownKeymap } from '../lib/cm-vim-default-keymap'
import { isVimAwaitingArgument } from '../lib/vim-nav'
import { toCodeMirrorKey, vimHalfPageKeymap } from '../lib/vim-half-page-keymap'
Expand Down Expand Up @@ -427,8 +427,7 @@ function buildEditorKeymap(vimMode: boolean, overrides: KeymapOverrides): Extens
indentWithTab,
...vimAwareDefaultKeymap(vimMode),
...historyKeymap,
...searchKeymap,
...completionKeymapForEditor
...searchKeymap
])
}

Expand Down Expand Up @@ -1834,6 +1833,7 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element {
}
}),
completionNavKeymap,
completionKeymapExtension,
editorKeymapCompartment.of(buildEditorKeymap(s0.vimMode, s0.keymapOverrides)),
EditorView.domEventHandlers({
mousedown: (event, view) => {
Expand Down
6 changes: 3 additions & 3 deletions packages/app-core/src/components/PinnedReferencePane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ import {
} from '../lib/cm-wikilinks'
import { hashtagSource } from '../lib/cm-hashtag-complete'
import { frontmatterTagSource } from '../lib/cm-frontmatter-tag-complete'
import { completionKeymapForEditor, completionNavKeymap } from '../lib/cm-completion-nav'
import { completionKeymapExtension, completionNavKeymap } from '../lib/cm-completion-nav'
import { classifyLocalAssetHref, hrefFragment, type LocalAssetKind } from '../lib/local-assets'
import { LazyPreview as Preview } from './LazyPreview'
import { CloseIcon, PanelLeftIcon, PinIcon } from './icons'
Expand Down Expand Up @@ -262,6 +262,7 @@ export function PinnedReferencePane(): JSX.Element | null {
}
}),
completionNavKeymap,
completionKeymapExtension,
keymap.of([
{
key: 'Mod-f',
Expand All @@ -275,8 +276,7 @@ export function PinnedReferencePane(): JSX.Element | null {
indentWithTab,
...vimAwareDefaultKeymap(s0.vimMode),
...historyKeymap,
...searchKeymap,
...completionKeymapForEditor
...searchKeymap
]),
EditorView.updateListener.of((upd) => {
if (!upd.docChanged) return
Expand Down
4 changes: 2 additions & 2 deletions packages/app-core/src/components/QuickCaptureApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ import {
closeCompletion,
completionStatus
} from '@codemirror/autocomplete'
import { completionKeymapForEditor, completionNavKeymap } from '../lib/cm-completion-nav'
import { completionKeymapExtension, completionNavKeymap } from '../lib/cm-completion-nav'
import { slashCommandRender, templateSlashCommandSource } from '../lib/cm-slash-commands'
import { calloutTypeSource } from '../lib/cm-callouts'
import type { NoteMeta } from '@shared/ipc'
Expand Down Expand Up @@ -500,6 +500,7 @@ export function QuickCaptureApp(): JSX.Element {
: 'slash-cmd-option'
}),
completionNavKeymap,
completionKeymapExtension,
// Esc closes an open slash menu instead of bubbling to the window-level
// Esc that saves + hides the capture window. Runs before everything,
// and only when a completion is actually open.
Expand All @@ -517,7 +518,6 @@ export function QuickCaptureApp(): JSX.Element {
),
keymap.of([
indentWithTab,
...completionKeymapForEditor,
...vimAwareDefaultKeymap(prefs.vimMode),
...historyKeymap,
...searchKeymap
Expand Down
4 changes: 2 additions & 2 deletions packages/app-core/src/components/TemplateEditorModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { editorTabSize } from '../lib/editor-tab-size'
import { templateVariableSource, TEMPLATE_VARIABLES } from '../lib/cm-template-variables'
import { templateSlashCommandSource, slashCommandRender } from '../lib/cm-slash-commands'
import { calloutTypeSource } from '../lib/cm-callouts'
import { completionKeymapForEditor, completionNavKeymap } from '../lib/cm-completion-nav'
import { completionKeymapExtension, completionNavKeymap } from '../lib/cm-completion-nav'
import { Modal } from './ui/Modal'
import { Button } from './ui/Button'

Expand Down Expand Up @@ -158,9 +158,9 @@ export function TemplateEditorModal({
: 'slash-cmd-option'
}),
completionNavKeymap,
completionKeymapExtension,
keymap.of([
indentWithTab,
...completionKeymapForEditor,
...vimAwareDefaultKeymap(vimModeRef.current),
...historyKeymap
]),
Expand Down
105 changes: 105 additions & 0 deletions packages/app-core/src/lib/cm-completion-nav-arrows.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// @vitest-environment jsdom

import {
autocompletion,
currentCompletions,
selectedCompletionIndex,
startCompletion,
type CompletionContext,
type CompletionResult
} from '@codemirror/autocomplete'
import { defaultKeymap } from '@codemirror/commands'
import { EditorState } from '@codemirror/state'
import { EditorView, keymap } from '@codemirror/view'
import { describe, expect, it } from 'vitest'
import { completionKeymapExtension, completionNavKeymap } from './cm-completion-nav'

/** Stands in for the `@` date/note sources: three options, no filtering. */
function source(context: CompletionContext): CompletionResult | null {
const match = context.matchBefore(/@\w*/)
if (!match) return null
return {
from: match.from + 1,
options: [{ label: 'Today' }, { label: 'Tomorrow' }, { label: 'Yesterday' }],
filter: false
}
}

function mount(): EditorView {
return new EditorView({
state: EditorState.create({
doc: 'line one\nline two\n@\nline four',
selection: { anchor: 19 },
extensions: [
autocompletion({ defaultKeymap: false, override: [source] }),
completionNavKeymap,
completionKeymapExtension,
keymap.of([...defaultKeymap])
]
}),
parent: document.body
})
}

function press(view: EditorView, key: string): void {
view.contentDOM.dispatchEvent(
new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true })
)
}

/** Past `interactionDelay` (75ms), before which the popup ignores navigation. */
const settle = (): Promise<void> => new Promise((resolve) => setTimeout(resolve, 250))

describe('completion arrow navigation', () => {
it('moves the highlighted option instead of the caret', async () => {
const view = mount()
startCompletion(view)
await settle()
expect(currentCompletions(view.state).length).toBe(3)
expect(selectedCompletionIndex(view.state)).toBe(0)
const caret = view.state.selection.main.head

press(view, 'ArrowDown')
expect(selectedCompletionIndex(view.state)).toBe(1)
press(view, 'ArrowDown')
expect(selectedCompletionIndex(view.state)).toBe(2)
press(view, 'ArrowUp')
expect(selectedCompletionIndex(view.state)).toBe(1)

// Mounted below `defaultKeymap` instead, its ArrowUp/ArrowDown caret motions
// win and the caret move closes the menu — the regression this guards.
expect(currentCompletions(view.state).length).toBe(3)
expect(view.state.selection.main.head).toBe(caret)

view.destroy()
})

it('lets the arrows through when no completion is open', () => {
let reached = 0
const view = new EditorView({
state: EditorState.create({
doc: 'line one',
extensions: [
autocompletion({ defaultKeymap: false, override: [source] }),
completionNavKeymap,
completionKeymapExtension,
keymap.of([
{
key: 'ArrowDown',
run: () => {
reached += 1
return true
}
}
])
]
}),
parent: document.body
})

press(view, 'ArrowDown')
expect(reached).toBe(1)

view.destroy()
})
})
12 changes: 11 additions & 1 deletion packages/app-core/src/lib/cm-completion-nav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
selectedCompletion
} from '@codemirror/autocomplete'
import { Prec } from '@codemirror/state'
import { EditorView, type KeyBinding } from '@codemirror/view'
import { EditorView, keymap, type KeyBinding } from '@codemirror/view'

/**
* macOS AltGr-style keyboard layouts (custom Ukelele `.keylayout` files, a
Expand All @@ -26,6 +26,16 @@ export const completionKeymapForEditor: readonly KeyBinding[] = completionKeymap
(binding) => !(typeof binding.mac === 'string' && MAC_TEXT_ENTRY_CHORDS.has(binding.mac))
)

/**
* Mount this instead of spreading the bindings into an editor's general
* `keymap.of([...])`, where they lose to anything listed earlier — the arrows
* then move the caret, which closes the popup. `Prec.highest` is what
* `@codemirror/autocomplete` gives its own keymap.
*/
export const completionKeymapExtension = Prec.highest(
keymap.of([...completionKeymapForEditor])
)

/**
* Direction a Ctrl-based chord should move the autocomplete selection,
* or `null` when the event isn't one of our nav chords.
Expand Down