mirror of
https://github.com/logancyang/obsidian-copilot.git
synced 2026-07-22 07:50:24 +00:00
Pass multiple notes to prompt by setting folder as context using Copilot command (#265)
This commit is contained in:
parent
d6a28ec3ac
commit
65f62513e0
18 changed files with 845 additions and 513 deletions
31
.github/workflows/node.js.yml
vendored
Normal file
31
.github/workflows/node.js.yml
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
|
||||
|
||||
name: Node.js CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20.x]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
- run: npm ci
|
||||
- run: npm run build --if-present
|
||||
- run: npm test
|
||||
16
__mocks__/obsidian.js
Normal file
16
__mocks__/obsidian.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// __mocks__/obsidian.js
|
||||
module.exports = {
|
||||
Vault: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
getMarkdownFiles: jest.fn().mockImplementation(() => {
|
||||
// Return an array of mock markdown file objects
|
||||
return Promise.resolve([
|
||||
{ path: 'test/test2/note1.md' },
|
||||
{ path: 'test/note2.md' },
|
||||
{ path: 'test2/note3.md' },
|
||||
{ path: 'note4.md' },
|
||||
]);
|
||||
}),
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
|
@ -8,8 +8,10 @@ module.exports = {
|
|||
moduleNameMapper: {
|
||||
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
|
||||
'^@/(.*)$': '<rootDir>/src/$1',
|
||||
'^obsidian$': '<rootDir>/__mocks__/obsidian.js'
|
||||
},
|
||||
testRegex: '(/tests/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$',
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
||||
testPathIgnorePatterns: ['/node_modules/'],
|
||||
};
|
||||
setupFiles: ['<rootDir>/jest.setup.js'],
|
||||
};
|
||||
1
jest.setup.js
Normal file
1
jest.setup.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import 'web-streams-polyfill/dist/polyfill.min.js';
|
||||
975
package-lock.json
generated
975
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -17,7 +17,7 @@
|
|||
"@testing-library/react": "^14.0.0",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/events": "^3.0.0",
|
||||
"@types/jest": "^29.5.0",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/koa": "^2.13.7",
|
||||
"@types/koa__cors": "^4.0.0",
|
||||
"@types/node": "^16.11.6",
|
||||
|
|
@ -28,12 +28,13 @@
|
|||
"@typescript-eslint/parser": "5.29.0",
|
||||
"builtin-modules": "3.3.0",
|
||||
"esbuild": "0.17.3",
|
||||
"jest": "^29.5.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.5.0",
|
||||
"obsidian": "latest",
|
||||
"ts-jest": "^29.1.0",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "4.7.4"
|
||||
"typescript": "4.7.4",
|
||||
"web-streams-polyfill": "^3.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@huggingface/inference": "^2.6.4",
|
||||
|
|
|
|||
|
|
@ -13,21 +13,21 @@ export class AddPromptModal extends Modal {
|
|||
|
||||
this.contentEl.createEl('h2', { text: 'User Custom Prompt' });
|
||||
|
||||
const formContainer = this.contentEl.createEl('div', { cls: 'custom-prompt-modal' });
|
||||
const formContainer = this.contentEl.createEl('div', { cls: 'copilot-command-modal' });
|
||||
|
||||
const titleContainer = formContainer.createEl(
|
||||
'div',
|
||||
{ cls: 'custom-prompt-input-container' }
|
||||
{ cls: 'copilot-command-input-container' }
|
||||
);
|
||||
|
||||
titleContainer.createEl(
|
||||
'h3', { text: 'Title', cls: 'custom-prompt-header' }
|
||||
'h3', { text: 'Title', cls: 'copilot-command-header' }
|
||||
);
|
||||
titleContainer.createEl(
|
||||
'p',
|
||||
{
|
||||
text: 'The title of the prompt, must be unique.',
|
||||
cls: 'custom-prompt-description',
|
||||
cls: 'copilot-command-input-description',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -41,17 +41,17 @@ export class AddPromptModal extends Modal {
|
|||
|
||||
const promptContainer = formContainer.createEl(
|
||||
'div',
|
||||
{ cls: 'custom-prompt-input-container' }
|
||||
{ cls: 'copilot-command-input-container' }
|
||||
);
|
||||
|
||||
promptContainer.createEl(
|
||||
'h3', { text: 'Prompt', cls: 'custom-prompt-header' }
|
||||
'h3', { text: 'Prompt', cls: 'copilot-command-header' }
|
||||
);
|
||||
promptContainer.createEl(
|
||||
'p',
|
||||
{
|
||||
text: 'The content of the prompt. Use "{}" to represent the selected text. For example, "Improve the readability of the following text: {}"',
|
||||
cls: 'custom-prompt-description',
|
||||
cls: 'copilot-command-input-description',
|
||||
}
|
||||
);
|
||||
const promptField = promptContainer.createEl('textarea');
|
||||
|
|
@ -75,18 +75,18 @@ export class AddPromptModal extends Modal {
|
|||
});
|
||||
|
||||
const descContainer = promptContainer.createEl('p', {
|
||||
cls: 'custom-prompt-description',
|
||||
cls: 'copilot-command-input-description',
|
||||
});
|
||||
|
||||
descContainer.appendChild(descFragment);
|
||||
|
||||
const saveButtonContainer = formContainer.createEl(
|
||||
'div',
|
||||
{ cls: 'custom-prompt-save-btn-container' }
|
||||
{ cls: 'copilot-command-save-btn-container' }
|
||||
);
|
||||
const saveButton = saveButtonContainer.createEl(
|
||||
'button',
|
||||
{ text: 'Save', cls: 'custom-prompt-save-btn' }
|
||||
{ text: 'Save', cls: 'copilot-command-save-btn' }
|
||||
);
|
||||
saveButton.addEventListener('click', () => {
|
||||
if (titleField.value && promptField.value) {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import ChatMessages from '@/components/ChatComponents/ChatMessages';
|
|||
import { AI_SENDER, USER_SENDER } from '@/constants';
|
||||
import { AppContext } from '@/context';
|
||||
import { getAIResponse } from '@/langchainStream';
|
||||
import { CopilotSettings } from '@/settings/SettingsPage';
|
||||
import SharedState, {
|
||||
ChatMessage, useSharedState,
|
||||
} from '@/sharedState';
|
||||
|
|
@ -20,6 +21,8 @@ import {
|
|||
formatDateTime,
|
||||
getFileContent,
|
||||
getFileName,
|
||||
getNotesFromPath,
|
||||
getSendChatContextNotesPrompt,
|
||||
glossaryPrompt,
|
||||
removeUrlsFromSelectionPrompt,
|
||||
rewriteLongerSelectionPrompt,
|
||||
|
|
@ -27,14 +30,14 @@ import {
|
|||
rewriteShorterSelectionPrompt,
|
||||
rewriteTweetSelectionPrompt,
|
||||
rewriteTweetThreadSelectionPrompt,
|
||||
sendNoteContentPrompt,
|
||||
sendNotesContentPrompt,
|
||||
simplifyPrompt,
|
||||
summarizePrompt,
|
||||
tocPrompt
|
||||
tocPrompt,
|
||||
} from '@/utils';
|
||||
import VectorDBManager from '@/vectorDBManager';
|
||||
import { EventEmitter } from 'events';
|
||||
import { Notice, TFile } from 'obsidian';
|
||||
import { Notice, TFile, Vault } from 'obsidian';
|
||||
import React, {
|
||||
useContext,
|
||||
useEffect,
|
||||
|
|
@ -49,15 +52,24 @@ interface CreateEffectOptions {
|
|||
|
||||
interface ChatProps {
|
||||
sharedState: SharedState;
|
||||
settings: CopilotSettings;
|
||||
chainManager: ChainManager;
|
||||
emitter: EventEmitter;
|
||||
getChatVisibility: () => Promise<boolean>;
|
||||
defaultSaveFolder: string;
|
||||
vault: Vault;
|
||||
debug: boolean;
|
||||
}
|
||||
|
||||
const Chat: React.FC<ChatProps> = ({
|
||||
sharedState, chainManager, emitter, getChatVisibility, defaultSaveFolder, debug
|
||||
sharedState,
|
||||
settings,
|
||||
chainManager,
|
||||
emitter,
|
||||
getChatVisibility,
|
||||
defaultSaveFolder,
|
||||
vault,
|
||||
debug
|
||||
}) => {
|
||||
const [
|
||||
chatHistory, addMessage, clearMessages,
|
||||
|
|
@ -138,30 +150,45 @@ const Chat: React.FC<ChatProps> = ({
|
|||
return;
|
||||
}
|
||||
|
||||
const file = app.workspace.getActiveFile();
|
||||
if (!file) {
|
||||
new Notice('No active note found.');
|
||||
console.error('No active note found.');
|
||||
return;
|
||||
let noteFiles: TFile[] = [];
|
||||
if (debug) {
|
||||
console.log('Chat note context path:', settings.chatNoteContextPath);
|
||||
}
|
||||
if (settings.chatNoteContextPath) {
|
||||
// Recursively get all note TFiles in the path
|
||||
noteFiles = await getNotesFromPath(vault, settings.chatNoteContextPath);
|
||||
}
|
||||
|
||||
const noteContent = await getFileContent(file);
|
||||
const noteName = getFileName(file);
|
||||
if (!noteContent) {
|
||||
new Notice('No note content found.');
|
||||
console.error('No note content found.');
|
||||
return;
|
||||
const file = app.workspace.getActiveFile();
|
||||
// If no note context provided, default to the active note
|
||||
if (noteFiles.length === 0) {
|
||||
if (!file) {
|
||||
new Notice('No active note found.');
|
||||
console.error('No active note found.');
|
||||
return;
|
||||
}
|
||||
new Notice('No valid Chat context provided. Defaulting to the active note.');
|
||||
noteFiles = [file];
|
||||
}
|
||||
|
||||
const notes = [];
|
||||
for (const file of noteFiles) {
|
||||
const content = await getFileContent(file);
|
||||
if (content) {
|
||||
notes.push({ name: getFileName(file), content });
|
||||
}
|
||||
}
|
||||
|
||||
// Send the content of the note to AI
|
||||
const promptMessageHidden: ChatMessage = {
|
||||
message: sendNoteContentPrompt(noteName, noteContent),
|
||||
message: sendNotesContentPrompt(notes),
|
||||
sender: USER_SENDER,
|
||||
isVisible: false,
|
||||
};
|
||||
|
||||
// Visible user message that is not sent to AI
|
||||
const sendNoteContentUserMessage = `Please read this note [[${noteName}]] and be ready to answer questions about it.`;
|
||||
// const sendNoteContentUserMessage = `Please read the following notes [[${activeNoteContent}]] and be ready to answer questions about it.`;
|
||||
const sendNoteContentUserMessage = getSendChatContextNotesPrompt(notes);
|
||||
const promptMessageVisible: ChatMessage = {
|
||||
message: sendNoteContentUserMessage,
|
||||
sender: USER_SENDER,
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ const ChatIcons: React.FC<ChatIconsProps> = ({
|
|||
{selectedChain === 'llm_chain' && (
|
||||
<button className='chat-icon-button' onClick={onSendActiveNoteToPrompt}>
|
||||
<SendActiveNoteToPromptIcon className='icon-scaler' />
|
||||
<span className="tooltip-text">Send Active Note to Prompt<br/>(use with long context models)</span>
|
||||
<span className="tooltip-text">Send Note(s) to Prompt<br/>(Set with Copilot command.<br/>Default is active note)</span>
|
||||
</button>
|
||||
)}
|
||||
{selectedChain === 'retrieval_qa' && (
|
||||
|
|
|
|||
57
src/components/ChatNoteContextModal.tsx
Normal file
57
src/components/ChatNoteContextModal.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { CopilotSettings } from "@/settings/SettingsPage";
|
||||
import { App, Modal } from "obsidian";
|
||||
|
||||
export class ChatNoteContextModal extends Modal {
|
||||
private settings: CopilotSettings;
|
||||
private onSubmit: (path: string) => void;
|
||||
|
||||
constructor(app: App, settings: CopilotSettings, onSubmit: (path: string) => void) {
|
||||
super(app);
|
||||
this.settings = settings;
|
||||
this.onSubmit = onSubmit;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const formContainer = this.contentEl.createEl('div', { cls: 'copilot-command-modal' });
|
||||
const pathContainer = formContainer.createEl('div', { cls: 'copilot-command-input-container' });
|
||||
|
||||
pathContainer.createEl('h3', { text: 'Folder Path', cls: 'copilot-command-header' });
|
||||
const descFragment = createFragment((frag) => {
|
||||
frag.appendText('All notes under the path will be sent to the prompt when the ');
|
||||
frag.createEl(
|
||||
'strong',
|
||||
{ text: 'Send Note(s) to Prompt' }
|
||||
);
|
||||
frag.appendText(' button is clicked in Chat mode. ');
|
||||
frag.appendText('If none provided, ');
|
||||
frag.createEl(
|
||||
'strong',
|
||||
{ text: 'default context is the active note' }
|
||||
);
|
||||
});
|
||||
pathContainer.appendChild(descFragment);
|
||||
|
||||
const pathField = pathContainer.createEl(
|
||||
'input',
|
||||
{
|
||||
type: 'text',
|
||||
cls: 'copilot-command-input',
|
||||
value: this.settings.chatNoteContextPath,
|
||||
}
|
||||
);
|
||||
pathField.setAttribute('name', 'folderPath');
|
||||
|
||||
const submitButtonContainer = formContainer.createEl('div', { cls: 'copilot-command-save-btn-container' });
|
||||
const submitButton = submitButtonContainer.createEl('button', { text: 'Submit', cls: 'copilot-command-save-btn' });
|
||||
|
||||
submitButton.addEventListener('click', () => {
|
||||
// Remove the leading slash if it exists
|
||||
let pathValue = pathField.value;
|
||||
if (pathValue.startsWith('/') && pathValue.length > 1) {
|
||||
pathValue = pathValue.slice(1);
|
||||
}
|
||||
this.onSubmit(pathValue);
|
||||
this.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -3,9 +3,10 @@ import Chat from '@/components/Chat';
|
|||
import { CHAT_VIEWTYPE } from '@/constants';
|
||||
import { AppContext } from '@/context';
|
||||
import CopilotPlugin from '@/main';
|
||||
import { CopilotSettings } from '@/settings/SettingsPage';
|
||||
import SharedState from '@/sharedState';
|
||||
import { EventEmitter } from 'events';
|
||||
import { ItemView, WorkspaceLeaf } from 'obsidian';
|
||||
import { ItemView, Vault, WorkspaceLeaf } from 'obsidian';
|
||||
import * as React from 'react';
|
||||
import { Root, createRoot } from 'react-dom/client';
|
||||
|
||||
|
|
@ -14,6 +15,8 @@ export default class CopilotView extends ItemView {
|
|||
private sharedState: SharedState;
|
||||
private chainManager: ChainManager;
|
||||
private root: Root | null = null;
|
||||
private vault: Vault;
|
||||
private settings: CopilotSettings;
|
||||
private defaultSaveFolder: string;
|
||||
private debug = false;
|
||||
emitter: EventEmitter;
|
||||
|
|
@ -22,12 +25,14 @@ export default class CopilotView extends ItemView {
|
|||
constructor(leaf: WorkspaceLeaf, private plugin: CopilotPlugin) {
|
||||
super(leaf);
|
||||
this.sharedState = plugin.sharedState;
|
||||
this.settings = plugin.settings;
|
||||
this.app = plugin.app;
|
||||
this.chainManager = plugin.chainManager;
|
||||
this.debug = plugin.settings.debug;
|
||||
this.emitter = new EventEmitter();
|
||||
this.getChatVisibility = this.getChatVisibility.bind(this);
|
||||
this.userSystemPrompt = plugin.settings.userSystemPrompt;
|
||||
this.vault = plugin.app.vault;
|
||||
this.defaultSaveFolder = plugin.settings.defaultSaveFolder;
|
||||
}
|
||||
|
||||
|
|
@ -63,10 +68,12 @@ export default class CopilotView extends ItemView {
|
|||
<React.StrictMode>
|
||||
<Chat
|
||||
sharedState={this.sharedState}
|
||||
settings={this.settings}
|
||||
chainManager={this.chainManager}
|
||||
emitter={this.emitter}
|
||||
getChatVisibility={this.getChatVisibility}
|
||||
defaultSaveFolder={this.defaultSaveFolder}
|
||||
vault={this.vault}
|
||||
debug={this.debug}
|
||||
/>
|
||||
</React.StrictMode>
|
||||
|
|
|
|||
|
|
@ -149,5 +149,6 @@ export const DEFAULT_SETTINGS: CopilotSettings = {
|
|||
stream: true,
|
||||
embeddingProvider: ModelProviders.OPENAI,
|
||||
defaultSaveFolder: 'copilot-conversations',
|
||||
chatNoteContextPath: '',
|
||||
debug: false,
|
||||
};
|
||||
|
|
|
|||
13
src/main.ts
13
src/main.ts
|
|
@ -2,6 +2,7 @@ import ChainManager from '@/LLMProviders/chainManager';
|
|||
import { LangChainParams, SetChainOptions } from '@/aiParams';
|
||||
import { ChainType } from '@/chainFactory';
|
||||
import { AddPromptModal } from "@/components/AddPromptModal";
|
||||
import { ChatNoteContextModal } from "@/components/ChatNoteContextModal";
|
||||
import CopilotView from '@/components/CopilotView';
|
||||
import { LanguageModal } from "@/components/LanguageModal";
|
||||
import { ListPromptModal } from "@/components/ListPromptModal";
|
||||
|
|
@ -368,6 +369,18 @@ export default class CopilotPlugin extends Plugin {
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'set-chat-note-context',
|
||||
name: 'Set note context for Chat mode',
|
||||
callback: async () => {
|
||||
new ChatNoteContextModal(this.app, this.settings, async (path: string) => {
|
||||
// Store the path in the plugin's settings, default to empty string
|
||||
this.settings.chatNoteContextPath = path;
|
||||
await this.saveSettings();
|
||||
}).open();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
processSelection(editor: Editor, eventType: string, eventSubtype?: string) {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ export interface CopilotSettings {
|
|||
stream: boolean;
|
||||
embeddingProvider: string;
|
||||
defaultSaveFolder: string;
|
||||
chatNoteContextPath: string;
|
||||
debug: boolean;
|
||||
}
|
||||
|
||||
|
|
|
|||
45
src/utils.ts
45
src/utils.ts
|
|
@ -13,7 +13,30 @@ import {
|
|||
RetrievalQAChain
|
||||
} from "langchain/chains";
|
||||
import moment from 'moment';
|
||||
import { TFile } from 'obsidian';
|
||||
import { TFile, Vault } from 'obsidian';
|
||||
|
||||
export const isFolderMatch = (fileFullpath: string, inputPath: string): boolean => {
|
||||
const fileSegments = fileFullpath.split('/').map(segment => segment.toLowerCase());
|
||||
return fileSegments.includes(inputPath.toLowerCase());
|
||||
}
|
||||
|
||||
export const getNotesFromPath = async (vault: Vault, path: string): Promise<TFile[]> => {
|
||||
const files = await vault.getMarkdownFiles();
|
||||
|
||||
// Special handling for the root path '/'
|
||||
if (path === '/') {
|
||||
return files;
|
||||
}
|
||||
|
||||
// Split the path to get the last folder name
|
||||
const pathSegments = path.split('/');
|
||||
const lastSegment = pathSegments[pathSegments.length - 1].toLowerCase();
|
||||
|
||||
return files.filter(file => {
|
||||
// Split the file path and get the last directory name
|
||||
return isFolderMatch(file.path, lastSegment) || file.basename === lastSegment;
|
||||
});
|
||||
}
|
||||
|
||||
export const stringToChainType = (chain: string): ChainType => {
|
||||
switch(chain) {
|
||||
|
|
@ -73,7 +96,7 @@ export const formatDateTime = (now: Date, timezone: 'local' | 'utc' = 'local') =
|
|||
|
||||
export async function getFileContent(file: TFile): Promise<string | null> {
|
||||
if (file.extension != "md") return null;
|
||||
return await this.app.vault.read(file);
|
||||
return await this.app.vault.cachedRead(file);
|
||||
}
|
||||
|
||||
export function getFileName(file: TFile): string {
|
||||
|
|
@ -117,17 +140,19 @@ export function sendNoteContentPrompt(
|
|||
+ `Feel free to ask related questions, such as 'give me a summary of this note in bullet points', 'what key questions does it answer', etc. "\n`
|
||||
}
|
||||
|
||||
export function useNoteAsContextPrompt(
|
||||
noteName: string,
|
||||
noteContent: string | null,
|
||||
): string {
|
||||
return `Please read the note below and be ready to answer questions about it. `
|
||||
export function sendNotesContentPrompt(notes: { name: string; content: string }[]): string {
|
||||
return `Please read the notes below and be ready to answer questions about them. `
|
||||
+ `If there's no information about a certain topic, just say the note `
|
||||
+ `does not mention it. `
|
||||
+ `The content of the note is between "/***/":\n\n/***/\n\n${noteContent}\n\n/***/\n\n`
|
||||
+ `The content of the note is between "/***/":\n\n/***/\n\n${JSON.stringify(notes)}\n\n/***/\n\n`
|
||||
+ `Please reply with the following word for word:`
|
||||
+ `"OK I've read this note titled [[ ${noteName} ]]. `
|
||||
+ `Feel free to ask **specific** questions about it. For generic questions like 'give me a summary', 'brainstorm based on the content', Chat mode with *context sent in the prompt* is a better choice."\n`
|
||||
+ `"OK I've read these notes. `
|
||||
+ `Feel free to ask related questions, such as 'give me a summary of these notes in bullet points', 'what key questions does these notes answer', etc. "\n`
|
||||
}
|
||||
|
||||
export function getSendChatContextNotesPrompt(notes: { name: string; content: string }[]): string {
|
||||
const noteTitles = notes.map(note => `[[${note.name}]]`).join('\n\n');
|
||||
return `Please read the notes below and be ready to answer questions about them. \n\n${noteTitles}`;
|
||||
}
|
||||
|
||||
export function fixGrammarSpellingSelectionPrompt(selectedText: string): string {
|
||||
|
|
|
|||
15
styles.css
15
styles.css
|
|
@ -269,38 +269,37 @@ If your plugin does not need CSS, delete this file.
|
|||
width: 95%;
|
||||
}
|
||||
|
||||
/* Apply some style to AddPromptModal */
|
||||
.custom-prompt-modal {
|
||||
.copilot-command-modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.custom-prompt-input-container {
|
||||
.copilot-command-input-container {
|
||||
width: 90%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.custom-prompt-input-container input,
|
||||
.custom-prompt-input-container textarea {
|
||||
.copilot-command-input-container input,
|
||||
.copilot-command-input-container textarea {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.custom-prompt-input-container textarea {
|
||||
.copilot-command-input-container textarea {
|
||||
height: 150px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.custom-prompt-save-btn-container {
|
||||
.copilot-command-save-btn-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.custom-prompt-save-btn {
|
||||
.copilot-command-save-btn {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
|
|
|||
97
tests/utils.test.ts
Normal file
97
tests/utils.test.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import * as Obsidian from 'obsidian';
|
||||
import { getNotesFromPath, isFolderMatch } from '../src/utils';
|
||||
|
||||
|
||||
describe('isFolderMatch', () => {
|
||||
it('should return file from the folder name 1', async () => {
|
||||
const match = isFolderMatch('test2/note3.md', 'test2');
|
||||
expect(match).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return file from the folder name 2', async () => {
|
||||
const match = isFolderMatch('test/test2/note1.md', 'test2');
|
||||
expect(match).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return file from the folder name 3', async () => {
|
||||
const match = isFolderMatch('test/test2/note1.md', 'test');
|
||||
expect(match).toEqual(true);
|
||||
});
|
||||
|
||||
it('should not return file from the folder name 1', async () => {
|
||||
const match = isFolderMatch('test/test2/note1.md', 'tes');
|
||||
expect(match).toEqual(false);
|
||||
});
|
||||
|
||||
it('should return file from file name 1', async () => {
|
||||
const match = isFolderMatch('test/test2/note1.md', 'note1.md');
|
||||
expect(match).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Vault', () => {
|
||||
it('should return all markdown files', async () => {
|
||||
const vault = new Obsidian.Vault();
|
||||
const files = await vault.getMarkdownFiles();
|
||||
expect(files).toEqual([
|
||||
{ path: 'test/test2/note1.md' },
|
||||
{ path: 'test/note2.md' },
|
||||
{ path: 'test2/note3.md' },
|
||||
{ path: 'note4.md' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNotesFromPath', () => {
|
||||
it('should return all markdown files', async () => {
|
||||
const vault = new Obsidian.Vault();
|
||||
const files = await getNotesFromPath(vault, '/');
|
||||
expect(files).toEqual([
|
||||
{ path: 'test/test2/note1.md' },
|
||||
{ path: 'test/note2.md' },
|
||||
{ path: 'test2/note3.md' },
|
||||
{ path: 'note4.md' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return filtered markdown files 1', async () => {
|
||||
const vault = new Obsidian.Vault();
|
||||
const files = await getNotesFromPath(vault, 'test2');
|
||||
expect(files).toEqual([
|
||||
{ path: 'test/test2/note1.md' },
|
||||
{ path: 'test2/note3.md' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return filtered markdown files 2', async () => {
|
||||
const vault = new Obsidian.Vault();
|
||||
const files = await getNotesFromPath(vault, 'test');
|
||||
expect(files).toEqual([
|
||||
{ path: 'test/test2/note1.md' },
|
||||
{ path: 'test/note2.md' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return filtered markdown files 3', async () => {
|
||||
const vault = new Obsidian.Vault();
|
||||
const files = await getNotesFromPath(vault, 'note4.md');
|
||||
expect(files).toEqual([
|
||||
{ path: 'note4.md' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return filtered markdown files 4', async () => {
|
||||
const vault = new Obsidian.Vault();
|
||||
const files = await getNotesFromPath(vault, '/test');
|
||||
expect(files).toEqual([
|
||||
{ path: 'test/test2/note1.md' },
|
||||
{ path: 'test/note2.md' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not return markdown files', async () => {
|
||||
const vault = new Obsidian.Vault();
|
||||
const files = await getNotesFromPath(vault, '');
|
||||
expect(files).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
@ -16,7 +16,8 @@
|
|||
"jsx": "react",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"strictNullChecks": true,
|
||||
"strictNullChecks": true,
|
||||
"types": ["jest"],
|
||||
"lib": [
|
||||
"DOM",
|
||||
"ES5",
|
||||
|
|
|
|||
Loading…
Reference in a new issue