1- import { App , Notice , Plugin , PluginSettingTab , Setting , MarkdownView } from "obsidian" ;
1+ import { App , Notice , Plugin , PluginSettingTab , Setting , MarkdownView , Editor } from "obsidian" ;
22import { GeminiService } from "./src/services/GeminiService" ;
33import { LanguageSelectionModal } from "./src/modals/LanguageSelectionModal" ;
44import { 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 ( ) ;
0 commit comments