mirror of
https://github.com/fbarrca/obsidian-inlineAI.git
synced 2026-07-22 11:50:24 +00:00
Add tooltip widget functionality
This commit is contained in:
parent
d67c4ad70b
commit
4d725b1c2b
5 changed files with 318 additions and 1 deletions
8
src/api.ts
Normal file
8
src/api.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// api.ts
|
||||
export async function callApi(content: string): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve("API response for content: " + content);
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
96
src/components/tooltipWidget.ts
Normal file
96
src/components/tooltipWidget.ts
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
64
src/main.ts
Normal file
64
src/main.ts
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
81
src/modules/tooltipExtension.ts
Normal file
81
src/modules/tooltipExtension.ts
Normal file
|
|
@ -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<null>();
|
||||
|
||||
export function cursorTooltipExtension() {
|
||||
return StateField.define<Tooltip | null>({
|
||||
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(),
|
||||
}),
|
||||
};
|
||||
}
|
||||
70
styles.css
70
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: <div class="loader"></div> */
|
||||
.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;
|
||||
}
|
||||
Loading…
Reference in a new issue