дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
fix(ui): Make read-only content selectable with a Copy context menu (#48)
Root cause: body is user-select: none and each feature had to opt back in; the Markdown preview (and other read-only views) never did, and the app-wide contextmenu guard left no Copy item. Fix: shared selectableText class (src/ui) for read-only content, and a fallback Copy context menu from browserGuards when right-clicking a selection or a selectable area (component menus still win). Siblings covered: Quick Documentation / hover / completion docs, hover errors, Quick Definition, debugger value tooltip, inspection messages, message dialogs (no drag on body text), notification titles, About dialog; existing selectable consoles now get Copy too. Verified in-app on macOS: select in Markdown preview, right-click Copy puts the text on the clipboard; tab context menu unchanged. Claude-Session: https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
Цей коміт міститься в:
1 джерело
6dfd82de4c
коміт
de84a643aa
17 files changed
+453
-55
No files matched your search
@@ -14,6 +14,7 @@ import {
|
||||
SymbolBadge,
|
||||
fileIconName,
|
||||
fuzzyMatch,
|
||||
selectableText,
|
||||
T,
|
||||
} from "@/ui";
|
||||
import { basename, relativePath, useProject } from "@/platform/project";
|
||||
@@ -329,7 +330,7 @@ function DocView({ host, history, themeClasses }: { host: DocHost; history: DocH
|
||||
{entry && (
|
||||
<DomContent
|
||||
key={`${history.index}:${history.entries.length}`}
|
||||
className={styles.docBody}
|
||||
className={clsx(styles.docBody, selectableText)}
|
||||
build={() => renderMarkup(entry.markup, entry.language, docLinkHandler(host))}
|
||||
/>
|
||||
)}
|
||||
@@ -352,7 +353,13 @@ function DocView({ host, history, themeClasses }: { host: DocHost; history: DocH
|
||||
/>
|
||||
<span className={styles.spacer} />
|
||||
{entry?.source && (
|
||||
<IconButton icon="edit" size="small" title="Edit Source" shortcut="F4" onClick={() => void editDocSource(host)} />
|
||||
<IconButton
|
||||
icon="edit"
|
||||
size="small"
|
||||
title="Edit Source"
|
||||
shortcut="F4"
|
||||
onClick={() => void editDocSource(host)}
|
||||
/>
|
||||
)}
|
||||
{host === "popup" && (
|
||||
<IconButton
|
||||
@@ -373,7 +380,8 @@ function DocPopup({ popup }: { popup: Extract<LspPopup, { kind: "doc" }> }) {
|
||||
const scope = useRef<HTMLDivElement>(null);
|
||||
const opened = useRef(focusSeq);
|
||||
useEffect(() => {
|
||||
if (focusSeq !== opened.current) scope.current?.querySelector<HTMLElement>("[data-lsp-doc]")?.focus({ preventScroll: true });
|
||||
if (focusSeq !== opened.current)
|
||||
scope.current?.querySelector<HTMLElement>("[data-lsp-doc]")?.focus({ preventScroll: true });
|
||||
}, [focusSeq]);
|
||||
return (
|
||||
<Popup
|
||||
@@ -463,7 +471,7 @@ function DefinitionPopup({ popup }: { popup: Extract<LspPopup, { kind: "definiti
|
||||
<div className={clsx(popup.themeClasses, styles.docScope)}>
|
||||
<DomContent
|
||||
key={popup.index}
|
||||
className={styles.defBody}
|
||||
className={clsx(styles.defBody, selectableText)}
|
||||
build={() => renderCode(target.text, target.language, "cm-lsp-code")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -25,13 +25,22 @@ import {
|
||||
} from "@codemirror/autocomplete";
|
||||
import { EditorSelection, Prec, type Text } from "@codemirror/state";
|
||||
import { EditorView, ViewPlugin, keymap, type ViewUpdate } from "@codemirror/view";
|
||||
import { call, commands, isTauri, nextRequestId, type CompletionItem, type Markup, type RequestId } from "@/platform/ipc";
|
||||
import {
|
||||
call,
|
||||
commands,
|
||||
isTauri,
|
||||
nextRequestId,
|
||||
type CompletionItem,
|
||||
type Markup,
|
||||
type RequestId,
|
||||
} from "@/platform/ipc";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { completionSymbolKind, identifierEnd, toCmCompletion, triggerCharacter } from "./completionItems";
|
||||
import { lspSnippetToCm, snippetVariables } from "./snippet";
|
||||
import { docOf, isUnavailable, lspDoc, lspRequest, reportLspError } from "./lsp";
|
||||
import { offsetToPos, posToOffset, textEditsToChanges } from "./positions";
|
||||
import { isEmptyMarkup, renderCode, renderMarkup } from "./markdown";
|
||||
import { selectableText } from "@/ui/selectableText";
|
||||
import { iconNode } from "./iconMarkup";
|
||||
import { triggerSignatureHelp } from "./signature";
|
||||
import { serverBackoff } from "./availability";
|
||||
@@ -101,7 +110,7 @@ function buildInfo(c: LspCompletion, delay: number): Promise<CompletionInfo> {
|
||||
const hasDoc = !isEmptyMarkup(item.documentation);
|
||||
if (!hasDoc && !item.detail) return null;
|
||||
const dom = document.createElement("div");
|
||||
dom.className = "cm-lsp-completion-doc";
|
||||
dom.className = `cm-lsp-completion-doc ${selectableText}`;
|
||||
if (item.detail) dom.appendChild(renderCode(item.detail, c.req.language, "cm-lsp-code cm-lsp-doc-signature"));
|
||||
if (hasDoc) dom.appendChild(renderMarkup(item.documentation!, c.req.language));
|
||||
return dom;
|
||||
@@ -160,7 +169,8 @@ export function applyLspCompletion(
|
||||
const item = completion.resolved ?? completion.lsp;
|
||||
const state = view.state;
|
||||
const delta = to - req.pos;
|
||||
const mapOld = (offset: number) => Math.max(0, Math.min(state.doc.length, offset < req.pos ? offset : offset + delta));
|
||||
const mapOld = (offset: number) =>
|
||||
Math.max(0, Math.min(state.doc.length, offset < req.pos ? offset : offset + delta));
|
||||
|
||||
let start = from;
|
||||
let end = to;
|
||||
@@ -190,7 +200,9 @@ export function applyLspCompletion(
|
||||
}
|
||||
snippet(lspSnippetToCm(item.insertText, snippetVariables(req.path)))(view, completion, start, end);
|
||||
} else {
|
||||
const parens = editorPrefs().completion.insertParentheses ? callParens(item, state.doc.lineAt(start), start, end) : null;
|
||||
const parens = editorPrefs().completion.insertParentheses
|
||||
? callParens(item, state.doc.lineAt(start), start, end)
|
||||
: null;
|
||||
insertedParens = parens;
|
||||
const changes = state.changes([...extra, { from: start, to: end, insert: item.insertText + (parens ? "()" : "") }]);
|
||||
const caret = changes.mapPos(end, 1) - (parens === "inside" ? 1 : 0);
|
||||
@@ -203,7 +215,8 @@ export function applyLspCompletion(
|
||||
});
|
||||
}
|
||||
|
||||
if (!completion.resolved && !item.additionalEdits.length && item.data != null) void applyLateEdits(view, completion, start);
|
||||
if (!completion.resolved && !item.additionalEdits.length && item.data != null)
|
||||
void applyLateEdits(view, completion, start);
|
||||
if (insertedParens === "inside" && !item.command) setTimeout(() => triggerSignatureHelp(view), 0);
|
||||
runCommand(view, item);
|
||||
}
|
||||
@@ -240,7 +253,9 @@ export function callParens(
|
||||
async function applyLateEdits(view: EditorView, c: LspCompletion, insertedAt: number) {
|
||||
const item = await resolveItem(c.req.doc, c.lsp);
|
||||
if (!item.additionalEdits.length || !view.dom.isConnected) return;
|
||||
const edits = textEditsToChanges(c.req.text, item.additionalEdits).filter((e) => e.to <= Math.min(insertedAt, c.req.pos));
|
||||
const edits = textEditsToChanges(c.req.text, item.additionalEdits).filter(
|
||||
(e) => e.to <= Math.min(insertedAt, c.req.pos),
|
||||
);
|
||||
if (!edits.length) return;
|
||||
const limit = Math.max(...edits.map((e) => e.to));
|
||||
if (view.state.doc.sliceString(0, limit) !== c.req.text.sliceString(0, limit)) return;
|
||||
@@ -259,7 +274,10 @@ function runCommand(view: EditorView, item: CompletionItem) {
|
||||
else if (!CLIENT_COMMANDS.has(cmd) && !cmd.startsWith("editor.")) {
|
||||
const doc = docOf(view);
|
||||
// Server commands (auto-import, postfix…); resulting edits come back as workspace/applyEdit.
|
||||
if (doc) void lspRequest(() => commands.lspExecuteCommand(doc.doc, command)).catch((e: unknown) => reportLspError(view, e, false));
|
||||
if (doc)
|
||||
void lspRequest(() => commands.lspExecuteCommand(doc.doc, command)).catch((e: unknown) =>
|
||||
reportLspError(view, e, false),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,7 +296,10 @@ function cancelInflight() {
|
||||
|
||||
/** Language-data completion sources (keywords etc.), used when no server answers. */
|
||||
async function fallback(ctx: CompletionContext): Promise<CompletionResult | null> {
|
||||
const sources = ctx.state.languageDataAt<CompletionSource | readonly (string | Completion)[]>("autocomplete", ctx.pos);
|
||||
const sources = ctx.state.languageDataAt<CompletionSource | readonly (string | Completion)[]>(
|
||||
"autocomplete",
|
||||
ctx.pos,
|
||||
);
|
||||
for (const s of sources) {
|
||||
const source = typeof s === "function" ? s : completeFromList(s);
|
||||
const r = await source(ctx);
|
||||
@@ -317,7 +338,9 @@ export function mergeResults(results: readonly CompletionResult[]): CompletionRe
|
||||
const [first] = results;
|
||||
if (!first || results.length === 1) return first ?? null;
|
||||
const same = results.filter((r) => r.from === first.from);
|
||||
const validFor = same.every((r) => r.validFor instanceof RegExp && r.validFor.source === (first.validFor as RegExp | undefined)?.source)
|
||||
const validFor = same.every(
|
||||
(r) => r.validFor instanceof RegExp && r.validFor.source === (first.validFor as RegExp | undefined)?.source,
|
||||
)
|
||||
? first.validFor
|
||||
: undefined;
|
||||
return { from: first.from, to: first.to, options: same.flatMap((r) => r.options), validFor };
|
||||
@@ -360,7 +383,9 @@ async function lspCompletionSource(ctx: CompletionContext): Promise<CompletionRe
|
||||
const req: RequestContext = { doc: doc.doc, path: doc.path, language: doc.uiMode, pos: ctx.pos, text: ctx.state.doc };
|
||||
const prefs = editorPrefs().completion;
|
||||
const typed = word?.text ?? "";
|
||||
const items = prefs.matchCase ? list.items.filter((it) => matchesCase(it.filterText ?? it.label, typed, prefs.caseSensitivity)) : list.items;
|
||||
const items = prefs.matchCase
|
||||
? list.items.filter((it) => matchesCase(it.filterText ?? it.label, typed, prefs.caseSensitivity))
|
||||
: list.items;
|
||||
return {
|
||||
from: word ? word.from : ctx.pos,
|
||||
options: items.map((item) => makeCompletion(item, req)),
|
||||
@@ -396,7 +421,10 @@ export function sortAlphabetically(result: CompletionResult, typed: string): Com
|
||||
update: validFor
|
||||
? (_cur, from, to, ctx) => {
|
||||
const text = ctx.state.sliceDoc(from, to);
|
||||
const ok = typeof validFor === "function" ? validFor(text, from, to, ctx.state) : new RegExp(`^(?:${validFor.source})$`, validFor.flags).test(text);
|
||||
const ok =
|
||||
typeof validFor === "function"
|
||||
? validFor(text, from, to, ctx.state)
|
||||
: new RegExp(`^(?:${validFor.source})$`, validFor.flags).test(text);
|
||||
return ok ? sortAlphabetically({ ...result, from }, text) : null;
|
||||
}
|
||||
: undefined,
|
||||
@@ -455,7 +483,9 @@ function gated(source: CompletionSource): CompletionSource {
|
||||
if (only) return source === contributedSource ? only(ctx) : null;
|
||||
if (!ctx.explicit && (!editorPrefs().completion.autoPopup || inplaceRenameActive(ctx.state))) return null;
|
||||
if (!editorPrefs().completion.sortAlphabetically) return source(ctx);
|
||||
return Promise.resolve(source(ctx)).then((r) => r && sortAlphabetically(r, ctx.state.sliceDoc(r.from, r.to ?? ctx.pos)));
|
||||
return Promise.resolve(source(ctx)).then(
|
||||
(r) => r && sortAlphabetically(r, ctx.state.sliceDoc(r.from, r.to ?? ctx.pos)),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -478,7 +508,12 @@ const explicitPending = new WeakSet<EditorView>();
|
||||
const NAVIGATION_KEYS = new Set(["ArrowUp", "ArrowDown", "PageUp", "PageDown"]);
|
||||
|
||||
/** Whether typing `ch` finishes the open lookup with its selected item. */
|
||||
export function commitsLookup(ch: string, focused: boolean, insertOnSpecialKeys: boolean, itemChars: readonly string[] = []): boolean {
|
||||
export function commitsLookup(
|
||||
ch: string,
|
||||
focused: boolean,
|
||||
insertOnSpecialKeys: boolean,
|
||||
itemChars: readonly string[] = [],
|
||||
): boolean {
|
||||
if (!focused && !insertOnSpecialKeys) return false;
|
||||
return COMMIT_CHARS.includes(ch) || itemChars.includes(ch);
|
||||
}
|
||||
@@ -495,11 +530,15 @@ const lookupKeys = Prec.highest(
|
||||
const selected = selectedCompletion(view.state);
|
||||
if (!selected) return false;
|
||||
const itemChars = isLsp(selected) ? selected.lsp.commitCharacters : (selected.commitCharacters ?? []);
|
||||
if (!commitsLookup(e.key, focusedLookups.has(view), editorPrefs().completion.insertOnSpecialKeys, itemChars)) return false;
|
||||
if (!commitsLookup(e.key, focusedLookups.has(view), editorPrefs().completion.insertOnSpecialKeys, itemChars))
|
||||
return false;
|
||||
if (!acceptCompletion(view)) return false;
|
||||
// A call snippet or inserted parentheses already opened the call: the typed one is consumed.
|
||||
const head = view.state.selection.main.head;
|
||||
if (e.key === "(" && (view.state.sliceDoc(head - 1, head) === "(" || view.state.sliceDoc(head - 2, head) === "()")) {
|
||||
if (
|
||||
e.key === "(" &&
|
||||
(view.state.sliceDoc(head - 1, head) === "(" || view.state.sliceDoc(head - 2, head) === "()")
|
||||
) {
|
||||
e.preventDefault();
|
||||
return true;
|
||||
}
|
||||
@@ -526,7 +565,8 @@ const lookupTracker = ViewPlugin.fromClass(
|
||||
resetLookupDocs();
|
||||
if (explicitPending.has(view)) {
|
||||
explicitPending.delete(view);
|
||||
if (prev === "pending" && !u.docChanged) queueMicrotask(() => showHint(view, "No suggestions", { timeout: 2000 }));
|
||||
if (prev === "pending" && !u.docChanged)
|
||||
queueMicrotask(() => showHint(view, "No suggestions", { timeout: 2000 }));
|
||||
}
|
||||
} else if (status === "active" && explicitPending.has(view)) {
|
||||
explicitPending.delete(view);
|
||||
@@ -535,7 +575,12 @@ const lookupTracker = ViewPlugin.fromClass(
|
||||
const doc = u.state.doc;
|
||||
setTimeout(() => {
|
||||
const now = currentCompletions(view.state);
|
||||
if (view.state.doc === doc && completionStatus(view.state) === "active" && now.length === 1 && now[0] === options[0])
|
||||
if (
|
||||
view.state.doc === doc &&
|
||||
completionStatus(view.state) === "active" &&
|
||||
now.length === 1 &&
|
||||
now[0] === options[0]
|
||||
)
|
||||
acceptCompletion(view);
|
||||
}, AUTO_INSERT_DELAY);
|
||||
}
|
||||
@@ -583,7 +628,9 @@ function typeColumn(c: Completion): Node | null {
|
||||
async function toggleSortByName(view: EditorView) {
|
||||
const on = !editorPrefs().completion.sortAlphabetically;
|
||||
if (isTauri()) {
|
||||
const settings = await call(commands.settingsSet("global", "editor.completion.sortAlphabetically", JSON.stringify(on)));
|
||||
const settings = await call(
|
||||
commands.settingsSet("global", "editor.completion.sortAlphabetically", JSON.stringify(on)),
|
||||
);
|
||||
useProject.setState({ settings });
|
||||
}
|
||||
if (!view.dom.isConnected) return;
|
||||
@@ -647,7 +694,9 @@ export function completionExtension() {
|
||||
optionClass: (c) => {
|
||||
// IDEA shows keywords in bold.
|
||||
const keyword = (isLsp(c) ? c.lsp.kind : c.type) === "keyword" ? " cm-lsp-keyword-option" : "";
|
||||
return (isLsp(c) ? (c.lsp.deprecated ? "cm-lsp-option cm-lsp-deprecated-option" : "cm-lsp-option") : "") + keyword;
|
||||
return (
|
||||
(isLsp(c) ? (c.lsp.deprecated ? "cm-lsp-option cm-lsp-deprecated-option" : "cm-lsp-option") : "") + keyword
|
||||
);
|
||||
},
|
||||
tooltipClass: () => "cm-lsp-completion",
|
||||
}),
|
||||
|
||||
@@ -5,11 +5,21 @@
|
||||
*/
|
||||
import { EditorSelection } from "@codemirror/state";
|
||||
import { hoverTooltip, type EditorView } from "@codemirror/view";
|
||||
import { commands, isTauri, nextRequestId, type Hover, type Location, type Pos, type Range, type RequestId } from "@/platform/ipc";
|
||||
import {
|
||||
commands,
|
||||
isTauri,
|
||||
nextRequestId,
|
||||
type Hover,
|
||||
type Location,
|
||||
type Pos,
|
||||
type Range,
|
||||
type RequestId,
|
||||
} from "@/platform/ipc";
|
||||
import { executeAction } from "@/platform/actions";
|
||||
import { shortcutLabel } from "@/platform/keymap";
|
||||
import { diagnosticsAt, type PlacedDiagnostic } from "./diagnostics";
|
||||
import { isEmptyMarkup, languageForPath, renderMarkup } from "./markdown";
|
||||
import { selectableText } from "@/ui/selectableText";
|
||||
import { docOf, lspRequest, reportLspError } from "./lsp";
|
||||
import { offsetToPos, rangeToOffsets } from "./positions";
|
||||
import { showHint } from "./hint";
|
||||
@@ -51,7 +61,13 @@ function renderDiagnostics(view: EditorView, diags: PlacedDiagnostic[]): HTMLEle
|
||||
for (const { diagnostic: d, from } of diags) {
|
||||
const row = document.createElement("div");
|
||||
row.className = `cm-lsp-diag cm-lsp-diag-${d.severity}`;
|
||||
row.appendChild(iconNode(d.severity === "error" ? "error" : d.severity === "warning" ? "warning" : "info", 16, "cm-lsp-diag-icon"));
|
||||
row.appendChild(
|
||||
iconNode(
|
||||
d.severity === "error" ? "error" : d.severity === "warning" ? "warning" : "info",
|
||||
16,
|
||||
"cm-lsp-diag-icon",
|
||||
),
|
||||
);
|
||||
const msg = document.createElement("div");
|
||||
msg.className = "cm-lsp-diag-message";
|
||||
msg.textContent = d.message;
|
||||
@@ -110,7 +126,7 @@ export function hoverExtension() {
|
||||
above: true,
|
||||
create: () => {
|
||||
const dom = document.createElement("div");
|
||||
dom.className = "cm-lsp-hover";
|
||||
dom.className = `cm-lsp-hover ${selectableText}`;
|
||||
if (diags.length) dom.appendChild(renderDiagnostics(view, diags));
|
||||
if (hover) dom.appendChild(renderMarkup(hover.contents, lang));
|
||||
return { dom };
|
||||
@@ -141,7 +157,13 @@ function showDoc(view: EditorView, entry: DocEntry, at: number) {
|
||||
showInDocWindow(entry, view.themeClasses);
|
||||
return;
|
||||
}
|
||||
openPopup({ kind: "doc", anchor: caretAnchor(view, at), entries: [entry], index: 0, themeClasses: view.themeClasses });
|
||||
openPopup({
|
||||
kind: "doc",
|
||||
anchor: caretAnchor(view, at),
|
||||
entries: [entry],
|
||||
index: 0,
|
||||
themeClasses: view.themeClasses,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -210,7 +232,10 @@ export function definitionSpan(
|
||||
|
||||
async function declarationRange(loc: Location): Promise<Range | null> {
|
||||
try {
|
||||
return await withDocument(loc.path, async (doc) => (await structure.structuralRanges(doc, loc.range.start)).declaration?.range ?? null);
|
||||
return await withDocument(
|
||||
loc.path,
|
||||
async (doc) => (await structure.structuralRanges(doc, loc.range.start)).declaration?.range ?? null,
|
||||
);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -249,14 +274,20 @@ export async function quickDefinition(view: EditorView, kind: "definition" | "ty
|
||||
showHint(view, kind === "typeDefinition" ? "No type definition found" : "No definition found");
|
||||
return;
|
||||
}
|
||||
const targets = (await Promise.all(locations.slice(0, MAX_TARGETS).map((l) => definitionTarget(l, doc.uiMode)))).filter(
|
||||
(t): t is DefinitionTarget => !!t,
|
||||
);
|
||||
const targets = (
|
||||
await Promise.all(locations.slice(0, MAX_TARGETS).map((l) => definitionTarget(l, doc.uiMode)))
|
||||
).filter((t): t is DefinitionTarget => !!t);
|
||||
if (!targets.length) {
|
||||
showHint(view, "Cannot read the definition's file");
|
||||
return;
|
||||
}
|
||||
openPopup({ kind: "definition", anchor: caretAnchor(view, pos), targets, index: 0, themeClasses: view.themeClasses });
|
||||
openPopup({
|
||||
kind: "definition",
|
||||
anchor: caretAnchor(view, pos),
|
||||
targets,
|
||||
index: 0,
|
||||
themeClasses: view.themeClasses,
|
||||
});
|
||||
} catch (e) {
|
||||
reportLspError(view, e, true);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import type { Markup } from "@/bindings";
|
||||
import { maybeEditorService } from "@/workbench/contributions";
|
||||
import { isTauri } from "@/platform/ipc";
|
||||
import { isVirtualUri } from "@/platform/virtualDocs";
|
||||
import { selectableText } from "@/ui/selectableText";
|
||||
|
||||
type Parser = typeof import("./markdownParser");
|
||||
|
||||
@@ -71,7 +72,10 @@ export function isEmptyMarkup(m: Markup | null | undefined): boolean {
|
||||
/**
|
||||
* The declaration line(s) of hover documentation, for the brief ⌘-hover info: the first fenced code block, else the first paragraph.
|
||||
*/
|
||||
export function declarationSummary(m: Markup, maxLines = 6): { code: string; language: string | null } | { text: string } | null {
|
||||
export function declarationSummary(
|
||||
m: Markup,
|
||||
maxLines = 6,
|
||||
): { code: string; language: string | null } | { text: string } | null {
|
||||
if (m.kind === "markdown") {
|
||||
const fence = /^\s*(```|~~~)[ \t]*([\w+#.-]*)[^\n]*\n([\s\S]*?)\n\s*\1/m.exec(m.value);
|
||||
if (fence && fence[3]!.trim()) {
|
||||
@@ -80,7 +84,10 @@ export function declarationSummary(m: Markup, maxLines = 6): { code: string; lan
|
||||
return { code, language: fence[2] || null };
|
||||
}
|
||||
}
|
||||
const para = m.value.trim().split(/\n\s*\n/)[0]?.trim();
|
||||
const para = m.value
|
||||
.trim()
|
||||
.split(/\n\s*\n/)[0]
|
||||
?.trim();
|
||||
if (!para) return null;
|
||||
return { text: para.length > 200 ? `${para.slice(0, 199)}…` : para };
|
||||
}
|
||||
@@ -187,7 +194,10 @@ export function sourceLink(href: string): { path: string; line: number; col: num
|
||||
export function openDocLink(href: string) {
|
||||
const target = sourceLink(href);
|
||||
if (target) {
|
||||
void maybeEditorService()?.openFile(target.path, { selection: { line: target.line, col: target.col }, focus: true });
|
||||
void maybeEditorService()?.openFile(target.path, {
|
||||
selection: { line: target.line, col: target.col },
|
||||
focus: true,
|
||||
});
|
||||
} else if (/^https?:\/\//.test(href) && isTauri()) {
|
||||
void import("@tauri-apps/plugin-opener").then((m) => m.openUrl(href)).catch(() => {});
|
||||
}
|
||||
@@ -210,7 +220,7 @@ function linkClickHandler(onLink?: DocLinkHandler) {
|
||||
/** Renders markup into a `.cm-lsp-markdown` element. */
|
||||
export function renderMarkup(m: Markup, fallbackLanguage: string | null = null, onLink?: DocLinkHandler): HTMLElement {
|
||||
const dom = document.createElement("div");
|
||||
dom.className = "cm-lsp-markdown";
|
||||
dom.className = `cm-lsp-markdown ${selectableText}`;
|
||||
dom.innerHTML = markupToHtml(m);
|
||||
highlightBlocks(dom, fallbackLanguage);
|
||||
if (m.kind === "markdown" && !parser) {
|
||||
@@ -228,7 +238,7 @@ export function renderMarkup(m: Markup, fallbackLanguage: string | null = null,
|
||||
/** Renders a code snippet (e.g. a signature or definition) as highlighted mono text. */
|
||||
export function renderCode(text: string, language: string | null, className = "cm-lsp-code"): HTMLElement {
|
||||
const pre = document.createElement("pre");
|
||||
pre.className = className;
|
||||
pre.className = `${className} ${selectableText}`;
|
||||
const code = document.createElement("code");
|
||||
code.textContent = text;
|
||||
pre.appendChild(code);
|
||||
|
||||
Reference in new issue
Заблокувати користувача