mirror of
https://github.com/stablyai/orca.git
synced 2026-10-12 08:02:37 +00:00
fix: correct doc link resolution order, memoize render props, handle stale Monaco provider
- Check exact relative path before extensionless match so [[docs/guide.md]] resolves uniquely when docs/guide.mdx also exists - Memoize markdownDocuments, previewProps, and handleSave to avoid unnecessary re-renders of MarkdownPreview and MonacoEditor - Re-register Monaco completion provider when the instance changes and clear stale model entries Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -85,10 +85,7 @@ export function EditorContent({
|
||||
const openConflictReview = useAppStore((s) => s.openConflictReview)
|
||||
const closeFile = useAppStore((s) => s.closeFile)
|
||||
const setRightSidebarTab = useAppStore((s) => s.setRightSidebarTab)
|
||||
const markdownDocLinks = useMarkdownDocuments(activeFile, isMarkdown, mdViewMode)
|
||||
const handleMarkdownSave = (content: string): Promise<void> =>
|
||||
handleSave(content).then(() => markdownDocLinks.refreshMarkdownDocuments())
|
||||
|
||||
const md = useMarkdownDocuments(activeFile, isMarkdown, mdViewMode, handleSave)
|
||||
const activeConflictEntry =
|
||||
worktreeEntries.find((entry) => entry.path === activeFile.relativePath) ?? null
|
||||
|
||||
@@ -111,7 +108,7 @@ export function EditorContent({
|
||||
content={editBuffers[activeFile.id] ?? fc.content}
|
||||
language={resolvedLanguage}
|
||||
onContentChange={handleContentChange}
|
||||
onSave={isMarkdown ? handleMarkdownSave : handleSave}
|
||||
onSave={isMarkdown ? md.mdSave : handleSave}
|
||||
revealLine={
|
||||
pendingEditorReveal?.filePath === activeFile.filePath ? pendingEditorReveal.line : undefined
|
||||
}
|
||||
@@ -125,7 +122,7 @@ export function EditorContent({
|
||||
? pendingEditorReveal.matchLength
|
||||
: undefined
|
||||
}
|
||||
markdownDocuments={isMarkdown ? markdownDocLinks.markdownDocuments : undefined}
|
||||
markdownDocuments={isMarkdown ? md.markdownDocuments : undefined}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -174,8 +171,8 @@ export function EditorContent({
|
||||
: handleContentChange
|
||||
|
||||
const onSaveWithFm = fm
|
||||
? (body: string): Promise<void> => handleMarkdownSave(prependFrontMatter(fm.raw, body))
|
||||
: handleMarkdownSave
|
||||
? (body: string): Promise<void> => md.mdSave(prependFrontMatter(fm.raw, body))
|
||||
: md.mdSave
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-col">
|
||||
@@ -226,7 +223,7 @@ export function EditorContent({
|
||||
content={currentContent}
|
||||
filePath={activeFile.filePath}
|
||||
scrollCacheKey={`${editorViewStateKey}:preview`}
|
||||
{...markdownDocLinks.previewProps}
|
||||
{...md.previewProps}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -308,7 +305,7 @@ export function EditorContent({
|
||||
filePath={activeFile.filePath}
|
||||
scrollCacheKey={markdownPreviewViewStateKey}
|
||||
initialAnchor={activeFile.markdownPreviewAnchor ?? null}
|
||||
{...markdownDocLinks.previewProps}
|
||||
{...md.previewProps}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
@@ -410,7 +407,7 @@ export function EditorContent({
|
||||
content={modifiedDiffContent}
|
||||
filePath={activeFile.filePath}
|
||||
scrollCacheKey={`${diffViewStateKey}:preview`}
|
||||
{...markdownDocLinks.previewProps}
|
||||
{...md.previewProps}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -429,7 +426,7 @@ export function EditorContent({
|
||||
editable={isEditable}
|
||||
worktreeId={activeFile.worktreeId}
|
||||
onContentChange={isEditable ? handleContentChange : undefined}
|
||||
onSave={isEditable ? (isMarkdown ? handleMarkdownSave : handleSave) : undefined}
|
||||
onSave={isEditable ? (isMarkdown ? md.mdSave : handleSave) : undefined}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -98,6 +98,33 @@ describe('resolveMarkdownDocLink', () => {
|
||||
expect(resolveMarkdownDocLink('docs/setup-guide', index).status).toBe('resolved')
|
||||
})
|
||||
|
||||
it('prefers exact relative path over ambiguous extensionless match', () => {
|
||||
const index = createMarkdownDocumentIndex([
|
||||
{
|
||||
filePath: '/repo/docs/guide.md',
|
||||
relativePath: 'docs/guide.md',
|
||||
basename: 'guide.md',
|
||||
name: 'guide'
|
||||
},
|
||||
{
|
||||
filePath: '/repo/docs/guide.mdx',
|
||||
relativePath: 'docs/guide.mdx',
|
||||
basename: 'guide.mdx',
|
||||
name: 'guide'
|
||||
}
|
||||
])
|
||||
|
||||
expect(resolveMarkdownDocLink('docs/guide.md', index)).toMatchObject({
|
||||
status: 'resolved',
|
||||
document: { relativePath: 'docs/guide.md' }
|
||||
})
|
||||
expect(resolveMarkdownDocLink('docs/guide.mdx', index)).toMatchObject({
|
||||
status: 'resolved',
|
||||
document: { relativePath: 'docs/guide.mdx' }
|
||||
})
|
||||
expect(resolveMarkdownDocLink('docs/guide', index).status).toBe('ambiguous')
|
||||
})
|
||||
|
||||
it('normalizes Windows-style targets', () => {
|
||||
const result = resolveMarkdownDocLink(
|
||||
'docs\\setup-guide.md',
|
||||
|
||||
@@ -100,6 +100,14 @@ export function resolveMarkdownDocLink(
|
||||
const normalizedTarget = normalizeDocLinkKey(target)
|
||||
const extensionlessTarget = stripMarkdownExtension(normalizedTarget)
|
||||
|
||||
// Why: exact relative path must be checked before the extensionless lookup
|
||||
// so that [[docs/guide.md]] resolves uniquely even when docs/guide.mdx also
|
||||
// exists (both share the extensionless key "docs/guide").
|
||||
const relativeWithExtension = resolveMatches(index.byRelativePath.get(normalizedTarget))
|
||||
if (relativeWithExtension) {
|
||||
return relativeWithExtension
|
||||
}
|
||||
|
||||
const relativeWithoutExtension = resolveMatches(
|
||||
index.byRelativePathWithoutExtension.get(extensionlessTarget)
|
||||
)
|
||||
@@ -107,11 +115,6 @@ export function resolveMarkdownDocLink(
|
||||
return relativeWithoutExtension
|
||||
}
|
||||
|
||||
const relativeWithExtension = resolveMatches(index.byRelativePath.get(normalizedTarget))
|
||||
if (relativeWithExtension) {
|
||||
return relativeWithExtension
|
||||
}
|
||||
|
||||
if (!normalizedTarget.includes('/')) {
|
||||
const byName = resolveMatches(index.byName.get(extensionlessTarget))
|
||||
if (byName) {
|
||||
|
||||
@@ -9,12 +9,21 @@ import {
|
||||
type MonacoApi = Parameters<OnMount>[1]
|
||||
|
||||
let provider: IDisposable | null = null
|
||||
let providerMonaco: MonacoApi | null = null
|
||||
const documentsByModel = new Map<string, MarkdownDocument[]>()
|
||||
|
||||
export function ensureMarkdownDocCompletionProvider(monaco: MonacoApi): void {
|
||||
if (provider) {
|
||||
// Why: if Monaco was torn down and re-created (e.g. window reload), the old
|
||||
// provider reference is stale. Detect this by checking whether the Monaco
|
||||
// instance changed and re-register.
|
||||
if (provider && providerMonaco === monaco) {
|
||||
return
|
||||
}
|
||||
if (provider) {
|
||||
provider.dispose()
|
||||
documentsByModel.clear()
|
||||
}
|
||||
providerMonaco = monaco
|
||||
|
||||
provider = monaco.languages.registerCompletionItemProvider('markdown', {
|
||||
triggerCharacters: ['['],
|
||||
|
||||
@@ -11,13 +11,14 @@ type UseMarkdownDocumentsResult = {
|
||||
markdownDocuments: MarkdownDocument[]
|
||||
onOpenDocument: (document: MarkdownDocument) => Promise<void>
|
||||
}
|
||||
refreshMarkdownDocuments: () => Promise<void>
|
||||
mdSave: (content: string) => Promise<void>
|
||||
}
|
||||
|
||||
export function useMarkdownDocuments(
|
||||
activeFile: OpenFile,
|
||||
isMarkdown: boolean,
|
||||
viewMode: MarkdownViewMode
|
||||
viewMode: MarkdownViewMode,
|
||||
onSave: (content: string) => Promise<void>
|
||||
): UseMarkdownDocumentsResult {
|
||||
const worktreeId = activeFile.worktreeId
|
||||
const worktreesByRepo = useAppStore((s) => s.worktreesByRepo)
|
||||
@@ -103,11 +104,20 @@ export function useMarkdownDocuments(
|
||||
void refreshMarkdownDocuments()
|
||||
}, [activeFile.id, isMarkdown, viewMode, refreshMarkdownDocuments])
|
||||
|
||||
const markdownDocuments = worktreeId ? (markdownDocumentsByWorktree[worktreeId] ?? []) : []
|
||||
const markdownDocuments = useMemo(
|
||||
() => (worktreeId ? (markdownDocumentsByWorktree[worktreeId] ?? []) : []),
|
||||
[worktreeId, markdownDocumentsByWorktree]
|
||||
)
|
||||
|
||||
return {
|
||||
markdownDocuments,
|
||||
previewProps: { markdownDocuments, onOpenDocument: openMarkdownDocument },
|
||||
refreshMarkdownDocuments
|
||||
}
|
||||
const previewProps = useMemo(
|
||||
() => ({ markdownDocuments, onOpenDocument: openMarkdownDocument }),
|
||||
[markdownDocuments, openMarkdownDocument]
|
||||
)
|
||||
|
||||
const mdSave = useCallback(
|
||||
(content: string) => onSave(content).then(() => refreshMarkdownDocuments()),
|
||||
[onSave, refreshMarkdownDocuments]
|
||||
)
|
||||
|
||||
return { markdownDocuments, previewProps, mdSave }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user