mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 16:57:46 +00:00
refactor(vscode-ide-companion/qwenAgentManager): remove saveCheckpointViaCommand method
refactor(vscode-ide-companion/webview): improve message handling during checkpoint saves feat(vscode-ide-companion/markdown): enhance file path link handling with line numbers support feat(vscode-ide-companion/message): add enableFileLinks prop to MessageContent component feat(vscode-ide-companion/user-message): disable file links in user messages
This commit is contained in:
@@ -14,6 +14,8 @@ import './MarkdownRenderer.css';
|
||||
interface MarkdownRendererProps {
|
||||
content: string;
|
||||
onFileClick?: (filePath: string) => void;
|
||||
/** When false, do not convert file paths into clickable links. Default: true */
|
||||
enableFileLinks?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,6 +34,7 @@ const FILE_PATH_WITH_LINES_REGEX =
|
||||
export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
|
||||
content,
|
||||
onFileClick,
|
||||
enableFileLinks = true,
|
||||
}) => {
|
||||
/**
|
||||
* Initialize markdown-it with plugins
|
||||
@@ -59,8 +62,10 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
|
||||
// Process the markdown content
|
||||
let html = md.render(content);
|
||||
|
||||
// Post-process to add file path click handlers
|
||||
html = processFilePaths(html);
|
||||
// Post-process to add file path click handlers unless disabled
|
||||
if (enableFileLinks) {
|
||||
html = processFilePaths(html);
|
||||
}
|
||||
|
||||
return html;
|
||||
} catch (error) {
|
||||
@@ -108,20 +113,41 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
|
||||
container.innerHTML = html;
|
||||
|
||||
const union = new RegExp(
|
||||
`${FILE_PATH_WITH_LINES_REGEX.source}|${FILE_PATH_REGEX.source}`,
|
||||
`${FILE_PATH_WITH_LINES_REGEX.source}|${FILE_PATH_REGEX.source}|${BARE_FILE_REGEX.source}`,
|
||||
'gi',
|
||||
);
|
||||
|
||||
// Convert a "path#fragment" into VS Code friendly "path:line" (we only keep the start line)
|
||||
const normalizePathAndLine = (
|
||||
raw: string,
|
||||
): { displayText: string; dataPath: string } => {
|
||||
const displayText = raw;
|
||||
let base = raw;
|
||||
// Extract hash fragment like #12, #L12 or #12-34 and keep only the first number
|
||||
const hashIndex = raw.indexOf('#');
|
||||
if (hashIndex >= 0) {
|
||||
const frag = raw.slice(hashIndex + 1);
|
||||
// Accept L12, 12 or 12-34
|
||||
const m = frag.match(/^L?(\d+)(?:-\d+)?$/i);
|
||||
if (m) {
|
||||
const line = parseInt(m[1], 10);
|
||||
base = raw.slice(0, hashIndex);
|
||||
return { displayText, dataPath: `${base}:${line}` };
|
||||
}
|
||||
}
|
||||
return { displayText, dataPath: base };
|
||||
};
|
||||
|
||||
const makeLink = (text: string) => {
|
||||
const link = document.createElement('a');
|
||||
// Pass base path to the handler; keep the full text as label
|
||||
const filePath = text.split('#')[0];
|
||||
// Pass base path (with optional :line) to the handler; keep the full text as label
|
||||
const { dataPath } = normalizePathAndLine(text);
|
||||
link.className = 'file-path-link';
|
||||
link.textContent = text;
|
||||
link.setAttribute('href', '#');
|
||||
link.setAttribute('title', `Open ${text}`);
|
||||
// Carry file path via data attribute; click handled by event delegation
|
||||
link.setAttribute('data-file-path', filePath);
|
||||
link.setAttribute('data-file-path', dataPath);
|
||||
return link;
|
||||
};
|
||||
|
||||
@@ -129,29 +155,54 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
|
||||
const href = a.getAttribute('href') || '';
|
||||
const text = (a.textContent || '').trim();
|
||||
|
||||
// Helper function to check if a string looks like a code reference
|
||||
// Helper: identify dot-chained code refs (e.g. vscode.commands.register)
|
||||
// but DO NOT treat filenames/paths as code refs.
|
||||
const isCodeReference = (str: string): boolean => {
|
||||
// Check if it looks like a code reference (e.g., module.property)
|
||||
// Patterns like "vscode.contribution", "module.submodule.function"
|
||||
if (BARE_FILE_REGEX.test(str)) {
|
||||
return false; // looks like a filename
|
||||
}
|
||||
if (/[/\\]/.test(str)) {
|
||||
return false; // contains a path separator
|
||||
}
|
||||
const codeRefPattern = /^[a-zA-Z_$][\w$]*(\.[a-zA-Z_$][\w$]*)+$/;
|
||||
return codeRefPattern.test(str);
|
||||
};
|
||||
|
||||
// If linkify turned a bare filename into http://<filename>, convert it back
|
||||
// If linkify turned a bare filename (e.g. README.md) into http://<filename>, convert it back
|
||||
const httpMatch = href.match(/^https?:\/\/(.+)$/i);
|
||||
if (httpMatch && BARE_FILE_REGEX.test(text) && httpMatch[1] === text) {
|
||||
// Skip if it looks like a code reference
|
||||
if (isCodeReference(text)) {
|
||||
return;
|
||||
}
|
||||
if (httpMatch) {
|
||||
try {
|
||||
const url = new URL(href);
|
||||
const host = url.hostname || '';
|
||||
const pathname = url.pathname || '';
|
||||
const noPath = pathname === '' || pathname === '/';
|
||||
|
||||
// Treat as a file link instead of external URL
|
||||
const filePath = text; // no leading slash
|
||||
a.classList.add('file-path-link');
|
||||
a.setAttribute('href', '#');
|
||||
a.setAttribute('title', `Open ${text}`);
|
||||
a.setAttribute('data-file-path', filePath);
|
||||
return;
|
||||
// Case 1: anchor text itself is a bare filename and equals the host (e.g. README.md)
|
||||
if (
|
||||
noPath &&
|
||||
BARE_FILE_REGEX.test(text) &&
|
||||
host.toLowerCase() === text.toLowerCase()
|
||||
) {
|
||||
const { dataPath } = normalizePathAndLine(text);
|
||||
a.classList.add('file-path-link');
|
||||
a.setAttribute('href', '#');
|
||||
a.setAttribute('title', `Open ${text}`);
|
||||
a.setAttribute('data-file-path', dataPath);
|
||||
return;
|
||||
}
|
||||
|
||||
// Case 2: host itself looks like a filename (rare but happens), use it
|
||||
if (noPath && BARE_FILE_REGEX.test(host)) {
|
||||
const { dataPath } = normalizePathAndLine(host);
|
||||
a.classList.add('file-path-link');
|
||||
a.setAttribute('href', '#');
|
||||
a.setAttribute('title', `Open ${text || host}`);
|
||||
a.setAttribute('data-file-path', dataPath);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// fall through; unparseable URL
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore other external protocols
|
||||
@@ -170,18 +221,33 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
|
||||
FILE_PATH_WITH_LINES_NO_G.test(candidate) ||
|
||||
FILE_PATH_NO_G.test(candidate)
|
||||
) {
|
||||
const filePath = candidate.split('#')[0];
|
||||
const { dataPath } = normalizePathAndLine(candidate);
|
||||
a.classList.add('file-path-link');
|
||||
a.setAttribute('href', '#');
|
||||
a.setAttribute('title', `Open ${text || href}`);
|
||||
a.setAttribute('data-file-path', filePath);
|
||||
a.setAttribute('data-file-path', dataPath);
|
||||
return;
|
||||
}
|
||||
|
||||
// Bare file name or relative path (e.g. README.md or docs/README.md)
|
||||
if (BARE_FILE_REGEX.test(candidate)) {
|
||||
const { dataPath } = normalizePathAndLine(candidate);
|
||||
a.classList.add('file-path-link');
|
||||
a.setAttribute('href', '#');
|
||||
a.setAttribute('title', `Open ${text || href}`);
|
||||
a.setAttribute('data-file-path', dataPath);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to check if a string looks like a code reference
|
||||
// Helper: identify dot-chained code refs (e.g. vscode.commands.register)
|
||||
// but DO NOT treat filenames/paths as code refs.
|
||||
const isCodeReference = (str: string): boolean => {
|
||||
// Check if it looks like a code reference (e.g., module.property)
|
||||
// Patterns like "vscode.contribution", "module.submodule.function"
|
||||
if (BARE_FILE_REGEX.test(str)) {
|
||||
return false; // looks like a filename
|
||||
}
|
||||
if (/[/\\]/.test(str)) {
|
||||
return false; // contains a path separator
|
||||
}
|
||||
const codeRefPattern = /^[a-zA-Z_$][\w$]*(\.[a-zA-Z_$][\w$]*)+$/;
|
||||
return codeRefPattern.test(str);
|
||||
};
|
||||
@@ -194,6 +260,11 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
|
||||
upgradeAnchorIfFilePath(el as HTMLAnchorElement);
|
||||
return; // Don't descend into <a>
|
||||
}
|
||||
// Avoid transforming inside code/pre blocks
|
||||
const tag = el.tagName.toLowerCase();
|
||||
if (tag === 'code' || tag === 'pre') {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (let child = node.firstChild; child; ) {
|
||||
@@ -252,6 +323,10 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
|
||||
const handleContainerClick = (
|
||||
e: React.MouseEvent<HTMLDivElement, MouseEvent>,
|
||||
) => {
|
||||
// If file links disabled, do nothing
|
||||
if (!enableFileLinks) {
|
||||
return;
|
||||
}
|
||||
const target = e.target as HTMLElement | null;
|
||||
if (!target) {
|
||||
return;
|
||||
@@ -260,18 +335,46 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
|
||||
// Find nearest anchor with our marker class
|
||||
const anchor = (target.closest &&
|
||||
target.closest('a.file-path-link')) as HTMLAnchorElement | null;
|
||||
if (!anchor) {
|
||||
if (anchor) {
|
||||
const filePath = anchor.getAttribute('data-file-path');
|
||||
if (!filePath) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onFileClick?.(filePath);
|
||||
return;
|
||||
}
|
||||
|
||||
const filePath = anchor.getAttribute('data-file-path');
|
||||
if (!filePath) {
|
||||
// Fallback: intercept "http://README.md" style links that slipped through
|
||||
const anyAnchor = (target.closest &&
|
||||
target.closest('a')) as HTMLAnchorElement | null;
|
||||
if (!anyAnchor) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onFileClick?.(filePath);
|
||||
const href = anyAnchor.getAttribute('href') || '';
|
||||
if (!/^https?:\/\//i.test(href)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const url = new URL(href);
|
||||
const host = url.hostname || '';
|
||||
const path = url.pathname || '';
|
||||
const noPath = path === '' || path === '/';
|
||||
|
||||
// Basic bare filename heuristic on the host part (e.g. README.md)
|
||||
if (noPath && /\.[a-z0-9]+$/i.test(host)) {
|
||||
// Prefer the readable text content if it looks like a file
|
||||
const text = (anyAnchor.textContent || '').trim();
|
||||
const candidate = /\.[a-z0-9]+$/i.test(text) ? text : host;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onFileClick?.(candidate);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user