Skip to content
This repository was archived by the owner on Mar 7, 2026. It is now read-only.
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
114 changes: 114 additions & 0 deletions electron/main/common/chunking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,40 @@ export function chunkMarkdownByHeadings(markdownContent: string): string[] {
return chunks
}

export function chunkMarkdownByHeadingsWithPositions(markdownContent: string): Array<{ chunk: string; pos: number }> {
const lines = markdownContent.split('\n')
const chunks: Array<{ chunk: string; pos: number }> = []
let currentChunk: string[] = []
let currentPos = 0
let chunkStartPos = 0

lines.forEach((line) => {
if (line.startsWith('#')) {
if (currentChunk.length) {
chunks.push({
chunk: currentChunk.join('\n'),
pos: chunkStartPos,
})
currentChunk = []
chunkStartPos = currentPos
} else {
chunkStartPos = currentPos
}
}
currentChunk.push(line)
currentPos += line.length + 1
})

if (currentChunk.length) {
chunks.push({
chunk: currentChunk.join('\n'),
pos: chunkStartPos,
})
}

return chunks
}

export const chunkStringsRecursively = async (
strings: string[],
_chunkSize: number,
Expand All @@ -44,6 +78,55 @@ export const chunkStringsRecursively = async (
return mappedChunks
}

export const chunkStringsRecursivelyWithPositions = async (
stringInfos: Array<{ text: string; pos: number }>,
_chunkSize: number,
chunkOverlap: number,
): Promise<Array<{ chunk: string; pos: number }>> => {
const result: Array<{ chunk: string; pos: number }> = []

await Promise.all(
stringInfos.map(async (stringInfo) => {
const splitter = new RecursiveCharacterTextSplitter({
chunkSize: _chunkSize,
chunkOverlap,
})

const chunks = await splitter.createDocuments([stringInfo.text])

let currentOffset = 0

chunks.forEach((chunk) => {
const chunkText = chunk.pageContent

// Find where this chunk appears in the original text
// Start searching from the current offset to handle repeated content
const chunkPosition = stringInfo.text.indexOf(chunkText, currentOffset)

if (chunkPosition !== -1) {
const absolutePosition = stringInfo.pos + chunkPosition

currentOffset = chunkPosition + 1
Comment on lines +104 to +109

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

logic: The currentOffset = chunkPosition + 1 logic might skip content if there are overlapping chunks with identical text. Consider using currentOffset = chunkPosition + chunkText.length to ensure proper progression through the text.

Suggested change
const chunkPosition = stringInfo.text.indexOf(chunkText, currentOffset)
if (chunkPosition !== -1) {
const absolutePosition = stringInfo.pos + chunkPosition
currentOffset = chunkPosition + 1
const chunkPosition = stringInfo.text.indexOf(chunkText, currentOffset)
if (chunkPosition !== -1) {
const absolutePosition = stringInfo.pos + chunkPosition
currentOffset = chunkPosition + chunkText.length


result.push({
chunk: chunkText,
pos: absolutePosition,
})
} else {
// Fallback if we can't find the exact position (shouldn't happen)

result.push({
chunk: chunkText,
pos: stringInfo.pos,
})
}
})
}),
)

return result
}

export const chunkMarkdownByHeadingsAndByCharsIfBig = async (markdownContent: string): Promise<string[]> => {
const chunkOverlap = 20
const chunksByHeading = chunkMarkdownByHeadings(markdownContent)
Expand All @@ -62,3 +145,34 @@ export const chunkMarkdownByHeadingsAndByCharsIfBig = async (markdownContent: st

return chunksWithSmallChunksSplit.concat(chunkedRecursively)
}

export const chunkMarkdownByHeadingsAndByCharsIfBigWithPositions = async (
markdownContent: string,
): Promise<Array<{ chunk: string; pos: number }>> => {
const chunkOverlap = 20
const chunksByHeading = chunkMarkdownByHeadingsWithPositions(markdownContent)

const chunksWithBigChunksSplit: Array<{ text: string; pos: number }> = []
const chunksWithSmallChunksSplit: Array<{ chunk: string; pos: number }> = []

chunksByHeading.forEach((chunkInfo) => {
if (chunkInfo.chunk.length > chunkSize) {
chunksWithBigChunksSplit.push({
text: chunkInfo.chunk,
pos: chunkInfo.pos,
})
} else {
chunksWithSmallChunksSplit.push(chunkInfo)
}
})

const chunkedRecursively = await chunkStringsRecursivelyWithPositions(
chunksWithBigChunksSplit,
chunkSize,
chunkOverlap,
)

const result = chunksWithSmallChunksSplit.concat(chunkedRecursively)

return result
}
3 changes: 3 additions & 0 deletions electron/main/vector-database/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface DBEntry {
timeadded: Date
filemodified: Date
filecreated: Date
startPos?: number // Store position in the document
}
export interface DBQueryResult extends DBEntry {
_distance: number
Expand All @@ -22,6 +23,7 @@ export enum DatabaseFields {
TIME_ADDED = 'timeadded',
FILE_MODIFIED = 'filemodified',
FILE_CREATED = 'filecreated',
START_POS = 'startPos',
DISTANCE = '_distance',
}

Expand All @@ -34,6 +36,7 @@ const CreateDatabaseSchema = (vectorDim: number): Schema => {
new Field(DatabaseFields.TIME_ADDED, new ArrowDate(DateUnit.MILLISECOND), false),
new Field(DatabaseFields.FILE_MODIFIED, new ArrowDate(DateUnit.MILLISECOND), false),
new Field(DatabaseFields.FILE_CREATED, new ArrowDate(DateUnit.MILLISECOND), false),
new Field(DatabaseFields.START_POS, new Float64(), true),
]
const schema = new Schema(schemaFields)
return schema
Expand Down
20 changes: 13 additions & 7 deletions electron/main/vector-database/tableHelperFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as fs from 'fs'
import * as fsPromises from 'fs/promises'

import { BrowserWindow } from 'electron'
import { chunkMarkdownByHeadingsAndByCharsIfBig } from '../common/chunking'
import { chunkMarkdownByHeadingsAndByCharsIfBigWithPositions } from '../common/chunking'
import {
GetFilesInfoList,
flattenFileInfoTree,
Expand All @@ -18,15 +18,18 @@ import WindowsManager from '../common/windowManager'

const convertFileTypeToDBType = async (file: FileInfo): Promise<DBEntry[]> => {
const fileContent = readFile(file.path)
const chunks = await chunkMarkdownByHeadingsAndByCharsIfBig(fileContent)
const entries = chunks.map((content, index) => ({
const chunksWithPositions = await chunkMarkdownByHeadingsAndByCharsIfBigWithPositions(fileContent)

const entries = chunksWithPositions.map((chunkInfo, index) => ({
notepath: file.path,
content,
content: chunkInfo.chunk,
subnoteindex: index,
timeadded: new Date(),
filemodified: file.dateModified,
filecreated: file.dateCreated,
startPos: chunkInfo.pos,
}))

return entries
}

Expand Down Expand Up @@ -168,16 +171,19 @@ export const removeFileTreeFromDBTable = async (
export const updateFileInTable = async (dbTable: LanceDBTableWrapper, filePath: string): Promise<void> => {
await dbTable.deleteDBItemsByFilePaths([filePath])
const content = readFile(filePath)
const chunkedContentList = await chunkMarkdownByHeadingsAndByCharsIfBig(content)
const chunksWithPositions = await chunkMarkdownByHeadingsAndByCharsIfBigWithPositions(content)

const stats = fs.statSync(filePath)
const dbEntries = chunkedContentList.map((_content, index) => ({
const dbEntries = chunksWithPositions.map((chunkInfo, index) => ({
notepath: filePath,
content: _content,
content: chunkInfo.chunk,
subnoteindex: index,
timeadded: new Date(), // time now
filemodified: stats.mtime,
filecreated: stats.birthtime,
startPos: chunkInfo.pos,
}))

await dbTable.add(dbEntries)
}

Expand Down
10 changes: 6 additions & 4 deletions src/components/File/DBResultPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const formatModifiedDate = (date: Date) => {

interface DBResultPreviewProps {
dbResult: DBQueryResult
onSelect: (path: string) => void
onSelect: (path: string, position?: number) => void
}

export const DBResultPreview: React.FC<DBResultPreviewProps> = ({ dbResult: entry, onSelect }) => {
Expand All @@ -46,7 +46,7 @@ export const DBResultPreview: React.FC<DBResultPreviewProps> = ({ dbResult: entr
borderColor="$gray7"
paddingHorizontal="$2"
paddingVertical="$1"
onPress={() => onSelect(entry.notepath)}
onPress={() => onSelect(entry.notepath, entry.startPos)}
>
<Stack width="100%">
<Text fontSize="sm" color="$gray11">
Expand All @@ -65,7 +65,7 @@ export const DBResultPreview: React.FC<DBResultPreviewProps> = ({ dbResult: entr

interface DBSearchPreviewProps {
dbResult: DBQueryResult
onSelect: (path: string) => void
onSelect: (path: string, position?: number) => void
}

export const DBSearchPreview: React.FC<DBSearchPreviewProps> = ({ dbResult: entry, onSelect }) => {
Expand All @@ -88,7 +88,9 @@ export const DBSearchPreview: React.FC<DBSearchPreviewProps> = ({ dbResult: entr
hoverStyle={{
shadowRadius: '$4',
}}
onPress={() => onSelect(entry.notepath)}
onPress={() => {
onSelect(entry.notepath, entry.startPos)
}}
>
<YStack>
<Text fontSize="$2" color="$colorLight">
Expand Down
6 changes: 3 additions & 3 deletions src/components/Sidebars/SearchComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ interface SearchComponentProps {

export type SearchModeTypes = 'vector' | 'hybrid'

// Custom toggle component
const ToggleSwitch: React.FC<{
isHybrid: boolean
onChange: (searchMode: SearchModeTypes) => void
Expand Down Expand Up @@ -103,8 +102,9 @@ const SearchComponent: React.FC<SearchComponentProps> = ({
}, [searchParams.searchMode, searchParams.vectorWeight, debouncedSearch, searchQuery])

const openFileSelectSearch = useCallback(
(path: string) => {
openTabContent(path)
(path: string, position?: number) => {
openTabContent(path, undefined, false, position)

posthog.capture('open_file_from_search')
},
[openTabContent],
Expand Down
16 changes: 13 additions & 3 deletions src/contexts/ContentContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ import { getFilesInDirectory, getNextAvailableFileNameGivenBaseName } from '@/li
interface ContentContextType {
showEditor: boolean
setShowEditor: (showEditor: boolean) => void
openContent: (pathOrChatID: string, optionalContentToWriteOnCreate?: string, dontUpdateChatHistory?: boolean) => void
openContent: (
pathOrChatID: string,
optionalContentToWriteOnCreate?: string,
dontUpdateChatHistory?: boolean,
scrollToPosition?: number,
) => void
currentOpenFileOrChatID: string | null
createUntitledNote: (parentFileOrDirectory?: string) => void
}
Expand Down Expand Up @@ -40,14 +45,19 @@ export const ContentProvider: React.FC<ContentProviderProps> = ({ children }) =>
} = useFileContext()

const openContent = React.useCallback(
async (pathOrChatID: string, optionalContentToWriteOnCreate?: string, dontUpdateChatHistory?: boolean) => {
async (
pathOrChatID: string,
optionalContentToWriteOnCreate?: string,
dontUpdateChatHistory?: boolean,
scrollToPosition?: number,
) => {
if (!pathOrChatID) return
const chatMetadata = allChatsMetadata.find((chat) => chat.id === pathOrChatID)
if (chatMetadata) {
openNewChat(pathOrChatID)
} else {
setShowEditor(true)
openOrCreateFile(pathOrChatID, optionalContentToWriteOnCreate)
openOrCreateFile(pathOrChatID, optionalContentToWriteOnCreate, scrollToPosition)
}
setCurrentOpenFileOrChatID(pathOrChatID)
if (!dontUpdateChatHistory) {
Expand Down
Loading