Skip to content

Extending blocknote #518

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open

Conversation

milaiwi
Copy link
Collaborator

@milaiwi milaiwi commented May 7, 2025

Added

  • Internal Linking (between files)
  • External Linking (across different medias)
  • Maintained current file path in the editor
  • Caching

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Summary

This PR significantly extends the BlockNote editor with internal/external linking capabilities and semantic file search functionality, along with improved file path management and caching mechanisms.

  • Security concern in electron/main/path/ipcHandlers.ts: The 'find-absolute-path' handler allows resolving arbitrary paths without validation, potentially enabling access outside intended directories
  • src/lib/utils/cache/fileSearchIndex.ts lacks error handling for race conditions in async operations and path manipulation
  • src/lib/semanticService.tsx needs improved error handling and content sanitization for semantic search
  • src/lib/utils/editor-state.ts has potential memory leaks due to uncleaned semantic cache entries
  • src/lib/blocknote/react/LinkToolbar/components/LinkToolbarContent.tsx contains an unbounded re-render loop with a 100ms interval

39 file(s) reviewed, 38 comment(s)
Edit PR Review Bot Settings | Greptile

Comment on lines +115 to 135
export function createFileRecursive(filePath: string, content: string, charset?: BufferEncoding): FileInfo | null {
const dirname = path.dirname(filePath)

if (!fs.existsSync(dirname)) {
fs.mkdirSync(dirname, { recursive: true })
}

if (fs.existsSync(filePath)) {
return
return null
}
const filePathWithExtension = addExtensionToFilenameIfNoExtensionPresent(filePath, markdownExtensions, '.md')

fs.writeFileSync(filePathWithExtension, content, charset)
return {
name: path.basename(filePathWithExtension),
path: filePathWithExtension,
relativePath: path.relative(dirname, filePathWithExtension),
dateModified: new Date(),
dateCreated: new Date(),
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: The function now returns a FileInfo object with hardcoded dates. Consider using the actual file stats (mtime and birthtime) from fs.statSync() after file creation for accuracy.

Suggested change
export function createFileRecursive(filePath: string, content: string, charset?: BufferEncoding): FileInfo | null {
const dirname = path.dirname(filePath)
if (!fs.existsSync(dirname)) {
fs.mkdirSync(dirname, { recursive: true })
}
if (fs.existsSync(filePath)) {
return
return null
}
const filePathWithExtension = addExtensionToFilenameIfNoExtensionPresent(filePath, markdownExtensions, '.md')
fs.writeFileSync(filePathWithExtension, content, charset)
return {
name: path.basename(filePathWithExtension),
path: filePathWithExtension,
relativePath: path.relative(dirname, filePathWithExtension),
dateModified: new Date(),
dateCreated: new Date(),
}
}
export function createFileRecursive(filePath: string, content: string, charset?: BufferEncoding): FileInfo | null {
const dirname = path.dirname(filePath)
if (!fs.existsSync(dirname)) {
fs.mkdirSync(dirname, { recursive: true })
}
if (fs.existsSync(filePath)) {
return null
}
const filePathWithExtension = addExtensionToFilenameIfNoExtensionPresent(filePath, markdownExtensions, '.md')
fs.writeFileSync(filePathWithExtension, content, charset)
const stats = fs.statSync(filePathWithExtension)
return {
name: path.basename(filePathWithExtension),
path: filePathWithExtension,
relativePath: path.relative(dirname, filePathWithExtension),
dateModified: stats.mtime,
dateCreated: stats.birthtime,
}
}

Comment on lines +223 to +232
ipcMain.handle('get-file-info', (event, absolutePath: string, parentRelativePath: string): FileInfo => {
const fileInfo = fs.statSync(absolutePath)
return {
name: path.basename(absolutePath),
path: absolutePath,
relativePath: parentRelativePath,
dateModified: fileInfo.mtime,
dateCreated: fileInfo.birthtime, // Add the birthtime property here
}
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: get-file-info doesn't handle errors from fs.statSync which could crash the app. Should use try/catch

Suggested change
ipcMain.handle('get-file-info', (event, absolutePath: string, parentRelativePath: string): FileInfo => {
const fileInfo = fs.statSync(absolutePath)
return {
name: path.basename(absolutePath),
path: absolutePath,
relativePath: parentRelativePath,
dateModified: fileInfo.mtime,
dateCreated: fileInfo.birthtime, // Add the birthtime property here
}
})
ipcMain.handle('get-file-info', (event, absolutePath: string, parentRelativePath: string): FileInfo => {
try {
const fileInfo = fs.statSync(absolutePath)
return {
name: path.basename(absolutePath),
path: absolutePath,
relativePath: parentRelativePath,
dateModified: fileInfo.mtime,
dateCreated: fileInfo.birthtime,
}
} catch (error) {
throw new Error(`Failed to get file info: ${error}`)
}
})

Comment on lines +205 to +226
export function findFileRecursive(dir: string, filename: string): string | null {
const files = fs.readdirSync(dir)
const basename = path.parse(filename).name

for (const file of files) {
const fullPath = path.resolve(dir, file)
const stat = fs.statSync(fullPath)

if (stat.isDirectory()) {
const result = findFileRecursive(fullPath, filename)
if (result) return result
} else {
// Check if files match
const fileName = path.parse(file).name
if (fileName === basename) {
return fullPath
}
}
}

return null
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: The recursive file search doesn't handle file extensions, which could lead to incorrect matches when multiple files have the same basename but different extensions.

Comment on lines +82 to +90
const hydrateIndex = async () => {
const files = await window.fileSystem.getFilesTreeForWindow()
const flat = flattenFileInfoTree(files).map((f: FileInfo) => ({
...f,
}))
useFileSearchIndex.getState().hydrate(flat)
}
hydrateIndex()
}, [])
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: No error handling for file tree fetching and hydration. Could crash if filesystem access fails

Suggested change
const hydrateIndex = async () => {
const files = await window.fileSystem.getFilesTreeForWindow()
const flat = flattenFileInfoTree(files).map((f: FileInfo) => ({
...f,
}))
useFileSearchIndex.getState().hydrate(flat)
}
hydrateIndex()
}, [])
const hydrateIndex = async () => {
try {
const files = await window.fileSystem.getFilesTreeForWindow()
const flat = flattenFileInfoTree(files).map((f: FileInfo) => ({
...f,
}))
useFileSearchIndex.getState().hydrate(flat)
} catch (error) {
toast.error('Failed to load file index: ' + error.message)
}
}
hydrateIndex()
}, [])

