Skip to content

Commit 44df593

Browse files
authored
Merge pull request #17 from 0xneobyte/feat/deep-editor-integration
[FEAT] : Deep Editor Integration - Transform VaultAI into a Full AI Writing Assistant
2 parents 631a0a0 + 890e8fb commit 44df593

4 files changed

Lines changed: 291 additions & 4 deletions

File tree

main.ts

Lines changed: 287 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { App, Notice, Plugin, PluginSettingTab, Setting, MarkdownView } from "obsidian";
1+
import { App, Notice, Plugin, PluginSettingTab, Setting, MarkdownView, Editor } from "obsidian";
22
import { GeminiService } from "./src/services/GeminiService";
33
import { LanguageSelectionModal } from "./src/modals/LanguageSelectionModal";
44
import { MarkdownRenderer } from "obsidian";
@@ -57,6 +57,12 @@ export default class GeminiChatbotPlugin extends Plugin {
5757
private currentSession: ChatSession | null = null;
5858
private referencedFiles: Map<string, string> | null = null;
5959

60+
// Editor integration properties
61+
private activeEditor: Editor | null = null;
62+
private cursorPosition: { line: number; ch: number } | null = null;
63+
private editorContext: { fileName: string; lineContent: string; surroundingLines: string[] } | null = null;
64+
private insertMode = false;
65+
6066
// Rate limiting and context management
6167
private lastApiCall = 0;
6268
private readonly API_COOLDOWN = 1000; // Prevent rapid-fire API calls
@@ -89,6 +95,39 @@ export default class GeminiChatbotPlugin extends Plugin {
8995
// Register custom prompt commands
9096
this.registerCustomPromptCommands();
9197

98+
// Add editor integration commands
99+
this.addCommand({
100+
id: 'vaultai-generate-at-cursor',
101+
name: 'VaultAI: Generate content at cursor',
102+
editorCallback: (editor: Editor) => {
103+
this.generateAtCursor(editor);
104+
}
105+
});
106+
107+
this.addCommand({
108+
id: 'vaultai-complete-line',
109+
name: 'VaultAI: Complete current line',
110+
editorCallback: (editor: Editor) => {
111+
this.completeLine(editor);
112+
}
113+
});
114+
115+
this.addCommand({
116+
id: 'vaultai-explain-selection',
117+
name: 'VaultAI: Explain selected text',
118+
editorCallback: (editor: Editor) => {
119+
this.explainSelection(editor);
120+
}
121+
});
122+
123+
this.addCommand({
124+
id: 'vaultai-improve-selection',
125+
name: 'VaultAI: Improve selected text',
126+
editorCallback: (editor: Editor) => {
127+
this.improveSelection(editor);
128+
}
129+
});
130+
92131
// Add settings tab
93132
this.addSettingTab(new GeminiChatbotSettingTab(this.app, this));
94133

@@ -117,8 +156,20 @@ export default class GeminiChatbotPlugin extends Plugin {
117156
this.updateChatHeader();
118157
}
119158
}
159+
// Update editor context when active file changes
160+
this.updateEditorContext();
161+
})
162+
);
163+
164+
// Add cursor position tracking
165+
this.registerEvent(
166+
this.app.workspace.on("editor-change", () => {
167+
this.updateEditorContext();
120168
})
121169
);
170+
171+
// Initial editor context update
172+
this.updateEditorContext();
122173
}
123174

