mirror of
https://github.com/logancyang/obsidian-copilot.git
synced 2026-07-22 07:50:24 +00:00
First wave of eliminating global `app` references (per Obsidian's
`obsidianmd/no-restricted-globals` rule). Adds a `useApp()` hook that
reads from `AppContext` and throws if no provider is in scope, then
converts the React layer to use it. Modals that render React content
now wrap their tree with `<AppContext.Provider value={this.app}>` so
descendants pick up the modal-owned app rather than the global one,
which matters in popout windows.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
297 lines
9.6 KiB
TypeScript
297 lines
9.6 KiB
TypeScript
import { TruncatedText } from "@/components/TruncatedText";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu";
|
|
import {
|
|
categorizePatterns,
|
|
createPatternSettingsValue,
|
|
getDecodedPatterns,
|
|
getExtensionPattern,
|
|
getFilePattern,
|
|
getTagPattern,
|
|
} from "@/search/searchUtils";
|
|
import { File, FileText, Folder, Tag, Wrench, X } from "lucide-react";
|
|
import { App, Modal, TFile } from "obsidian";
|
|
import React, { useState } from "react";
|
|
import { createRoot, Root } from "react-dom/client";
|
|
import { AppContext, useApp } from "@/context";
|
|
import { CustomPatternInputModal } from "./CustomPatternInputModal";
|
|
import { ExtensionInputModal } from "./ExtensionInputModal";
|
|
import { FolderSearchModal } from "./FolderSearchModal";
|
|
import { ProjectFileSelectModal } from "./ProjectFileSelectModal";
|
|
import { TagSearchModal } from "./TagSearchModal";
|
|
|
|
function PatternListGroup({
|
|
title,
|
|
patterns,
|
|
onRemove,
|
|
}: {
|
|
title: string;
|
|
patterns: string[];
|
|
onRemove: (pattern: string) => void;
|
|
}) {
|
|
return (
|
|
<div className="tw-grid tw-grid-cols-4 tw-gap-2">
|
|
<div className="tw-font-bold">{title}</div>
|
|
<ul className="tw-col-span-3 tw-m-0 tw-flex tw-list-inside tw-list-disc tw-flex-col tw-gap-1 tw-pl-0">
|
|
{patterns.map((pattern) => (
|
|
<li
|
|
key={pattern}
|
|
className="tw-flex tw-gap-2 tw-rounded-md tw-pl-2 tw-pr-1 hover:tw-bg-modifier-hover"
|
|
>
|
|
<TruncatedText className="tw-flex-1">{pattern}</TruncatedText>
|
|
<Button variant="ghost2" size="fit" onClick={() => onRemove(pattern)}>
|
|
<X className="tw-size-4" />
|
|
</Button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ProjectPatternMatchingModalContent({
|
|
value: initialValue,
|
|
onUpdate,
|
|
container,
|
|
}: {
|
|
value: string;
|
|
onUpdate: (value: string) => void;
|
|
container: HTMLElement;
|
|
}) {
|
|
const app = useApp();
|
|
const [value, setValue] = useState(initialValue);
|
|
const patterns = getDecodedPatterns(value);
|
|
const { tagPatterns, extensionPatterns, folderPatterns, notePatterns } =
|
|
categorizePatterns(patterns);
|
|
|
|
const updateCategories = (newCategories: {
|
|
tagPatterns?: string[];
|
|
extensionPatterns?: string[];
|
|
folderPatterns?: string[];
|
|
notePatterns?: string[];
|
|
}) => {
|
|
const newValue = createPatternSettingsValue({
|
|
tagPatterns: newCategories.tagPatterns ?? tagPatterns,
|
|
extensionPatterns: newCategories.extensionPatterns ?? extensionPatterns,
|
|
folderPatterns: newCategories.folderPatterns ?? folderPatterns,
|
|
notePatterns: newCategories.notePatterns ?? notePatterns,
|
|
});
|
|
setValue(newValue);
|
|
onUpdate(newValue);
|
|
};
|
|
|
|
const hasValue =
|
|
tagPatterns.length > 0 ||
|
|
extensionPatterns.length > 0 ||
|
|
folderPatterns.length > 0 ||
|
|
notePatterns.length > 0;
|
|
|
|
return (
|
|
<div className="tw-mt-2 tw-flex tw-flex-col tw-gap-4">
|
|
<div className="tw-flex tw-max-h-[400px] tw-flex-col tw-gap-2 tw-overflow-y-auto tw-rounded-md tw-border tw-border-solid tw-border-border tw-p-4">
|
|
{!hasValue && <div className="tw-text-center tw-text-sm">No patterns specified</div>}
|
|
{tagPatterns.length > 0 && (
|
|
<PatternListGroup
|
|
title="Tags"
|
|
patterns={tagPatterns}
|
|
onRemove={(pattern) => {
|
|
const newPatterns = tagPatterns.filter((p) => p !== pattern);
|
|
updateCategories({
|
|
tagPatterns: newPatterns,
|
|
});
|
|
}}
|
|
/>
|
|
)}
|
|
{extensionPatterns.length > 0 && (
|
|
<PatternListGroup
|
|
title="Extensions"
|
|
patterns={extensionPatterns}
|
|
onRemove={(pattern) => {
|
|
const newPatterns = extensionPatterns.filter((p) => p !== pattern);
|
|
updateCategories({
|
|
extensionPatterns: newPatterns,
|
|
});
|
|
}}
|
|
/>
|
|
)}
|
|
{folderPatterns.length > 0 && (
|
|
<PatternListGroup
|
|
title="Folders"
|
|
patterns={folderPatterns}
|
|
onRemove={(pattern) => {
|
|
const newPatterns = folderPatterns.filter((p) => p !== pattern);
|
|
updateCategories({
|
|
folderPatterns: newPatterns,
|
|
});
|
|
}}
|
|
/>
|
|
)}
|
|
{notePatterns.length > 0 && (
|
|
<PatternListGroup
|
|
title="Files"
|
|
patterns={notePatterns}
|
|
onRemove={(pattern) => {
|
|
const newPatterns = notePatterns.filter((p) => p !== pattern);
|
|
updateCategories({
|
|
notePatterns: newPatterns,
|
|
});
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
<div className="tw-flex tw-justify-end tw-gap-2">
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="secondary">Add...</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="start" container={container}>
|
|
<DropdownMenuItem
|
|
onSelect={() => {
|
|
new TagSearchModal(app, (tag) => {
|
|
const tagPattern = getTagPattern(tag);
|
|
if (tagPatterns.includes(tagPattern)) {
|
|
return;
|
|
}
|
|
updateCategories({
|
|
tagPatterns: [...tagPatterns, tagPattern],
|
|
});
|
|
}).open();
|
|
}}
|
|
>
|
|
<div className="tw-flex tw-items-center tw-gap-2">
|
|
<Tag className="tw-size-4" />
|
|
Tag
|
|
</div>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
onSelect={() => {
|
|
new FolderSearchModal(app, (folder) => {
|
|
if (folderPatterns.includes(folder)) {
|
|
return;
|
|
}
|
|
updateCategories({
|
|
folderPatterns: [...folderPatterns, folder],
|
|
});
|
|
}).open();
|
|
}}
|
|
>
|
|
<div className="tw-flex tw-items-center tw-gap-2">
|
|
<Folder className="tw-size-4" />
|
|
Folder
|
|
</div>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
onSelect={() => {
|
|
new ProjectFileSelectModal({
|
|
app,
|
|
onFileSelect: (file: TFile) => {
|
|
const filePattern = getFilePattern(file);
|
|
if (notePatterns.includes(filePattern)) {
|
|
return;
|
|
}
|
|
updateCategories({
|
|
notePatterns: [...notePatterns, filePattern],
|
|
});
|
|
},
|
|
excludeFilePaths: [],
|
|
titleOnly: true,
|
|
}).open();
|
|
}}
|
|
>
|
|
<div className="tw-flex tw-items-center tw-gap-2">
|
|
<FileText className="tw-size-4" />
|
|
Files
|
|
</div>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
onSelect={() => {
|
|
new ExtensionInputModal(app, (extension) => {
|
|
const extensionPattern = getExtensionPattern(extension);
|
|
if (extensionPatterns.includes(extensionPattern)) {
|
|
return;
|
|
}
|
|
updateCategories({
|
|
extensionPatterns: [...extensionPatterns, extensionPattern],
|
|
});
|
|
}).open();
|
|
}}
|
|
>
|
|
<div className="tw-flex tw-items-center tw-gap-2">
|
|
<File className="tw-size-4" />
|
|
Extension
|
|
</div>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
onSelect={() => {
|
|
new CustomPatternInputModal(app, (value) => {
|
|
const patterns = getDecodedPatterns(value);
|
|
const {
|
|
tagPatterns: newTagPatterns,
|
|
extensionPatterns: newExtensionPatterns,
|
|
folderPatterns: newFolderPatterns,
|
|
notePatterns: newNotePatterns,
|
|
} = categorizePatterns(patterns);
|
|
updateCategories({
|
|
tagPatterns: [...tagPatterns, ...newTagPatterns],
|
|
extensionPatterns: [...extensionPatterns, ...newExtensionPatterns],
|
|
folderPatterns: [...folderPatterns, ...newFolderPatterns],
|
|
notePatterns: [...notePatterns, ...newNotePatterns],
|
|
});
|
|
}).open();
|
|
}}
|
|
>
|
|
<div className="tw-flex tw-items-center tw-gap-2">
|
|
<Wrench className="tw-size-4" />
|
|
Custom
|
|
</div>
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export class ProjectPatternMatchingModal extends Modal {
|
|
private root: Root;
|
|
|
|
constructor(
|
|
app: App,
|
|
private onUpdate: (value: string) => void,
|
|
/** The raw pattern matching value, separated by commas */
|
|
private value: string,
|
|
title: string
|
|
) {
|
|
super(app);
|
|
// @ts-ignore
|
|
this.setTitle(title);
|
|
}
|
|
|
|
onOpen() {
|
|
const { contentEl } = this;
|
|
this.root = createRoot(contentEl);
|
|
|
|
const handleUpdate = (value: string) => {
|
|
this.onUpdate(value);
|
|
};
|
|
|
|
this.root.render(
|
|
<AppContext.Provider value={this.app}>
|
|
<ProjectPatternMatchingModalContent
|
|
value={this.value}
|
|
onUpdate={handleUpdate}
|
|
container={this.contentEl}
|
|
/>
|
|
</AppContext.Provider>
|
|
);
|
|
}
|
|
|
|
onClose() {
|
|
this.root.unmount();
|
|
}
|
|
}
|