-
Notifications
You must be signed in to change notification settings - Fork 297
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
factor out @-mention query scanning and parsing
- Loading branch information
Showing
11 changed files
with
279 additions
and
158 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import { describe, expect, test } from 'vitest' | ||
|
||
import { | ||
type MentionQuery, | ||
type MentionTrigger, | ||
parseMentionQuery, | ||
scanForMentionTriggerInUserTextInput, | ||
} from './query' | ||
|
||
describe('parseMentionQuery', () => { | ||
test('empty query for empty string', () => { | ||
expect(parseMentionQuery('')).toEqual<MentionQuery>({ | ||
type: 'empty', | ||
text: '', | ||
}) | ||
}) | ||
|
||
test('file query without prefix', () => { | ||
expect(parseMentionQuery('foo')).toEqual<MentionQuery>({ | ||
type: 'file', | ||
text: 'foo', | ||
}) | ||
}) | ||
|
||
test('symbol query without prefix', () => { | ||
expect(parseMentionQuery('#bar')).toEqual<MentionQuery>({ | ||
type: 'symbol', | ||
text: 'bar', | ||
}) | ||
}) | ||
|
||
test('file query with @ prefix', () => { | ||
// Note: This means that the user is literally looking for a file whose name contains `@`. | ||
// This is a very rare case. See the docstring for `parseMentionQuery`. | ||
expect(parseMentionQuery('@baz')).toEqual<MentionQuery>({ | ||
type: 'file', | ||
text: '@baz', | ||
}) | ||
}) | ||
}) | ||
|
||
describe('scanForMentionTriggerInUserTextInput', () => { | ||
test('null if no @-mention is found', () => | ||
expect(scanForMentionTriggerInUserTextInput('Hello world')).toBeNull()) | ||
|
||
test('@-mention file', () => | ||
expect(scanForMentionTriggerInUserTextInput('Hello @abc')).toEqual<MentionTrigger | null>({ | ||
leadOffset: 6, | ||
matchingString: 'abc', | ||
replaceableString: '@abc', | ||
})) | ||
|
||
test('@-mention symbol', () => | ||
expect(scanForMentionTriggerInUserTextInput('Hello @#abc')).toEqual<MentionTrigger | null>({ | ||
leadOffset: 6, | ||
matchingString: '#abc', | ||
replaceableString: '@#abc', | ||
})) | ||
|
||
describe('special chars', () => { | ||
test('dotfile', () => | ||
expect(scanForMentionTriggerInUserTextInput('Hello @.abc')).toEqual<MentionTrigger | null>({ | ||
leadOffset: 6, | ||
matchingString: '.abc', | ||
replaceableString: '@.abc', | ||
})) | ||
|
||
test('forward slash', () => | ||
expect(scanForMentionTriggerInUserTextInput('Hello @a/b')).toEqual<MentionTrigger | null>({ | ||
leadOffset: 6, | ||
matchingString: 'a/b', | ||
replaceableString: '@a/b', | ||
})) | ||
|
||
test('backslash', () => | ||
expect(scanForMentionTriggerInUserTextInput('Hello @a\\b')).toEqual<MentionTrigger | null>({ | ||
leadOffset: 6, | ||
matchingString: 'a\\b', | ||
replaceableString: '@a\\b', | ||
})) | ||
|
||
test('hyphen', () => | ||
expect( | ||
scanForMentionTriggerInUserTextInput('Hello @a-b.txt') | ||
).toEqual<MentionTrigger | null>({ | ||
leadOffset: 6, | ||
matchingString: 'a-b.txt', | ||
replaceableString: '@a-b.txt', | ||
})) | ||
}) | ||
|
||
test('with range', () => { | ||
expect(scanForMentionTriggerInUserTextInput('a @b/c:')).toBeNull() | ||
expect(scanForMentionTriggerInUserTextInput('a @b/c:1')).toBeNull() | ||
expect(scanForMentionTriggerInUserTextInput('a @b/c:12-')).toBeNull() | ||
expect(scanForMentionTriggerInUserTextInput('a @b/c:12-34')).toEqual<MentionTrigger>({ | ||
leadOffset: 2, | ||
matchingString: 'b/c:12-34', | ||
replaceableString: '@b/c:12-34', | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
/** | ||
* The parsed representation of a user's (partial or complete) input of an @-mention query. | ||
*/ | ||
export interface MentionQuery { | ||
/** | ||
* The type of context item to search for. | ||
*/ | ||
type: 'file' | 'symbol' | 'empty' | ||
|
||
/** | ||
* The user's text input, to be interpreted as a fuzzy-matched query. It is stripped of any | ||
* prefix characters that indicate the {@link MentionQuery.type}, such as `#` for symbols. | ||
*/ | ||
text: string | ||
} | ||
|
||
/** | ||
* Parse an @-mention query. The {@link query} value is whatever the UI determines is the query | ||
* based on the current text input; it is not the full value of a message that may or may not | ||
* contain an @-mention. | ||
* | ||
* The {@link query} MUST be stripped of the trigger character (usually `@`). The only valid case | ||
* where {@link query} may begin with `@` is if the user is searching for context items that contain | ||
* `@`, such as if the user typed `@@foo` to mention a file that is literally named `@foo.js`. | ||
*/ | ||
export function parseMentionQuery(query: string): MentionQuery { | ||
if (query === '') { | ||
return { type: 'empty', text: '' } | ||
} | ||
|
||
if (query.startsWith('#')) { | ||
return { type: 'symbol', text: query.slice(1) } | ||
} | ||
return { type: 'file', text: query } | ||
} | ||
|
||
const PUNCTUATION = ',\\+\\*\\?\\$\\@\\|#{}\\(\\)\\^\\[\\]!%\'"~=<>:;' | ||
|
||
const TRIGGERS = '@' | ||
|
||
/** Chars we expect to see in a mention (non-space, non-punctuation). */ | ||
const VALID_CHARS = '[^' + TRIGGERS + PUNCTUATION + '\\s]' | ||
|
||
const MAX_LENGTH = 250 | ||
|
||
const RANGE_REGEXP = '(?::\\d+-\\d+)?' | ||
|
||
const AT_MENTIONS_REGEXP = new RegExp( | ||
'(?<maybeLeadingWhitespace>^|\\s|\\()(?<replaceableString>' + | ||
'[' + | ||
TRIGGERS + | ||
']' + | ||
'(?<matchingString>#?(?:' + | ||
VALID_CHARS + | ||
'){0,' + | ||
MAX_LENGTH + | ||
'}' + | ||
RANGE_REGEXP + | ||
')' + | ||
')$' | ||
) | ||
|
||
/** | ||
* The location and content of a mention in free-form user text input. | ||
*/ | ||
export interface MentionTrigger { | ||
/** The number of characters from the start of the text to the mention trigger (`@`). */ | ||
leadOffset: number | ||
|
||
/** | ||
* The string that is used to query for the context item to mention (to be passed to | ||
* {@link parseMentionQuery}). | ||
*/ | ||
matchingString: string | ||
|
||
/** | ||
* Equal to `@` + {@link matchingString}. The entire string that should be replaced with the | ||
* context item when the at-mention reference is chosen. | ||
*/ | ||
replaceableString: string | ||
} | ||
|
||
/** | ||
* Scans free-form user text input (in a chat message editor, for example) for possible mentions | ||
* with the `@` trigger character. | ||
* | ||
* The {@link textBeforeCursor} is all of the text in the input field before the text insertion | ||
* point cursor. For example, if the input field looks like `hello | ||
* @foo█bar`, then {@link textBeforeCursor} should be `hello @foo`. | ||
*/ | ||
export function scanForMentionTriggerInUserTextInput(textBeforeCursor: string): MentionTrigger | null { | ||
const match = AT_MENTIONS_REGEXP.exec(textBeforeCursor) | ||
if (match?.groups) { | ||
return { | ||
leadOffset: match.index + match.groups.maybeLeadingWhitespace.length, | ||
matchingString: match.groups.matchingString, | ||
replaceableString: match.groups.replaceableString, | ||
} | ||
} | ||
return null | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.