Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions media/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ body {
color: var(--vscode-input-placeholderForeground);
}

.filter-toggle-button {
.filter-toggle-button,
.regex-toggle-button {
background: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
border: 1px solid var(--vscode-input-border);
Expand All @@ -59,11 +60,20 @@ body {
transition: background 0.1s;
}

.filter-toggle-button:hover {
.regex-toggle-button {
min-width: 32px;
font-weight: 600;
font-size: 12px;
font-family: var(--vscode-editor-font-family, var(--vscode-font-family));
}

.filter-toggle-button:hover,
.regex-toggle-button:hover {
background: var(--vscode-button-secondaryHoverBackground);
}

.filter-toggle-button.active {
.filter-toggle-button.active,
.regex-toggle-button.active {
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
}
Expand Down Expand Up @@ -401,4 +411,4 @@ body.wrap-lines .code-line {

::-webkit-scrollbar-thumb {
background: var(--vscode-scrollbarSlider-hoverBackground);
}
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"pretest": "npm run compile && npm run lint",
"test:search-pattern": "npm run compile && node tests/searchPattern.test.js",
"lint": "eslint src --ext ts"
},
"devDependencies": {
Expand Down
11 changes: 6 additions & 5 deletions src/WebviewManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ export class WebviewManager {

case 'search':
if (message.text) {
await this.handleSearch(panelId, panel, message.text, message.includePattern, message.excludePattern);
await this.handleSearch(panelId, panel, message.text, message.includePattern, message.excludePattern, message.isRegex);
}
break;

Expand All @@ -174,22 +174,23 @@ export class WebviewManager {
}
}

private async handleSearch(panelId: string, panel: vscode.WebviewPanel, query: string, includePattern?: string, excludePattern?: string): Promise<void> {
private async handleSearch(panelId: string, panel: vscode.WebviewPanel, query: string, includePattern?: string, excludePattern?: string, isRegex: boolean = false): Promise<void> {
try {
this.panelSearches.set(panelId, query);
const searchId = `${isRegex ? 'regex' : 'literal'}:${query}`;
this.panelSearches.set(panelId, searchId);

const searchableFiles = await this.searchService.getSearchableFiles();
const searchOptions = this.searchService.getSearchOptions();
let resultCount = 0;

for (let i = 0; i < searchableFiles.length; i += searchOptions.batchSize) {
if (this.panelSearches.get(panelId) !== query) {
if (this.panelSearches.get(panelId) !== searchId) {
// A new search has been initiated in this panel, abort current search
return;
}

const batch = searchableFiles.slice(i, i + searchOptions.batchSize);
const results = await this.searchService.search(batch, query, includePattern, excludePattern);
const results = await this.searchService.search(batch, query, includePattern, excludePattern, isRegex);
Comment on lines +177 to +193
if (results.length === 0) {
continue;
}
Expand Down
50 changes: 50 additions & 0 deletions src/searchPattern.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { escapeRegExp } from './util';

export type SearchPattern = {
source: string;
flags: string;
};

export type PatternMatch = {
index: number;
length: number;
};

export function createSearchPattern(query: string, isRegex: boolean): SearchPattern | null {
if (!query) {
return null;
}

try {
const source = isRegex ? query : escapeRegExp(query);
const flags = 'gi';
new RegExp(source, flags);

return {
source,
flags
};
} catch {
return null;
}
}

export function findPatternMatches(text: string, pattern: SearchPattern): PatternMatch[] {
const matches: PatternMatch[] = [];
const expression = new RegExp(pattern.source, pattern.flags);

let match: RegExpExecArray | null;
while ((match = expression.exec(text)) !== null) {
if (match[0].length === 0) {
expression.lastIndex++;
continue;
}

matches.push({
index: match.index,
length: match[0].length
});
}

return matches;
}
39 changes: 18 additions & 21 deletions src/services/SearchService.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as vscode from 'vscode';
import { FileSearchResult, SearchMatch, SearchOptions } from '../types';
import { BINARY_EXTENSIONS, DEFAULT_SEARCH_OPTIONS } from '../constants';
import { escapeRegExp, matchGlob } from '../util';
import { matchGlob } from '../util';
import { createSearchPattern, findPatternMatches, SearchPattern } from '../searchPattern';

export class SearchService {
private options: SearchOptions;
Expand Down Expand Up @@ -83,20 +84,24 @@ export class SearchService {
return files;
}

async search(files: vscode.Uri[], query: string, includePattern?: string, excludePattern?: string): Promise<FileSearchResult[]> {
async search(files: vscode.Uri[], query: string, includePattern?: string, excludePattern?: string, isRegex: boolean = false): Promise<FileSearchResult[]> {
const fileMatchMap = new Map<string, SearchMatch[]>();
if (!query) {
return [];
}

const searchPattern = createSearchPattern(query, isRegex);
if (!searchPattern) {
return [];
}

// Filter files based on include/exclude patterns
let filteredFiles = files;
if (includePattern || excludePattern) {
filteredFiles = this.filterFilesByPatterns(files, includePattern, excludePattern);
}

const queryLower = query.toLowerCase();
await this.searchInBatches(filteredFiles, queryLower, fileMatchMap);
await this.searchInBatches(filteredFiles, searchPattern, fileMatchMap);
return this.convertMapToResults(fileMatchMap);
}

Expand Down Expand Up @@ -131,7 +136,7 @@ export class SearchService {

private async searchInBatches(
files: vscode.Uri[],
queryLower: string,
searchPattern: SearchPattern,
fileMatchMap: Map<string, SearchMatch[]>
): Promise<void> {
for (let i = 0; i < files.length; i += this.options.batchSize) {
Expand All @@ -140,7 +145,7 @@ export class SearchService {
}

const batch = files.slice(i, i + this.options.batchSize);
const results = await this.searchBatch(batch, queryLower);
const results = await this.searchBatch(batch, searchPattern);

for (const result of results) {
if (result) {
Expand All @@ -152,7 +157,7 @@ export class SearchService {

private async searchBatch(
batch: vscode.Uri[],
queryLower: string
searchPattern: SearchPattern
): Promise<Array<{ filePath: string; matches: SearchMatch[] } | null>> {
return Promise.all(batch.map(async (file) => {
try {
Expand All @@ -164,13 +169,8 @@ export class SearchService {

const uint8Array = await vscode.workspace.fs.readFile(file);
const text = new TextDecoder('utf-8', { fatal: false }).decode(uint8Array);
const textLower = text.toLowerCase();

if (!textLower.includes(queryLower)) {
return null;
}

const matches = this.findMatchesInFile(file, text, textLower, queryLower);
const matches = this.findMatchesInFile(file, text, searchPattern);
return matches.length > 0 ? { filePath: file.fsPath, matches } : null;
} catch (error) {
Comment on lines 169 to 175
return null;
Expand All @@ -181,32 +181,28 @@ export class SearchService {
private findMatchesInFile(
file: vscode.Uri,
text: string,
textLower: string,
queryLower: string
searchPattern: SearchPattern
): SearchMatch[] {
const regularLines = text.split('\n');
const lowerLines = textLower.split('\n');
const matches: SearchMatch[] = [];
const workspaceFolder = vscode.workspace.getWorkspaceFolder(file);
const relativePath = workspaceFolder
? vscode.workspace.asRelativePath(file, false)
: file.fsPath;

const matchExp = new RegExp(escapeRegExp(queryLower), 'g');
for (let i = 0; i < lowerLines.length; i++) {
for (let i = 0; i < regularLines.length; i++) {
if (this.options.maxMatchesPerFile && matches.length >= this.options.maxMatchesPerFile) {
break;
}

const previewLine = regularLines[i];
// const previewTrimOffset = regularLine.length - previewLine.length;

const lowerLine = lowerLines[i];
const lineMatches = lowerLine.matchAll(matchExp);
const lineMatches = findPatternMatches(previewLine, searchPattern);
for (const match of lineMatches) {
// clamp line preview to max 50 characters before and after to prevent issues with extremely long lines
const start = Math.max(0, match.index - 50);
const end = Math.min(previewLine.length, match.index + queryLower.length + 50);
const end = Math.min(previewLine.length, match.index + match.length + 50);
const preview = previewLine.substring(start, end);

const trimmedPreview = preview.trimStart();
Expand All @@ -220,6 +216,7 @@ export class SearchService {
relativePath,
line: i + 1,
column: match.index,
matchLength: match.length,
preview: trimmedPreview.trimEnd(),
previewColumn
});
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface SearchMatch {
relativePath: string;
line: number;
column: number;
matchLength: number;
previewColumn: number;
preview: string;
}
Expand Down Expand Up @@ -41,6 +42,7 @@ export type WebviewMessage = {
text: string;
includePattern?: string;
excludePattern?: string;
isRegex?: boolean;
} | {
command: 'getFileContent'
filePath: string;
Expand Down
Loading