fix(coding): preserve conflict and unicode search results

This commit is contained in:
2026-08-23 18:31:53 +08:00
parent 9520872d2f
commit 365c2b0f76
6 changed files with 152 additions and 19 deletions

View File

@@ -101,8 +101,13 @@ function normalizeRelativePath(value: string): string {
return normalized;
}
function statusFromSignature(signature: string, renamed: boolean): ConversationChangedFileStatus {
function statusFromSignature(
signature: string,
renamed: boolean,
conflicted = false,
): ConversationChangedFileStatus {
if (signature === '??') return 'untracked';
if (conflicted) return 'conflicted';
if (renamed || signature.includes('R')) return 'renamed';
if (signature.includes('D')) return 'deleted';
if (signature.includes('A')) return 'added';
@@ -121,7 +126,10 @@ function parsePorcelainV2(value: string): StatusEntry[] {
continue;
}
const renamed = record.startsWith('2 ');
const match = renamed
const conflicted = record.startsWith('u ');
const match = conflicted
? record.match(/^u ([^ ]+) [^ ]+ [^ ]+ [^ ]+ [^ ]+ [^ ]+ [^ ]+ [^ ]+ [^ ]+ (.*)$/s)
: renamed
? record.match(/^2 ([^ ]+) [^ ]+ [^ ]+ [^ ]+ [^ ]+ [^ ]+ [^ ]+ [^ ]+ (.*)$/s)
: record.match(/^1 ([^ ]+) [^ ]+ [^ ]+ [^ ]+ [^ ]+ [^ ]+ [^ ]+ (.*)$/s);
if (!match) continue;
@@ -129,7 +137,7 @@ function parsePorcelainV2(value: string): StatusEntry[] {
const filePath = normalizeRelativePath(match[2]);
entries.push({
path: filePath,
status: statusFromSignature(signature, renamed),
status: statusFromSignature(signature, renamed, conflicted),
signature,
});
if (renamed) index += 1;

View File

@@ -66,8 +66,13 @@ async function containedExistingTarget(projectPath: string, relativePath: string
return target;
}
function statusFromSignature(signature: string, renamed: boolean): CodingProjectFileStatus {
function statusFromSignature(
signature: string,
renamed: boolean,
conflicted = false,
): CodingProjectFileStatus {
if (signature === '??') return 'untracked';
if (conflicted) return 'conflicted';
if (renamed || signature.includes('R')) return 'renamed';
if (signature.includes('D')) return 'deleted';
if (signature.includes('A')) return 'added';
@@ -87,12 +92,15 @@ function parseStatus(value: string): CodingProjectFileEntry[] {
status = 'untracked';
} else {
const renamed = record.startsWith('2 ');
const match = renamed
const conflicted = record.startsWith('u ');
const match = conflicted
? record.match(/^u ([^ ]+) [^ ]+ [^ ]+ [^ ]+ [^ ]+ [^ ]+ [^ ]+ [^ ]+ [^ ]+ (.*)$/s)
: renamed
? record.match(/^2 ([^ ]+) [^ ]+ [^ ]+ [^ ]+ [^ ]+ [^ ]+ [^ ]+ [^ ]+ (.*)$/s)
: record.match(/^1 ([^ ]+) [^ ]+ [^ ]+ [^ ]+ [^ ]+ [^ ]+ [^ ]+ (.*)$/s);
if (!match) continue;
filePath = match[2];
status = statusFromSignature(match[1], renamed);
status = statusFromSignature(match[1], renamed, conflicted);
if (renamed) index += 1;
}
const normalized = normalizeRelativePath(filePath);
@@ -141,6 +149,30 @@ function decodeText(data: Buffer, allowIncompleteSuffix = false): string {
}
}
function foldedTextWithOffsets(value: string): {
text: string;
starts: number[];
ends: number[];
} {
let text = '';
const starts: number[] = [];
const ends: number[] = [];
for (let index = 0; index < value.length;) {
const codePoint = value.codePointAt(index);
if (codePoint === undefined) break;
const character = String.fromCodePoint(codePoint);
const end = index + character.length;
const folded = character.toLowerCase();
text += folded;
for (let foldedIndex = 0; foldedIndex < folded.length; foldedIndex += 1) {
starts.push(index);
ends.push(end);
}
index = end;
}
return { text, starts, ends };
}
async function fallbackFileList(projectPath: string): Promise<string[]> {
const root = path.resolve(projectPath);
const files: string[] = [];
@@ -243,7 +275,7 @@ export class CodingProjectFileService {
const needle = pattern.trim();
if (!needle) throw new Error('Search pattern is required');
if (needle.length > 512) throw new Error('Search pattern is too long');
const foldedNeedle = needle.toLocaleLowerCase();
const foldedNeedle = foldedTextWithOffsets(needle).text;
const matches: CodingTextSearchResult[] = [];
for (const filePath of await this.listFiles(projectPath)) {
if (matches.length >= MAX_SEARCH_RESULTS) break;
@@ -258,20 +290,24 @@ export class CodingProjectFileService {
const lines = content.split(/\r?\n/);
for (let lineIndex = 0; lineIndex < lines.length && matches.length < MAX_SEARCH_RESULTS; lineIndex += 1) {
const line = lines[lineIndex];
const foldedLine = line.toLocaleLowerCase();
const first = foldedLine.indexOf(foldedNeedle);
const foldedLine = foldedTextWithOffsets(line);
const first = foldedLine.text.indexOf(foldedNeedle);
if (first < 0) continue;
const windowStart = Math.max(0, first - 256);
const firstOriginalIndex = foldedLine.starts[first] ?? 0;
const windowStart = Math.max(0, firstOriginalIndex - 256);
const lineText = line.slice(windowStart, windowStart + MAX_SEARCH_LINE_CHARS);
const foldedText = lineText.toLocaleLowerCase();
const foldedText = foldedTextWithOffsets(lineText);
const submatches = [];
let offset = 0;
while (submatches.length < 20) {
const start = foldedText.indexOf(foldedNeedle, offset);
if (start < 0) break;
const end = start + needle.length;
const foldedStart = foldedText.text.indexOf(foldedNeedle, offset);
if (foldedStart < 0) break;
const foldedEnd = foldedStart + foldedNeedle.length;
const start = foldedText.starts[foldedStart];
const end = foldedText.ends[foldedEnd - 1];
if (start === undefined || end === undefined) break;
submatches.push({ text: lineText.slice(start, end), start, end });
offset = Math.max(end, start + 1);
offset = Math.max(foldedEnd, foldedStart + 1);
}
matches.push({
path: filePath,