if (this.options.openOnClick) {
plugins.push(
clickHandler({
openFile: (this.options as any).openFile,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Unsafe type assertion with (this.options as any).openFile. Consider adding proper type definition.

Comment on lines +63 to +68
await window.fileSystem.deleteFile(file.path)
set((s) => {
const newMap = new Map(s.index)
newMap.delete(fileName)
return { index: newMap }
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: state update should be wrapped in try/catch in case deleteFile fails

Suggested change
await window.fileSystem.deleteFile(file.path)
set((s) => {
const newMap = new Map(s.index)
newMap.delete(fileName)
return { index: newMap }
})
try {
await window.fileSystem.deleteFile(file.path)
set((s) => {
const newMap = new Map(s.index)
newMap.delete(fileName)
return { index: newMap }
})
} catch (error) {
console.error('Failed to delete file:', error)
}

markStale: (filePath: string) => {
set((state) => {
const entry = state.semanticCache[filePath]
if (!entry) return {}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Returning empty object here will cause no state update, but the function signature implies state will be updated. Should return the original state instead.

Suggested change
if (!entry) return {}
if (!entry) return state

setFetching: (filePath: string, isFetching: boolean) => {
set((state) => {
const entry = state.semanticCache[filePath]
if (!entry) return {}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Same issue as markStale - returning empty object when no entry exists could cause state inconsistencies

Suggested change
if (!entry) return {}
if (!entry) return state

Comment on lines +83 to +89
shouldRefetch: (filePath: string, thresholdMs = 30000) => {
const entry = get().semanticCache[filePath]
if (!entry) return true
if (entry.isStale) return true
const age = Date.now() - entry.lastFetched
return age > thresholdMs
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Should check isFetching state to prevent multiple concurrent fetches of the same data

Comment on lines 1 to +4
export const HYPERMEDIA_SCHEME = 'hm'

export function isHypermediaScheme(url?: string) {
return !!url?.startsWith(`${HYPERMEDIA_SCHEME}://`)
return !!url?.startsWith(`${HYPERMEDIA_SCHEME}://`) || !!url?.startsWith(`reor://`)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Define 'reor://' as a constant like HYPERMEDIA_SCHEME to avoid magic strings and make updates easier to maintain

Suggested change
export const HYPERMEDIA_SCHEME = 'hm'
export function isHypermediaScheme(url?: string) {
return !!url?.startsWith(`${HYPERMEDIA_SCHEME}://`)
return !!url?.startsWith(`${HYPERMEDIA_SCHEME}://`) || !!url?.startsWith(`reor://`)
export const HYPERMEDIA_SCHEME = 'hm'
export const REOR_SCHEME = 'reor'
export function isHypermediaScheme(url?: string) {
return !!url?.startsWith(`${HYPERMEDIA_SCHEME}://`) || !!url?.startsWith(`${REOR_SCHEME}://`)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

When typing in [[, there is a black dot/white dot, what is its functionality?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[[ displays all similar files and then allows you to select one. After that, it creates an inline link towards that file. If there is no files being displayed, that means there is no similar file.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

here I am in the note "Untitled", but it also suggests itself as a similar file. Also, how does it define similar files because what's the relevance between "Untitled"(an empty note) and the welcoming note?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants