дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-13 21:25:47 +02:00
Порівняти коміти
1
Коміти
| Автор | SHA1 | Дата | |
|---|---|---|---|
|
|
7c69eea7c2 |
No files matched your search
@@ -27,6 +27,13 @@ pub fn set_text(text: &str, owner: usize) -> std::io::Result<()> {
|
||||
imp::set_text(text, owner)
|
||||
}
|
||||
|
||||
/// The clipboard's plain text, None when it holds none. macOS only (main thread);
|
||||
/// Linux reads it asynchronously through GTK in the app, Windows through the WebView.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn text() -> Option<String> {
|
||||
imp::text()
|
||||
}
|
||||
|
||||
/// Size of `DROPFILES`: `pFiles`, `pt` (two `LONG`s), `fNC`, `fWide`.
|
||||
const HEADER: usize = 20;
|
||||
|
||||
@@ -373,6 +380,14 @@ mod imp {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn text() -> Option<String> {
|
||||
debug_assert!(MainThreadMarker::new().is_some(), "NSPasteboard access requires the main thread");
|
||||
autoreleasepool(|_| {
|
||||
// SAFETY: the type is an AppKit constant, valid for the process' lifetime.
|
||||
NSPasteboard::generalPasteboard().stringForType(unsafe { NSPasteboardTypeString }).map(|s| s.to_string())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_text(text: &str, _owner: usize) -> std::io::Result<()> {
|
||||
debug_assert!(MainThreadMarker::new().is_some(), "NSPasteboard access requires the main thread");
|
||||
autoreleasepool(|_| {
|
||||
|
||||
@@ -22,6 +22,37 @@ pub async fn clipboard_write_text(app: AppHandle, window: WebviewWindow, text: S
|
||||
with_clipboard(app, move || ide_clipboard::set_text(&text, owner).map_err(ide_core::IdeError::from)).await
|
||||
}
|
||||
|
||||
/// The clipboard's plain text, read natively as [`clipboard_write_text`] writes it: a Paste
|
||||
/// from a menu has no paste event, and WebKitGTK gives `navigator.clipboard.readText()` no
|
||||
/// text there (#3041). None on Windows, where the WebView's clipboard is used.
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn clipboard_text(app: AppHandle) -> IdeResult<Option<String>> {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||
app.run_on_main_thread(move || {
|
||||
let sender = std::cell::Cell::new(Some(tx));
|
||||
gtk::Clipboard::get(>k::gdk::SELECTION_CLIPBOARD).request_text(move |_, text| {
|
||||
if let Some(sender) = sender.take() {
|
||||
let _ = sender.send(text.map(|t| t.to_string()));
|
||||
}
|
||||
});
|
||||
})
|
||||
.map_err(|error| IdeError::internal(format!("Cannot dispatch clipboard access to the main thread: {error}")))?;
|
||||
Ok(rx.await.ok().flatten())
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
with_clipboard(app, || Ok(ide_clipboard::text())).await
|
||||
}
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
|
||||
{
|
||||
let _ = app;
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Absolute paths on the clipboard, empty when it holds no file list.
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
|
||||
@@ -187,6 +187,7 @@ pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
|
||||
fs::fs_read_text,
|
||||
fs::fs_open_with_system,
|
||||
clipboard::clipboard_write_text,
|
||||
clipboard::clipboard_text,
|
||||
clipboard::clipboard_file_list,
|
||||
clipboard::clipboard_has_files,
|
||||
clipboard::clipboard_image,
|
||||
|
||||
@@ -280,6 +280,12 @@ export const commands = {
|
||||
fsOpenWithSystem: (path: string) => typedError<null, IdeError>(__TAURI_INVOKE("fs_open_with_system", { path })),
|
||||
/** Writes text locally, without WebView user-activation requirements. */
|
||||
clipboardWriteText: (text: string) => typedError<null, IdeError>(__TAURI_INVOKE("clipboard_write_text", { text })),
|
||||
/**
|
||||
* The clipboard's plain text, read natively as [`clipboard_write_text`] writes it: a Paste
|
||||
* from a menu has no paste event, and WebKitGTK gives `navigator.clipboard.readText()` no
|
||||
* text there (#3041). None on Windows, where the WebView's clipboard is used.
|
||||
*/
|
||||
clipboardText: () => typedError<string | null, IdeError>(__TAURI_INVOKE("clipboard_text")),
|
||||
/** Absolute paths on the clipboard, empty when it holds no file list. */
|
||||
clipboardFileList: () => typedError<string[], IdeError>(__TAURI_INVOKE("clipboard_file_list")),
|
||||
/** Whether the clipboard holds files: cheap, for the Paste item's enabled state. */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** Edit › Paste from History and Code › Unwrap/Remove: the parts that need popups or the backend. */
|
||||
import { writeClipboardText } from "@/platform/clipboard";
|
||||
import { readClipboardPlainText, writeClipboardText } from "@/platform/clipboard";
|
||||
import { EditorSelection } from "@codemirror/state";
|
||||
import type { EditorView } from "@codemirror/view";
|
||||
import { structure } from "@/editor/structure";
|
||||
@@ -103,7 +103,7 @@ export async function pasteAsPlainText(view: EditorView) {
|
||||
if (view.state.readOnly) return;
|
||||
let text: string;
|
||||
try {
|
||||
text = (await navigator.clipboard?.readText()) ?? "";
|
||||
text = await readClipboardPlainText();
|
||||
} catch {
|
||||
hint("Cannot read the clipboard");
|
||||
return;
|
||||
|
||||
@@ -11,6 +11,7 @@ import { quitApp } from "./closeGuard";
|
||||
import { guessOs } from "./host";
|
||||
import { showKeymapReference } from "./KeymapReference";
|
||||
import { checkForUpdates } from "./updates";
|
||||
import { readClipboardPlainText } from "@/platform/clipboard";
|
||||
|
||||
function errorText(e: unknown) {
|
||||
return e instanceof Error ? e.message : String(e);
|
||||
@@ -50,8 +51,8 @@ export function editCommand(command: "undo" | "redo" | "cut" | "copy" | "paste"
|
||||
if (focused instanceof Element && runUndoHandlers(command, focused)) return;
|
||||
}
|
||||
if (command === "selectAll" && selectReadingPane(document.activeElement)) return;
|
||||
if (command === "paste" && navigator.clipboard?.readText) {
|
||||
void navigator.clipboard.readText().then((text) => document.execCommand("insertText", false, text));
|
||||
if (command === "paste") {
|
||||
void readClipboardPlainText().then((text) => document.execCommand("insertText", false, text));
|
||||
return;
|
||||
}
|
||||
document.execCommand(command);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { VENDORS, vendorPreset } from "./vendors";
|
||||
import { nodeTarget, objectOf, tableOf, type DbNode } from "./nodes";
|
||||
import { SCRIPT_LABELS, canDrop, canModifyTable, canRename, type ScriptKind } from "./objects";
|
||||
import { connectedTargets, openDialog, openSourceDialog, sourceById, useDatabase } from "./store";
|
||||
import { readClipboardPlainText } from "@/platform/clipboard";
|
||||
|
||||
const act = (
|
||||
action: string,
|
||||
@@ -48,7 +49,7 @@ function dataSourceItems(): MenuItem[] {
|
||||
async function sourceFromClipboard() {
|
||||
let text = "";
|
||||
try {
|
||||
text = (await navigator.clipboard?.readText()) ?? "";
|
||||
text = await readClipboardPlainText();
|
||||
} catch {
|
||||
// Unreadable clipboard: reported below like an unknown one.
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { writeClipboardText } from "@/platform/clipboard";
|
||||
import { readClipboardPlainText, writeClipboardText } from "@/platform/clipboard";
|
||||
import { useEffect, useState } from "react";
|
||||
import clsx from "clsx";
|
||||
import { commands, isTauri, type RunConfig } from "@/platform/ipc";
|
||||
@@ -94,7 +94,7 @@ export function EnvDialog({
|
||||
};
|
||||
const paste = async () => {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
const text = await readClipboardPlainText();
|
||||
setRows((r) => pasteRows(r, text));
|
||||
} catch {
|
||||
/* clipboard unavailable */
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { writeClipboardText } from "@/platform/clipboard";
|
||||
import { readClipboardPlainText, writeClipboardText } from "@/platform/clipboard";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import clsx from "clsx";
|
||||
import { call, commands, isTauri, type RunConfig, type TomcatInstallation, type WebArtifact } from "@/platform/ipc";
|
||||
@@ -899,7 +899,7 @@ export function TomcatSettings({ config, onChange }: { config: RunConfig; onChan
|
||||
iconSize={13}
|
||||
title="Paste"
|
||||
onClick={() => {
|
||||
void navigator.clipboard?.readText().then((text) => {
|
||||
void readClipboardPlainText().then((text) => {
|
||||
if (!text.trim()) return;
|
||||
const idx = text.indexOf("=");
|
||||
const name = idx >= 0 ? text.slice(0, idx).trim() : text.trim();
|
||||
|
||||
@@ -16,6 +16,7 @@ import { findOutputLinks } from "./links";
|
||||
import { useRun } from "./store";
|
||||
import styles from "./StackTrace.module.css";
|
||||
import { tx } from "@/platform/i18n";
|
||||
import { readClipboardPlainText } from "@/platform/clipboard";
|
||||
|
||||
export interface StackTraceTab {
|
||||
id: string;
|
||||
@@ -83,7 +84,7 @@ export function closeStackTrace(id: string) {
|
||||
async function openDialog() {
|
||||
let clip = "";
|
||||
try {
|
||||
clip = (await navigator.clipboard?.readText()) ?? "";
|
||||
clip = await readClipboardPlainText();
|
||||
} catch {
|
||||
// clipboard unavailable or denied: start empty
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ import { usePostfixSettings } from "./postfix/store";
|
||||
import { showChooser, type ChooserItem } from "./popup/store";
|
||||
import { expandEmmetAtCaret } from "./emmet/editor";
|
||||
import { hasEmmet, loadEmmet } from "./emmet/model";
|
||||
import { readClipboardPlainText } from "@/platform/clipboard";
|
||||
|
||||
export interface TemplateDocInfo {
|
||||
path: string;
|
||||
@@ -82,7 +83,7 @@ function userName(): string {
|
||||
|
||||
async function readClipboard(): Promise<string> {
|
||||
try {
|
||||
return (await navigator.clipboard?.readText()) ?? "";
|
||||
return await readClipboardPlainText();
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
||||
import { Terminal } from "@xterm/xterm";
|
||||
import { commands } from "@/platform/ipc";
|
||||
import { usePlatform } from "@/platform/platformInfo";
|
||||
import { installTerminalPaste, pasteTerminalClipboard, type TerminalPasteTarget } from "./paste";
|
||||
|
||||
const notify = vi.hoisted(() => vi.fn());
|
||||
@@ -525,3 +526,28 @@ it("recovers quoted local file paths when clipboard text permission is denied",
|
||||
expect(paste).toHaveBeenCalledExactlyOnceWith("'C:\\a b\\image.png'");
|
||||
expect(notify).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
// WebKitGTK pastes nothing from a menu through execCommand or readText: native text first.
|
||||
{ os: "linux", native: "ls -la", expected: ["ls -la"], execCommand: 0 },
|
||||
// An image-only clipboard keeps the WebView paths.
|
||||
{ os: "linux", native: "", expected: [], execCommand: 1 },
|
||||
{ os: "macos", native: "ls -la", expected: ["ls -la"], execCommand: 1 },
|
||||
] as const)("menu paste on $os with native text '$native'", async ({ os, native, expected, execCommand }) => {
|
||||
usePlatform.setState({ os });
|
||||
vi.spyOn(commands, "clipboardText").mockResolvedValue({ status: "ok", data: native });
|
||||
Object.defineProperty(navigator, "clipboard", { configurable: true, value: { readText: async () => "web" } });
|
||||
const exec = vi.fn(() => false);
|
||||
Object.defineProperty(document, "queryCommandSupported", { configurable: true, value: () => true });
|
||||
Object.defineProperty(document, "execCommand", { configurable: true, value: exec });
|
||||
const paste = vi.fn();
|
||||
try {
|
||||
await pasteTerminalClipboard(paste, () => localTarget());
|
||||
} finally {
|
||||
usePlatform.setState({ os: "macos" });
|
||||
Reflect.deleteProperty(document, "queryCommandSupported");
|
||||
Reflect.deleteProperty(document, "execCommand");
|
||||
}
|
||||
expect(exec).toHaveBeenCalledTimes(execCommand);
|
||||
expect(paste.mock.calls).toEqual(expected.map((text) => [text]));
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { pasteClipboardFiles, readClipboardText } from "@/platform/clipboard";
|
||||
import { pasteClipboardFiles, readClipboardPlainText, readClipboardText } from "@/platform/clipboard";
|
||||
import { call, commands, type TerminalId } from "@/platform/ipc";
|
||||
import { tx } from "@/platform/i18n";
|
||||
import { usePlatform } from "@/platform/platformInfo";
|
||||
import { notifications } from "@/workbench/contributions";
|
||||
|
||||
export interface TerminalPasteTarget {
|
||||
@@ -120,6 +121,15 @@ export function installTerminalPaste(host: HTMLElement, paste: (text: string) =>
|
||||
export async function pasteTerminalClipboard(paste: (text: string) => void, getTarget?: Target) {
|
||||
const target = getTarget?.();
|
||||
try {
|
||||
// WebKitGTK pastes nothing from a menu through either WebView path; the native
|
||||
// clipboard has the text. An image-only clipboard still takes the paths below.
|
||||
if (target && usePlatform.getState().os === "linux") {
|
||||
const text = await readClipboardPlainText().catch(() => "");
|
||||
if (text) {
|
||||
if (target.current()) paste(text);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Native paste preserves clipboard blobs where Clipboard.read() is unavailable.
|
||||
if (target && document.queryCommandSupported?.("paste") && document.execCommand("paste")) return;
|
||||
const images =
|
||||
@@ -130,7 +140,7 @@ export async function pasteTerminalClipboard(paste: (text: string) => void, getT
|
||||
)
|
||||
: null;
|
||||
const text = target?.remote
|
||||
? ((await navigator.clipboard?.readText().catch(() => "")) ?? "")
|
||||
? await readClipboardPlainText().catch(() => "")
|
||||
: await readClipboardText(undefined, target ? (paths) => pathsText(paths, target) : undefined);
|
||||
if (target && !target.current()) return;
|
||||
if (text) {
|
||||
|
||||
@@ -62,6 +62,7 @@ import { refreshNow } from "./store";
|
||||
import styles from "./MergeEditor.module.css";
|
||||
|
||||
import type { MergeData } from "./mergeKind";
|
||||
import { readClipboardPlainText } from "@/platform/clipboard";
|
||||
export { MERGE_KIND, mergeInput, type MergeData } from "./mergeKind";
|
||||
|
||||
const NOTHING_LEFT: MergeSummary = {
|
||||
@@ -497,7 +498,7 @@ export function MergeEditor({ input }: { input: EditorInput }) {
|
||||
const { from, to } = view.state.selection.main;
|
||||
if (id === "selectAll") view.dispatch({ selection: { anchor: 0, head: view.state.doc.length } });
|
||||
else if (id === "paste") {
|
||||
const text = await navigator.clipboard?.readText().catch(() => undefined);
|
||||
const text = await readClipboardPlainText().catch(() => undefined);
|
||||
if (text !== undefined) view.dispatch({ ...view.state.replaceSelection(text), userEvent: "input.paste" });
|
||||
} else {
|
||||
await copyText(view.state.sliceDoc(from, to));
|
||||
|
||||
@@ -14,6 +14,7 @@ export { compareFilesData, displayPath } from "./compareData";
|
||||
import { openDiff, openVcsDiff, type DiffData } from "./diffInput";
|
||||
import { baseName } from "./model";
|
||||
import { tx } from "@/platform/i18n";
|
||||
import { readClipboardPlainText } from "@/platform/clipboard";
|
||||
|
||||
function argPath(arg: unknown): string | undefined {
|
||||
if (typeof arg === "string") return arg;
|
||||
@@ -285,7 +286,7 @@ export function registerCompareActions(): () => void {
|
||||
if (!path) return info("Open or select a file to compare with the clipboard");
|
||||
let clipboard: string;
|
||||
try {
|
||||
clipboard = (await navigator.clipboard?.readText()) ?? "";
|
||||
clipboard = await readClipboardPlainText();
|
||||
} catch {
|
||||
return info("The clipboard could not be read");
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { writeClipboardText } from "@/platform/clipboard";
|
||||
import { readClipboardPlainText, writeClipboardText } from "@/platform/clipboard";
|
||||
import { commitRepo } from "./multiRoot";
|
||||
/** Create Patch… (from local changes or commits) and Apply Patch… (file or clipboard, with a preview). */
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -224,7 +224,7 @@ export async function showApplyPatch(from: "file" | "clipboard", path?: string)
|
||||
let source = "Clipboard";
|
||||
try {
|
||||
if (from === "clipboard") {
|
||||
text = await navigator.clipboard.readText();
|
||||
text = await readClipboardPlainText();
|
||||
} else {
|
||||
let file = path ?? null;
|
||||
if (!file) {
|
||||
|
||||
@@ -7,6 +7,19 @@ export async function writeClipboardText(text: string): Promise<void> {
|
||||
else await navigator.clipboard.writeText(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* The clipboard's plain text, read natively where the backend can (macOS, Linux), else
|
||||
* through the WebView. A Paste from a menu has no paste event to read, and WebKitGTK's
|
||||
* `navigator.clipboard.readText()` gives no text there. Rejects when the WebView refuses.
|
||||
*/
|
||||
export async function readClipboardPlainText(): Promise<string> {
|
||||
if (isTauri()) {
|
||||
const text = await call(commands.clipboardText()).catch(() => null);
|
||||
if (text != null) return text;
|
||||
}
|
||||
return (await navigator.clipboard?.readText()) ?? "";
|
||||
}
|
||||
|
||||
type FormatFiles = (paths: string[]) => string;
|
||||
const rawPaths: FormatFiles = (paths) => paths.join("\n");
|
||||
|
||||
@@ -15,9 +28,7 @@ export async function readClipboardText(
|
||||
transfer?: DataTransfer | null,
|
||||
formatFiles: FormatFiles = rawPaths,
|
||||
): Promise<string> {
|
||||
const text = transfer
|
||||
? transfer.getData("text/plain")
|
||||
: ((await navigator.clipboard?.readText().catch(() => "")) ?? "");
|
||||
const text = transfer ? transfer.getData("text/plain") : await readClipboardPlainText().catch(() => "");
|
||||
if ((text && formatFiles === rawPaths) || (transfer && !hasClipboardFiles(transfer)) || !isTauri()) return text;
|
||||
let paths: string[];
|
||||
try {
|
||||
|
||||
Reference in new issue
Заблокувати користувача