Android logcat이나 VS Code의 검색창을 보면 Regex 체크박스가 있다. 우리도 지원해 줘야 할 것 같아서, 지난번에 체크 박스는 넣어놨는데. 로직은 어떻게 하지? 아, VS Code랑 똑같이 하믄 되겠다! 흐흐 오픈소스 묻어가기.
VS Code의 로직 엿보기
유명한 오픈소스 프로젝트 답게 소스 정리가 아주 잘 되어 있다. 쉽게 찾을 수 있었다.
FindModelBoundToEditorModel
class 에서, private research
메소드가 호출될 때 _findMatches
에서 editor 인테페이스의 findMatches
메소드를 호출 함으로써 매치되는 Range Array를 리턴받는다.
/**
* Search the model.
* @param searchString The string used to search. If it is a regular expression, set `isRegex` to true.
* @param searchScope Limit the searching to only search inside this range.
* @param isRegex Used to indicate that `searchString` is a regular expression.
* @param matchCase Force the matching to match lower/upper case exactly.
* @param wordSeparators Force the matching to match entire words only. Pass null otherwise.
* @param captureMatches The result will contain the captured groups.
* @param limitResultCount Limit the number of results
* @return The ranges where the matches are. It is empty if no matches have been found.
*/
findMatches(searchString: string, searchScope: IRange, isRegex: boolean, matchCase: boolean, wordSeparators: string | null, captureMatches: boolean, limitResultCount?: number): FindMatch[];
내가 필요한 것은 구현체의 로직인데, textModel.ts
에서 찾을 수 있다. regex 일때, 로그 한 줄에 매치되는 단어가 있는지만 확인하면 되므로 범위를 더 좁힐 수 있다.
public findMatches(searchString: string, rawSearchScope: any, isRegex: boolean, matchCase: boolean, wordSeparators: string | null, captureMatches: boolean, limitResultCount: number = LIMIT_FIND_COUNT): model.FindMatch[] {
this._assertNotDisposed();
let searchRange: Range;
if (Range.isIRange(rawSearchScope)) {
searchRange = this.validateRange(rawSearchScope);
} else {
searchRange = this.getFullModelRange();
}
if (!isRegex && searchString.indexOf('\n') < 0) {
// not regex, not multi line
const searchParams = new SearchParams(searchString, isRegex, matchCase, wordSeparators);
const searchData = searchParams.parseSearchRequest();
if (!searchData) {
return [];
}
return this.findMatchesLineByLine(searchRange, searchData, captureMatches, limitResultCount);
}
return TextModelSearch.findMatches(this, new SearchParams(searchString, isRegex, matchCase, wordSeparators), searchRange, captureMatches, limitResultCount);
}
textModelSearch.ts
의 findMatches
가 나에게 필요한 부분인데, 검색 문자열의 전처리를 먼저 해야 한다. 할게 많구나.
public static findMatches(model: TextModel, searchParams: SearchParams, searchRange: Range, captureMatches: boolean, limitResultCount: number): FindMatch[] {
const searchData = searchParams.parseSearchRequest();
if (!searchData) {
return [];
}
if (searchData.regex.multiline) {
return this._doFindMatchesMultiline(model, searchRange, new Searcher(searchData.wordSeparators, searchData.regex), captureMatches, limitResultCount);
}
return this._doFindMatchesLineByLine(model, searchRange, searchData, captureMatches, limitResultCount);
}
parseSearchRequest
에서 전처리를 하는데, 각종 파라미터들과 함께 다시 strings.createRegExp
를 통해 비교 정규식을 만들어 내는고 있었다.
public parseSearchRequest(): SearchData | null {
if (this.searchString === '') {
return null;
}
// Try to create a RegExp out of the params
let multiline: boolean;
if (this.isRegex) {
multiline = isMultilineRegexSource(this.searchString);
} else {
multiline = (this.searchString.indexOf('\n') >= 0);
}
let regex: RegExp | null = null;
try {
regex = strings.createRegExp(this.searchString, this.isRegex, {
matchCase: this.matchCase,
wholeWord: false,
multiline: multiline,
global: true,
unicode: true
});
} catch (err) {
return null;
}
if (!regex) {
return null;
}
let canUseSimpleSearch = (!this.isRegex && !multiline);
if (canUseSimpleSearch && this.searchString.toLowerCase() !== this.searchString.toUpperCase()) {
// casing might make a difference
canUseSimpleSearch = this.matchCase;
}
return new SearchData(regex, this.wordSeparators ? getMapForWordSeparators(this.wordSeparators) : null, canUseSimpleSearch ? this.searchString : null);
}
createRegExp
는 아래와 같고, RegExp
를 생성해서 리턴한다.
export function createRegExp(searchString: string, isRegex: boolean, options: RegExpOptions = {}): RegExp {
if (!searchString) {
throw new Error('Cannot create regex from empty string');
}
if (!isRegex) {
searchString = escapeRegExpCharacters(searchString);
}
if (options.wholeWord) {
if (!/\B/.test(searchString.charAt(0))) {
searchString = '\\b' + searchString;
}
if (!/\B/.test(searchString.charAt(searchString.length - 1))) {
searchString = searchString + '\\b';
}
}
let modifiers = '';
if (options.global) {
modifiers += 'g';
}
if (!options.matchCase) {
modifiers += 'i';
}
if (options.multiline) {
modifiers += 'm';
}
if (options.unicode) {
modifiers += 'u';
}
return new RegExp(searchString, modifiers);
}
이제 다시 스택을 거슬러 올라가서, 생성된 정규식이 실질적으로 사용되는 _doFindMatchesLineByLine
을 살펴보자. 주석까지 잘 달려있는데, 내가 필요한건 라인 한 줄에 대한 평가니까 _findMatchesInLine
만 있으면 된다.
private static _doFindMatchesLineByLine(model: TextModel, searchRange: Range, searchData: SearchData, captureMatches: boolean, limitResultCount: number): FindMatch[] {
const result: FindMatch[] = [];
let resultLen = 0;
// Early case for a search range that starts & stops on the same line number
if (searchRange.startLineNumber === searchRange.endLineNumber) {
const text = model.getLineContent(searchRange.startLineNumber).substring(searchRange.startColumn - 1, searchRange.endColumn - 1);
resultLen = this._findMatchesInLine(searchData, text, searchRange.startLineNumber, searchRange.startColumn - 1, resultLen, result, captureMatches, limitResultCount);
return result;
}
// Collect results from first line
const text = model.getLineContent(searchRange.startLineNumber).substring(searchRange.startColumn - 1);
resultLen = this._findMatchesInLine(searchData, text, searchRange.startLineNumber, searchRange.startColumn - 1, resultLen, result, captureMatches, limitResultCount);
// Collect results from middle lines
for (let lineNumber = searchRange.startLineNumber + 1; lineNumber < searchRange.endLineNumber && resultLen < limitResultCount; lineNumber++) {
resultLen = this._findMatchesInLine(searchData, model.getLineContent(lineNumber), lineNumber, 0, resultLen, result, captureMatches, limitResultCount);
}
// Collect results from last line
if (resultLen < limitResultCount) {
const text = model.getLineContent(searchRange.endLineNumber).substring(0, searchRange.endColumn - 1);
resultLen = this._findMatchesInLine(searchData, text, searchRange.endLineNumber, 0, resultLen, result, captureMatches, limitResultCount);
}
return result;
}
이제 정규식이랑 매치되는 부분이 나올것 같은데도 안나오는뎈ㅋㅋ 이게 전체 editor에서 검색이라 로직이 훨씬 복잡하다. Searcher
의 next
메소드에서 정규식과 매치되는 부분을 찾고, createFindMatch
을 통해서 매치되는 부분의 위치를 결과 값으로 정리하는 것으로 마무리가 된다.
private static _findMatchesInLine(searchData: SearchData, text: string, lineNumber: number, deltaOffset: number, resultLen: number, result: FindMatch[], captureMatches: boolean, limitResultCount: number): number {
const wordSeparators = searchData.wordSeparators;
if (!captureMatches && searchData.simpleSearch) {
const searchString = searchData.simpleSearch;
const searchStringLen = searchString.length;
const textLength = text.length;
let lastMatchIndex = -searchStringLen;
while ((lastMatchIndex = text.indexOf(searchString, lastMatchIndex + searchStringLen)) !== -1) {
if (!wordSeparators || isValidMatch(wordSeparators, text, textLength, lastMatchIndex, searchStringLen)) {
result[resultLen++] = new FindMatch(new Range(lineNumber, lastMatchIndex + 1 + deltaOffset, lineNumber, lastMatchIndex + 1 + searchStringLen + deltaOffset), null);
if (resultLen >= limitResultCount) {
return resultLen;
}
}
}
return resultLen;
}
const searcher = new Searcher(searchData.wordSeparators, searchData.regex);
let m: RegExpExecArray | null;
// Reset regex to search from the beginning
searcher.reset(0);
do {
m = searcher.next(text);
if (m) {
result[resultLen++] = createFindMatch(new Range(lineNumber, m.index + 1 + deltaOffset, lineNumber, m.index + 1 + m[0].length + deltaOffset), m, captureMatches);
if (resultLen >= limitResultCount) {
return resultLen;
}
}
} while (m);
return resultLen;
}
최종적으로 RegExp
의 exec
메소드를 사용해서 매치되는 부분은 next
메소드 안에서 확인 할 수 있다.
public next(text: string): RegExpExecArray | null {
const textLength = text.length;
let m: RegExpExecArray | null;
do {
if (this._prevMatchStartIndex + this._prevMatchLength === textLength) {
// Reached the end of the line
return null;
}
m = this._searchRegex.exec(text);
if (!m) {
return null;
}
T.Viewer에 적용하기
생각보다 되게 길게 VS Code의 코드를 설명했는데 간단하게 정리해보면, 필요한 것은 매우 간단하다.
- 전처리를 통해서 정규식을 확정한다.
- 정규식으로 비교한다.
전처리라고 해서 대단한 것은 없고, unicode
설정만 추가해주면 된다.
regex 체크박스 상태가 변경되었을 때, RegExp
인스턴트를 생성해 두고, 필터에서 바로 사용하도록 했다.
onChangeTagRegex: function () {
if (this.tagRegexSetting)
this.tagRegex = new RegExp(this.tagFilter, 'u');
},
onChangeMessageRegex: function () {
if (this.messageRegexSetting)
this.messageRegex = new RegExp(this.messageFilter, 'u');
},
filterMessage: function (line) {
if (this.messageFilter == '')
return true;
if (!this.messageRegexSetting)
return -1 != line.indexOf(this.messageFilter);
return this.messageRegex.test(line);
},
이런 로직이 제대로 동작하는지 확인할 때 단위 테스트가 정말 유용하다. 여러가지 상황별 테스트를 쉽고 빠르게 적용해 볼 수 있기 때문이다. 간단하게 정규식이 제대로 적용되는지 테스트를 작성해보았다.
it('should filterTag return correct value when tagRegexSetting is true', () => {
const wrapper = mount(LogMonitor, {
localVue,
vuetify,
})
const msgTagBanana = "07-10 14:51:21.337+0900 V/Banana( 2617): heart-battery.c banana";
const msgTagBar = "07-10 14:51:21.337+0900 F/Bar( 2617): heart-battery.c orange";
const msgTagMangoBarSpace = "07-10 14:51:21.337+0900 F/MangoBar ( 2617): heart-battery.c orange";
const vm = wrapper.vm;
vm.tagFilter = '^Ba';
vm.tagRegexSetting = true;
vm.onChangeTagRegex();
expect(vm.filterTag(msgTagBanana)).toEqual(true);
expect(vm.filterTag(msgTagBar)).toEqual(true);
expect(vm.filterTag(msgTagMangoBarSpace)).toEqual(false);
vm.tagFilter = '.*Bar';
vm.tagRegexSetting = true;
vm.onChangeTagRegex();
expect(vm.filterTag(msgTagBanana)).toEqual(false);
expect(vm.filterTag(msgTagBar)).toEqual(true);
expect(vm.filterTag(msgTagMangoBarSpace)).toEqual(true);
vm.tagFilter = 'Bar$';
vm.tagRegexSetting = true;
vm.onChangeTagRegex();
expect(vm.filterTag(msgTagBanana)).toEqual(false);
expect(vm.filterTag(msgTagBar)).toEqual(true);
expect(vm.filterTag(msgTagMangoBarSpace)).toEqual(true);
})
정작 적용된 코드는 몇 줄 안됨. 뭥미. VS Code 관광한 셈 치자.ㅋ
마치며...
정규식 적용은 생각보다 어렵지 않았다. VS Code의 소스를 보면서 좋은 소스 코드의 구조에 대해서 한 번 더 생각해 볼 수 있어서 좋았다.
msaltnet/T.Viewer - Apply regular expression feature RegExp-MDN
'나의 경험담' 카테고리의 다른 글
[T.Viewer 개발일기] 13. Release 준비 (0) | 2020.08.09 |
---|---|
[T.Viewer 개발일기] 12. Optimization (0) | 2020.08.09 |
[T.Viewer 개발일기] 11. ACE Editor (0) | 2020.08.06 |
[T.Viewer 개발일기] 9. Vuetify 소소한 수정 (0) | 2020.07.26 |
[T.Viewer 개발일기] 8. PoC 검증 (0) | 2020.07.12 |
[T.Viewer 개발일기] 7. 모방은 창조의 어머니 (0) | 2020.07.05 |
[T.Viewer 개발일기] 6. PoC를 먼저 해보자 (0) | 2020.06.27 |
댓글