mirror of
https://github.com/zhukunpenglinyutong/jetbrains-cc-gui.git
synced 2026-08-28 16:31:27 +08:00
fix(webview): share path-segment rule to stop truncating @file refs with spaced filenames
Extract looksLikePathSegment into webview/src/utils/pathSegment.ts so the display-side scanner (CollapsibleTextBlock) and the input-side fallback scanner (useFileTags) share one rule: a word after a space continues the path when it contains a path separator or ends with a file extension, with any trailing #L line marker stripped first. PR #1576 applied this rule only to message display; the chat input box still truncated absolute paths whose filenames contain spaces (e.g. "第六章 框架开发实践.md"), and its comment promised extension support the code did not have. Reuse the shared helper there, sync the comments, and move the misplaced convertAtFileRefsToLinks docstring back to its function. Tests: new pathSegment.test.ts unit tests; useFileTags.test.ts gains fallback cases for spaced absolute paths with and without line markers.
This commit is contained in:
@@ -241,4 +241,42 @@ describe('useFileTags', () => {
|
||||
|
||||
expect(editable.querySelectorAll('.file-tag').length).toBe(1);
|
||||
});
|
||||
|
||||
it('renders absolute paths with spaces in the filename (not in path mapping)', () => {
|
||||
const editable = createEditable();
|
||||
editable.textContent = '@D:\\workspace\\docs\\chapter6\\第六章 框架开发实践.md ';
|
||||
mockSelection();
|
||||
|
||||
const { result } = setupHook(editable);
|
||||
|
||||
// No pathMappingRef entry: falls back to absolute-path pattern matching.
|
||||
// The space before "框架开发实践.md" must not truncate the path.
|
||||
result.current.renderFileTags();
|
||||
|
||||
expect(editable.querySelectorAll('.file-tag').length).toBe(1);
|
||||
expect(result.current.extractFileTags()).toEqual([
|
||||
{
|
||||
displayPath: 'D:\\workspace\\docs\\chapter6\\第六章 框架开发实践.md',
|
||||
absolutePath: 'D:\\workspace\\docs\\chapter6\\第六章 框架开发实践.md',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('renders absolute paths with spaces and a line marker (not in path mapping)', () => {
|
||||
const editable = createEditable();
|
||||
editable.textContent = '@D:\\docs\\第六章 框架开发实践.md#L10-20 ';
|
||||
mockSelection();
|
||||
|
||||
const { result } = setupHook(editable);
|
||||
|
||||
result.current.renderFileTags();
|
||||
|
||||
expect(editable.querySelectorAll('.file-tag').length).toBe(1);
|
||||
expect(result.current.extractFileTags()).toEqual([
|
||||
{
|
||||
displayPath: 'D:\\docs\\第六章 框架开发实践.md#L10-20',
|
||||
absolutePath: 'D:\\docs\\第六章 框架开发实践.md#L10-20',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
getVirtualCursorPosition,
|
||||
setVirtualCursorPosition,
|
||||
} from '../utils/virtualCursorUtils.js';
|
||||
import { looksLikePathSegment } from '../../../utils/pathSegment.js';
|
||||
import type { FileTagInfo } from '../types.js';
|
||||
|
||||
interface FileMatch {
|
||||
@@ -172,7 +173,8 @@ export function useFileTags({
|
||||
// Fall back to simple pattern matching for paths not in pathMappingRef.
|
||||
// This handles absolute paths and paths with line numbers.
|
||||
// The helper scans forward allowing spaces when the next segment
|
||||
// looks like a path continuation (contains a path separator: \ or /).
|
||||
// looks like a path continuation (contains a path separator: \ or /,
|
||||
// or ends with a file extension — e.g. a filename with spaces).
|
||||
const remainingText = text.substring(i);
|
||||
const afterAt = remainingText.slice(1); // text after '@'
|
||||
let endPos = 0;
|
||||
@@ -186,7 +188,7 @@ export function useFileTags({
|
||||
if (peekRemainder.length > 0 && peekRemainder[0] !== ' '
|
||||
&& peekRemainder[0] !== '\t' && peekRemainder[0] !== '\n'
|
||||
&& peekRemainder[0] !== '\r' && peekRemainder[0] !== '@'
|
||||
&& /[\\/]/.test(peekRemainder.split(/\s/)[0])
|
||||
&& looksLikePathSegment(peekRemainder.split(/\s/)[0])
|
||||
) {
|
||||
endPos++; // include the space, continue scanning
|
||||
continue;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { openFile } from '../utils/bridge';
|
||||
import { looksLikePathSegment } from '../utils/pathSegment';
|
||||
|
||||
interface CollapsibleTextBlockProps {
|
||||
content: string;
|
||||
@@ -24,31 +25,6 @@ function escapeHtml(text: string): string {
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert @path references into compact clickable `<a>` links at display time.
|
||||
*
|
||||
* Before: "@C:\Users\Bob\proj\src\app.ts#L10-20"
|
||||
* After: "<a href=... data-linkify=file>@app.ts#L10-20</a>"
|
||||
*
|
||||
* The href preserves the full absolute path (with :line format for navigation).
|
||||
* Protocol layer text sent to the AI is unchanged — this transformation is
|
||||
* display-only.
|
||||
*/
|
||||
/**
|
||||
* Whether a word following a space could be a continuation of a file path.
|
||||
* Accepts segments that contain a path separator (`\` or `/`), or that end
|
||||
* with a file extension — the latter covers filenames containing spaces
|
||||
* such as "第六章 框架开发实践.md". A trailing `#L10-20` line marker is
|
||||
* stripped first so the extension is still visible.
|
||||
*/
|
||||
function looksLikePathSegment(segment: string): boolean {
|
||||
const withoutLineMarker = segment.replace(/#L\d+(?:-\d+)?$/, '');
|
||||
return (
|
||||
/[\\/]/.test(withoutLineMarker) || // contains a path separator (dir / absolute segment)
|
||||
/\.[A-Za-z0-9]{1,10}$/.test(withoutLineMarker) // ends with a file extension (filename with spaces)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a file path starting after `@` at position `start` in `text`.
|
||||
* Returns `[fullMatch, filePath, lineStart?, lineEnd?]` or null.
|
||||
@@ -112,7 +88,18 @@ function extractAtFilePath(
|
||||
return { rawPath, lineStart, lineEnd };
|
||||
}
|
||||
|
||||
/** @visibleForTesting */
|
||||
/**
|
||||
* Convert @path references into compact clickable `<a>` links at display time.
|
||||
*
|
||||
* Before: "@C:\Users\Bob\proj\src\app.ts#L10-20"
|
||||
* After: "<a href=... data-linkify=file>@app.ts#L10-20</a>"
|
||||
*
|
||||
* The href preserves the full absolute path (with :line format for navigation).
|
||||
* Protocol layer text sent to the AI is unchanged — this transformation is
|
||||
* display-only.
|
||||
*
|
||||
* @visibleForTesting
|
||||
*/
|
||||
export function convertAtFileRefsToLinks(text: string): string {
|
||||
if (!text || !text.includes('@')) {
|
||||
return escapeHtml(text);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { looksLikePathSegment } from './pathSegment.js';
|
||||
|
||||
describe('looksLikePathSegment', () => {
|
||||
it('accepts segments containing a path separator', () => {
|
||||
expect(looksLikePathSegment('docs\\chapter6')).toBe(true);
|
||||
expect(looksLikePathSegment('docs/chapter6')).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts segments ending with a file extension (filename with spaces)', () => {
|
||||
expect(looksLikePathSegment('框架开发实践.md')).toBe(true);
|
||||
expect(looksLikePathSegment('my file.ts')).toBe(true);
|
||||
});
|
||||
|
||||
it('strips a trailing #L line marker before checking the extension', () => {
|
||||
expect(looksLikePathSegment('框架开发实践.md#L10-20')).toBe(true);
|
||||
expect(looksLikePathSegment('框架开发实践.md#L10')).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects plain words that are not path-like', () => {
|
||||
expect(looksLikePathSegment('and')).toBe(false);
|
||||
expect(looksLikePathSegment('中查看')).toBe(false);
|
||||
expect(looksLikePathSegment('')).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Whether a word following a space could be a continuation of a file path.
|
||||
* Accepts segments that contain a path separator (`\` or `/`), or that end
|
||||
* with a file extension — the latter covers filenames containing spaces
|
||||
* such as "第六章 框架开发实践.md". A trailing `#L10-20` line marker is
|
||||
* stripped first so the extension is still visible.
|
||||
*/
|
||||
export function looksLikePathSegment(segment: string): boolean {
|
||||
const withoutLineMarker = segment.replace(/#L\d+(?:-\d+)?$/, '');
|
||||
return (
|
||||
/[\\/]/.test(withoutLineMarker) || // contains a path separator (dir / absolute segment)
|
||||
/\.[A-Za-z0-9]{1,10}$/.test(withoutLineMarker) // ends with a file extension (filename with spaces)
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user