diff --git a/src/api.ts b/src/api.ts new file mode 100644 index 0000000..261fef5 --- /dev/null +++ b/src/api.ts @@ -0,0 +1,8 @@ +// api.ts +export async function callApi(content: string): Promise { + return new Promise((resolve) => { + setTimeout(() => { + resolve("API response for content: " + content); + }, 2000); + }); +} diff --git a/src/components/tooltipWidget.ts b/src/components/tooltipWidget.ts new file mode 100644 index 0000000..c9c74ec --- /dev/null +++ b/src/components/tooltipWidget.ts @@ -0,0 +1,96 @@ +// TooltipWidget.ts +import { EditorView, placeholder, keymap } from "@codemirror/view"; +import { EditorState } from "@codemirror/state"; +import { ButtonComponent, setIcon, Notice } from "obsidian"; +import { callApi } from "../api"; + +export class TooltipWidget { + dom: HTMLDivElement; + editorView: EditorView; + submitButton: ButtonComponent; + loader: HTMLDivElement; + selectedText: string; // Add property to store selected text + + constructor(selectedText: string) { + // Accept selectedText as a parameter + this.selectedText = selectedText; + + this.dom = document.createElement("div"); + this.dom.className = "cm-tooltip"; + setIcon(this.dom, "pencil"); + + const editorDom = document.createElement("div"); + editorDom.className = "cm-tooltip-editor"; + this.dom.appendChild(editorDom); + + this.editorView = new EditorView({ + state: EditorState.create({ + doc: "", + extensions: [ + placeholder("Ask copilot"), + keymap.of([ + { + key: "Enter", + run: () => { + this.submitAction(); + return true; + }, + preventDefault: true, + }, + ]), + ], + }), + parent: editorDom, + dispatch: (tr) => this.editorView.update([tr]), + }); + + this.submitButton = new ButtonComponent(this.dom); + this.submitButton.buttonEl.classList.add("submit-button"); + setIcon(this.submitButton.buttonEl, "send-horizontal"); + this.submitButton.onClick(() => this.submitAction()); + + this.loader = document.createElement("div"); + this.loader.className = "loader"; + this.loader.style.display = "none"; + this.dom.appendChild(this.loader); + + this.editorView.requestMeasure({ + read: () => { + this.editorView.dom.style.height = "100%"; + this.editorView.dom.style.width = "100%"; + }, + }); + } + + async submitAction() { + const userInput = this.editorView.state.doc.toString(); + + // Log the selected text from the main editor + console.log("Selected Text:", this.selectedText); + + this.submitButton.setDisabled(true); + this.submitButton.buttonEl.style.display = "none"; + this.loader.style.display = "block"; + + try { + const response = await callApi(userInput); + new Notice(response); + } catch (error) { + new Notice("Error: " + (error as Error).message); + } finally { + this.submitButton.setDisabled(false); + this.submitButton.buttonEl.style.display = "block"; + this.loader.style.display = "none"; + } + } + + destroy() { + this.editorView.destroy(); + } + + mount() { + setTimeout(() => { + this.editorView.focus(); + }, 0); + } +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..fb93b31 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,64 @@ +import { Plugin, MarkdownView } from "obsidian"; +import { + cursorTooltipExtension, + showTooltipEffect, +} from "./modules/tooltipExtension"; +import { EditorView } from "@codemirror/view"; + +interface MyPluginSettings { + mySetting: string; +} + +const DEFAULT_SETTINGS: MyPluginSettings = { + mySetting: "default", +}; + +export default class MyPlugin extends Plugin { + settings: MyPluginSettings = DEFAULT_SETTINGS; + + async onload() { + await this.loadSettings(); + + // Register the cursor tooltip extension + this.registerEditorExtension(cursorTooltipExtension()); + + // Add command to show tooltip + this.addCommand({ + id: "show-cursor-tooltip", + name: "Show Cursor Tooltip", + callback: () => { + const markdownView = + this.app.workspace.getActiveViewOfType(MarkdownView); + if (markdownView) { + // Dispatch the effect to show the tooltip + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ((markdownView.editor as any).cm as EditorView).dispatch({ + effects: showTooltipEffect.of(null), + }); + } + }, + hotkeys: [ + { + modifiers: ["Mod"], // "Mod" stands for "Ctrl" on Windows/Linux and "Cmd" on macOS + key: "a", // You can change this key to your preference + }, + ], + }); + } + + onunload() { + // Cleanup if necessary + } + + async loadSettings() { + this.settings = Object.assign( + {}, + DEFAULT_SETTINGS, + await this.loadData() + ); + } + + async saveSettings() { + await this.saveData(this.settings); + } +} diff --git a/src/modules/tooltipExtension.ts b/src/modules/tooltipExtension.ts new file mode 100644 index 0000000..81ae10b --- /dev/null +++ b/src/modules/tooltipExtension.ts @@ -0,0 +1,81 @@ +// extensions.ts +import { StateField, EditorState } from "@codemirror/state"; +import { showTooltip, type Tooltip } from "@codemirror/view"; +import { TooltipWidget } from "../components/tooltipWidget"; +import { StateEffect } from "@codemirror/state"; + +export const showTooltipEffect = StateEffect.define(); + +export function cursorTooltipExtension() { + return StateField.define({ + create() { + return null; + }, + update(tooltip, tr) { + if (tr.effects.some((e) => e.is(showTooltipEffect))) { + return getCursorTooltip(tr.state); + } + if (tr.docChanged || tr.selection) { + return null; + } + return tooltip; + }, + provide: (field) => showTooltip.from(field), + }); +} + +function getCursorTooltip(state: EditorState): Tooltip | null { + const { selection } = state; + const mainSelection = selection.main; + const { from, to, head, anchor } = mainSelection; + const currentLine = state.doc.lineAt(from); + const isSelectionActive = !mainSelection.empty; + + let posAt = currentLine.from; + let above = true; + let selectedText = ""; + + if (isSelectionActive) { + selectedText = state.doc.sliceString(from, to); + + if (head > anchor) { + console.log("Putting tooltip below"); + const endLine = state.doc.lineAt(to); + + if (endLine.number < state.doc.lines) { + // Get the next line's start position + const nextLine = state.doc.line(endLine.number + 1); + posAt = nextLine.from; + } else { + // If at the end of the document, place tooltip at the document's end + posAt = state.doc.length; + } + above = false; // Tooltip is below + } else if (head < anchor) { + console.log("Putting tooltip above"); + const startLine = state.doc.lineAt(from); + posAt = startLine.from; + above = true; // Tooltip is above + } else { + // Fallback for any other cases + posAt = currentLine.from; + above = true; + } + } else { + // When there's no active selection, place tooltip at the start of the current line + posAt = currentLine.from; + above = true; + } + + const tooltipWidget = new TooltipWidget(selectedText); // Pass selectedText + + return { + pos: posAt, + above, + create: () => ({ + dom: tooltipWidget.dom, + destroy: () => tooltipWidget.destroy(), + mount: () => tooltipWidget.mount(), + }), + }; +} diff --git a/styles.css b/styles.css index 71cc60f..d83120f 100644 --- a/styles.css +++ b/styles.css @@ -4,5 +4,73 @@ This CSS file will be included with your plugin, and available in the app when your plugin is enabled. If your plugin does not need CSS, delete this file. - + // Styling for the tooltip + dom.style.padding = "4px 8px"; + dom.style.background = "--background-secondary"; + dom.style.zIndex = "--layer-tooltip"; + dom.style.color = "#fff"; + dom.style.borderRadius = "4px"; + dom.style.fontSize = "--font-interface-theme"; */ +.cm-tooltip { + padding: 4px 8px; + background: var(--background-secondary) !important; + z-index: var(--layer-tooltip); + color: var(--text-normal); + border-radius: 4px; + font-size: var(--font-interface-theme); + width: 80%; + max-width: var(--file-line-width); + max-height: 200px; + border-color: var(--background-modifier-border) !important; + display: flex; + flex-direction: row; + align-items: center; /* Center children vertically */ +} + +.cm-tooltip > :first-child { + margin-right: 8px; /* Add some space to the right of the first child */ +} + +/* .cm-tooltip > :last-child { + margin-left: 8px; +} */ + +.cm-tooltip-editor { + display: flex; + padding: 1px; + padding-left: 10px; + height: var(--line-height); + width: 100%; + max-height: 100%; /* Ensure it can't be taller than the parent container */ + align-items: center; /* Center children vertically */ + background-color: var(--background-primary); + border: 1px solid var(--background-modifier-border-focus ); + border-radius: 4px; + overflow: hidden; /* Hide any overflow content */ +} + +/* HTML:
*/ +.loader { + width: 5px; /* Adjusted width to ensure total width is 42px with box-shadow */ + aspect-ratio: 1; + border-radius: 50%; + color: var(--text-normal); + background: var(--background-primary); + animation: l5 1s infinite linear alternate; + margin-left: 26px; + margin-right: 20px; +} + +@keyframes l5 { + 0% {box-shadow: 10px 0 var(--text-accent), -10px 0 var(--text-faint); background: var(--text-accent) } + 33% {box-shadow: 10px 0 var(--text-accent), -10px 0 var(--text-faint); background: var(--text-faint)} + 66% {box-shadow: 10px 0 var(--text-faint), -10px 0 var(--text-accent); background: var(--text-faint)} + 100%{box-shadow: 10px 0 var(--text-faint), -10px 0 var(--text-accent); background: var(--text-accent) } +} + +.submit-button { + height: 100%; + + margin-left: 8px; +} \ No newline at end of file