124175
public initializeGeminiService() {
@@ -333,6 +384,12 @@ export default class GeminiChatbotPlugin extends Plugin {
333384
context += `\nRelevant content from current note:\n${truncatedContent}\n`;
334385
}
335386

387+
// Add editor context (cursor position and surrounding lines)
388+
const editorContext = this.getEditorContextString();
389+
if (editorContext) {
390+
context += editorContext;
391+
}
392+
336393
// Prepare the final message
337394
const finalMessage = context
338395
? `${context}\n\nUser question: ${contextMessage}`
@@ -373,6 +430,12 @@ export default class GeminiChatbotPlugin extends Plugin {
373430

374431
await this.addMessageToChat(botMessage);
375432

433+
// If insert mode is on, automatically insert the response at cursor
434+
if (this.insertMode && this.activeEditor) {
435+
await this.insertAtCursor(response);
436+
new Notice("AI response inserted at cursor");
437+
}
438+
376439
// Update chat session
377440
if (this.currentSession) {
378441
if (this.currentSession.messages.length === 2) {
@@ -939,8 +1002,15 @@ export default class GeminiChatbotPlugin extends Plugin {
9391002
sendButton.addClass("send-button");
9401003
sendButton.textContent = "↑";
9411004

1005+
// Insert at cursor button
1006+
const insertButton = document.createElement("button");
1007+
insertButton.addClass("insert-button");
1008+
insertButton.textContent = "📍";
1009+
insertButton.title = "Insert AI response at cursor position";
1010+
9421011
actionsContainer.appendChild(promptsButton);
9431012
actionsContainer.appendChild(mentionButton);
1013+
actionsContainer.appendChild(insertButton);
9441014
actionsContainer.appendChild(sendButton);
9451015

9461016
inputWrapper.appendChild(textarea);
@@ -958,6 +1028,7 @@ export default class GeminiChatbotPlugin extends Plugin {
9581028

9591029
const sendButton = this.chatContainer.querySelector(".send-button");
9601030
const promptsButton = this.chatContainer.querySelector(".prompts-button");
1031+
const insertButton = this.chatContainer.querySelector(".insert-button");
9611032
const inputField = this.chatContainer.querySelector(
9621033
".chat-input"
9631034
) as HTMLTextAreaElement;
@@ -970,6 +1041,10 @@ export default class GeminiChatbotPlugin extends Plugin {
9701041
this.showCustomPromptsDropdown();
9711042
});
9721043

1044+
insertButton?.addEventListener("click", () => {
1045+
this.toggleInsertMode();
1046+
});
1047+
9731048
sendButton?.addEventListener("click", () => {
9741049
if (this.inputField) {
9751050
const message = this.inputField.value.trim();
@@ -1195,6 +1270,217 @@ export default class GeminiChatbotPlugin extends Plugin {
11951270
}
11961271
}
11971272

1273+
// Editor integration methods
1274+
private updateEditorContext(): void {
1275+
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
1276+
if (!activeView || !activeView.editor) {
1277+
this.activeEditor = null;
1278+
this.cursorPosition = null;
1279+
this.editorContext = null;
1280+
return;
1281+
}
1282+
1283+
this.activeEditor = activeView.editor;
1284+
this.cursorPosition = this.activeEditor.getCursor();
1285+
1286+
const fileName = activeView.file?.name || "Untitled";
1287+
const currentLine = this.cursorPosition.line;
1288+
const lineContent = this.activeEditor.getLine(currentLine);
1289+
1290+
// Get surrounding lines for context (3 before, 3 after)
1291+
const surroundingLines: string[] = [];
1292+
const start = Math.max(0, currentLine - 3);
1293+
const end = Math.min(this.activeEditor.lineCount() - 1, currentLine + 3);
1294+
1295+
for (let i = start; i <= end; i++) {
1296+
const line = this.activeEditor.getLine(i);
1297+
const lineNumber = i + 1; // 1-indexed for display
1298+
const marker = i === currentLine ? " → " : " ";
1299+
surroundingLines.push(`${lineNumber.toString().padStart(3)}${marker}${line}`);
1300+
}
1301+
1302+
this.editorContext = {
1303+
fileName,
1304+
lineContent,
1305+
surroundingLines
1306+
};
1307+
}
1308+
1309+
private getEditorContextString(): string {
1310+
if (!this.editorContext || !this.cursorPosition) {
1311+
return "";
1312+
}
1313+
1314+
const { fileName, lineContent, surroundingLines } = this.editorContext;
1315+
const currentLineNum = this.cursorPosition.line + 1;
1316+
1317+
return `\n\n--- Editor Context ---
1318+
File: ${fileName}
1319+
Current line: ${currentLineNum}
1320+
Current line content: "${lineContent}"
1321+
1322+
Surrounding context:
1323+
\`\`\`
1324+
${surroundingLines.join('\n')}
1325+
\`\`\`
1326+
--- End Context ---\n`;
1327+
}
1328+
1329+
private async insertAtCursor(content: string): Promise<void> {
1330+
if (!this.activeEditor || !this.cursorPosition) {
1331+
new Notice("No active editor found");
1332+
return;
1333+
}
1334+
1335+
// Insert content at current cursor position
1336+
this.activeEditor.replaceRange(content, this.cursorPosition);
1337+
1338+
// Update cursor position to end of inserted content
1339+
const lines = content.split('\n');
1340+
const newLine = this.cursorPosition.line + lines.length - 1;
1341+
const newCh = lines.length === 1
1342+
? this.cursorPosition.ch + content.length
1343+
: lines[lines.length - 1].length;
1344+
1345+
this.activeEditor.setCursor({ line: newLine, ch: newCh });
1346+
}
1347+
1348+
private async replaceSelection(content: string): Promise<void> {
1349+
if (!this.activeEditor) {
1350+
new Notice("No active editor found");
1351+
return;
1352+
}
1353+
1354+
const selection = this.activeEditor.getSelection();
1355+
if (selection) {
1356+
this.activeEditor.replaceSelection(content);
1357+
} else {
1358+
// If no selection, insert at cursor
1359+
await this.insertAtCursor(content);
1360+
}
1361+
}
1362+
1363+
// Editor command methods
1364+
private async generateAtCursor(editor: Editor): Promise<void> {
1365+
this.activeEditor = editor;
1366+
this.updateEditorContext();
1367+
1368+
// Open chat and focus on input with a helpful prompt
1369+
this.openChatFromCommand();
1370+
if (this.inputField) {
1371+
this.inputField.value = "Generate content here: ";
1372+
this.inputField.focus();
1373+
// Position cursor at the end
1374+
setTimeout(() => {
1375+
if (this.inputField) {
1376+
this.inputField.setSelectionRange(this.inputField.value.length, this.inputField.value.length);
1377+
}
1378+
}, 100);
1379+
}
1380+
}
1381+
1382+
private async completeLine(editor: Editor): Promise<void> {
1383+
this.activeEditor = editor;
1384+
const cursor = editor.getCursor();
1385+
const currentLine = editor.getLine(cursor.line);
1386+
1387+
if (!currentLine.trim()) {
1388+
new Notice("Current line is empty");
1389+
return;
1390+
}
1391+
1392+
// Prepare a completion prompt
1393+
const prompt = `Complete this line: "${currentLine}"`;
1394+
1395+
this.updateEditorContext();
1396+
this.openChatFromCommand();
1397+
1398+
if (this.inputField) {
1399+
this.inputField.value = prompt;
1400+
// Auto-send the completion request
1401+
setTimeout(() => {
1402+
const sendButton = this.chatContainer?.querySelector('.send-button') as HTMLButtonElement;
1403+
if (sendButton) {
1404+
sendButton.click();
1405+
}
1406+
}, 200);
1407+
}
1408+
}
1409+
1410+
private async explainSelection(editor: Editor): Promise<void> {
1411+
this.activeEditor = editor;
1412+
const selection = editor.getSelection();
1413+
1414+
if (!selection.trim()) {
1415+
new Notice("Please select some text first");
1416+
return;
1417+
}
1418+
1419+
const prompt = `Explain this text: "${selection}"`;
1420+
1421+
this.updateEditorContext();
1422+
this.openChatFromCommand();
1423+
1424+
if (this.inputField) {
1425+
this.inputField.value = prompt;
1426+
// Auto-send the explanation request
1427+
setTimeout(() => {
1428+
const sendButton = this.chatContainer?.querySelector('.send-button') as HTMLButtonElement;
1429+
if (sendButton) {
1430+
sendButton.click();
1431+
}
1432+
}, 200);
1433+
}
1434+
}
1435+
1436+
private async improveSelection(editor: Editor): Promise<void> {
1437+
this.activeEditor = editor;
1438+
const selection = editor.getSelection();
1439+
1440+
if (!selection.trim()) {
1441+
new Notice("Please select some text first");
1442+
return;
1443+
}
1444+
1445+
const prompt = `Improve this text: "${selection}"`;
1446+
1447+
this.updateEditorContext();
1448+
this.openChatFromCommand();
1449+
1450+
if (this.inputField) {
1451+
this.inputField.value = prompt;
1452+
this.inputField.focus();
1453+
// Position cursor at the end
1454+
setTimeout(() => {
1455+
if (this.inputField) {
1456+
this.inputField.setSelectionRange(this.inputField.value.length, this.inputField.value.length);
1457+
}
1458+
}, 100);
1459+
}
1460+
}
1461+
1462+
private toggleInsertMode(): void {
1463+
this.insertMode = !this.insertMode;
1464+
const insertButton = this.chatContainer?.querySelector(".insert-button") as HTMLElement;
1465+
1466+
if (insertButton) {
1467+
if (this.insertMode) {
1468+
insertButton.textContent = "📍✓";
1469+
insertButton.style.backgroundColor = "var(--interactive-accent)";
1470+
insertButton.style.color = "var(--text-on-accent)";
1471+
insertButton.title = "Insert mode ON - AI responses will be inserted at cursor";
1472+
} else {
1473+
insertButton.textContent = "📍";
1474+
insertButton.style.backgroundColor = "";
1475+
insertButton.style.color = "";
1476+
insertButton.title = "Insert AI response at cursor position";
1477+
}
1478+
}
1479+
1480+
this.updateEditorContext();
1481+
new Notice(this.insertMode ? "Insert mode ON" : "Insert mode OFF");
1482+
}
1483+
11981484
onunload() {
11991485
this.chatIcon?.remove();
12001486
this.chatContainer?.remove();

manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"id": "vault-ai",
33
"name": "VaultAI",
4-
"version": "1.0.5",
4+
"version": "1.0.6",
55
"minAppVersion": "0.15.0",
66
"description": "Transform your note-taking with an intelligent AI assistant powered by Google's Gemini AI. Features a sleek floating chat interface for seamless writing assistance, content generation, and smart note enhancement.",
77
"author": "Tharushka Dinujaya",

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "vault-ai",
3-
"version": "1.0.5",
3+
"version": "1.0.6",
44
"description": "Transform your note-taking with an intelligent AI assistant powered by Google's Gemini AI.",
55
"main": "main.js",
66
"scripts": {

versions.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
22
"1.0.0": "0.15.0",
3-
"1.0.5": "0.15.0"
3+
"1.0.5": "0.15.0",
4+
"1.0.6": "0.15.0"
45
}

0 commit comments

Comments
 (0)