diff --git a/packages/web/src/javascripts/Components/LinkedItems/LinkedItemBubble.tsx b/packages/web/src/javascripts/Components/LinkedItems/LinkedItemBubble.tsx
index 432ab05b3..5d1c54499 100644
--- a/packages/web/src/javascripts/Components/LinkedItems/LinkedItemBubble.tsx
+++ b/packages/web/src/javascripts/Components/LinkedItems/LinkedItemBubble.tsx
@@ -10,6 +10,7 @@ import { LinkableItem } from '@/Utils/Items/Search/LinkableItem'
import { getIconForItem } from '@/Utils/Items/Icons/getIconForItem'
import { useApplication } from '../ApplicationProvider'
import { getTitleForLinkedTag } from '@/Utils/Items/Display/getTitleForLinkedTag'
+import { getItemTitleInContextOfLinkBubble } from '@/Utils/Items/Search/doesItemMatchSearchQuery'
type Props = {
link: ItemLink
@@ -117,7 +118,7 @@ const LinkedItemBubble = ({
{link.type === 'linked-by' && link.item.content_type !== ContentType.Tag && (
Linked By:
)}
- {link.item.title}
+ {getItemTitleInContextOfLinkBubble(link.item)}
{showUnlinkButton && (
diff --git a/packages/web/src/javascripts/Components/LinkedItems/LinkedItemMeta.tsx b/packages/web/src/javascripts/Components/LinkedItems/LinkedItemMeta.tsx
index 00d523876..a4f993f56 100644
--- a/packages/web/src/javascripts/Components/LinkedItems/LinkedItemMeta.tsx
+++ b/packages/web/src/javascripts/Components/LinkedItems/LinkedItemMeta.tsx
@@ -6,6 +6,7 @@ import { LinkableItem } from '@/Utils/Items/Search/LinkableItem'
import { observer } from 'mobx-react-lite'
import { useApplication } from '../ApplicationProvider'
import Icon from '../Icon/Icon'
+import { getItemTitleInContextOfLinkBubble } from '@/Utils/Items/Search/doesItemMatchSearchQuery'
type Props = {
item: LinkableItem
@@ -16,7 +17,7 @@ const LinkedItemMeta = ({ item, searchQuery }: Props) => {
const application = useApplication()
const [icon, className] = getIconForItem(item, application)
const tagTitle = getTitleForLinkedTag(item, application)
- const title = item.title ?? ''
+ const title = getItemTitleInContextOfLinkBubble(item)
return (
<>
diff --git a/packages/web/src/javascripts/Components/NoteView/NoteView.tsx b/packages/web/src/javascripts/Components/NoteView/NoteView.tsx
index 606685a82..53c074243 100644
--- a/packages/web/src/javascripts/Components/NoteView/NoteView.tsx
+++ b/packages/web/src/javascripts/Components/NoteView/NoteView.tsx
@@ -905,9 +905,7 @@ class NoteView extends AbstractComponent {
)}
- {editorMode !== 'super' && (
-
- )}
+
)}
diff --git a/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ItemSelectionPlugin/ItemSelectionPlugin.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ItemSelectionPlugin/ItemSelectionPlugin.tsx
index 09bd136f3..898cfcb98 100644
--- a/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ItemSelectionPlugin/ItemSelectionPlugin.tsx
+++ b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ItemSelectionPlugin/ItemSelectionPlugin.tsx
@@ -1,5 +1,5 @@
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
-import { LexicalTypeaheadMenuPlugin, useBasicTypeaheadTriggerMatch } from '@lexical/react/LexicalTypeaheadMenuPlugin'
+import { LexicalTypeaheadMenuPlugin } from '@lexical/react/LexicalTypeaheadMenuPlugin'
import { TextNode } from 'lexical'
import { FunctionComponent, useCallback, useMemo, useState } from 'react'
import { ItemSelectionItemComponent } from './ItemSelectionItemComponent'
@@ -12,6 +12,7 @@ import { INSERT_BUBBLE_COMMAND, INSERT_FILE_COMMAND } from '../Commands'
import { useLinkingController } from '../../../../../Controllers/LinkingControllerProvider'
import { PopoverClassNames } from '../ClassNames'
import { isMobileScreen } from '@/Utils'
+import { useTypeaheadAllowingSpacesAndPunctuation } from './useTypeaheadAllowingSpacesAndPunctuation'
type Props = {
currentNote: SNNote
@@ -26,7 +27,7 @@ export const ItemSelectionPlugin: FunctionComponent = ({ currentNote }) =
const [queryString, setQueryString] = useState('')
- const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('@', {
+ const checkForTriggerMatch = useTypeaheadAllowingSpacesAndPunctuation('@', {
minLength: 0,
})
diff --git a/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ItemSelectionPlugin/useTypeaheadAllowingSpacesAndPunctuation.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ItemSelectionPlugin/useTypeaheadAllowingSpacesAndPunctuation.tsx
new file mode 100644
index 000000000..8a601dcf4
--- /dev/null
+++ b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ItemSelectionPlugin/useTypeaheadAllowingSpacesAndPunctuation.tsx
@@ -0,0 +1,41 @@
+import { LexicalEditor } from 'lexical'
+import { useCallback } from 'react'
+
+export type QueryMatch = {
+ leadOffset: number
+ matchingString: string
+ replaceableString: string
+}
+type TriggerFn = (text: string, editor: LexicalEditor) => QueryMatch | null
+
+/**
+ * Derived from
+ * https://github.com/facebook/lexical/blob/main/packages/lexical-react/src/LexicalTypeaheadMenuPlugin.tsx#L545
+ */
+export function useTypeaheadAllowingSpacesAndPunctuation(
+ trigger: string,
+ { minLength = 1, maxLength = 75 }: { minLength?: number; maxLength?: number },
+): TriggerFn {
+ return useCallback(
+ (text: string) => {
+ const validChars = '[^' + trigger + ']'
+ const TypeaheadTriggerRegex = new RegExp(
+ '(^|\\s|\\()(' + '[' + trigger + ']' + '((?:' + validChars + '){0,' + maxLength + '})' + ')$',
+ )
+ const match = TypeaheadTriggerRegex.exec(text)
+ if (match !== null) {
+ const maybeLeadingWhitespace = match[1]
+ const matchingString = match[3]
+ if (matchingString.length >= minLength) {
+ return {
+ leadOffset: match.index + maybeLeadingWhitespace.length,
+ matchingString,
+ replaceableString: match[2],
+ }
+ }
+ }
+ return null
+ },
+ [maxLength, minLength, trigger],
+ )
+}
diff --git a/packages/web/src/javascripts/Utils/Items/Search/doesItemMatchSearchQuery.ts b/packages/web/src/javascripts/Utils/Items/Search/doesItemMatchSearchQuery.ts
index 351561d2d..4405d7ef2 100644
--- a/packages/web/src/javascripts/Utils/Items/Search/doesItemMatchSearchQuery.ts
+++ b/packages/web/src/javascripts/Utils/Items/Search/doesItemMatchSearchQuery.ts
@@ -1,12 +1,26 @@
-import { SNTag, WebApplicationInterface, DecryptedItemInterface, ItemContent } from '@standardnotes/snjs'
+import { WebApplicationInterface, DecryptedItemInterface, ItemContent, isNote, isTag } from '@standardnotes/snjs'
+
+export function getItemTitleInContextOfLinkBubble(item: DecryptedItemInterface) {
+ return item.title && item.title.length > 0 ? item.title : isNote(item) ? item.preview_plain : ''
+}
+
+function getItemSearchableString(item: DecryptedItemInterface, application: WebApplicationInterface) {
+ if (isNote(item)) {
+ return item.title.length > 0 ? item.title : item.preview_plain
+ } else if (isTag(item)) {
+ return application.items.getTagLongTitle(item)
+ }
+
+ return item.title ?? ''
+}
export function doesItemMatchSearchQuery(
item: DecryptedItemInterface,
searchQuery: string,
application: WebApplicationInterface,
) {
- const title = item instanceof SNTag ? application.items.getTagLongTitle(item) : item.title ?? ''
- const matchesQuery = title.toLowerCase().includes(searchQuery.toLowerCase())
+ const title = getItemSearchableString(item, application).toLowerCase()
+ const matchesQuery = title.includes(searchQuery.toLowerCase())
const isArchivedOrTrashed = item.archived || item.trashed
const isValidSearchResult = matchesQuery && !isArchivedOrTrashed