mirror of
https://github.com/foambubble/foam.git
synced 2026-01-09 14:08:13 -05:00
Allow for # alone to trigger tag completion (#1192)
* Allow for `#` alone to trigger tag completion
In #1183, I reused [HASHTAG_REGEX](83a90177b9/packages/foam-vscode/src/core/utils/hashtags.ts (L2-L3))
to validate the tag line when the `CompletionProvider` was triggered.
I wanted to prevent this:
```markdown
# This is a Markdown header
```
but using the `HASHTAG_REGEX` had the side effect of requiring an
_additional_ character to trigger the completion provider.
```markdown
1. #p <-- triggers completion
2. # <-- does not trigger
3. #_ (space) <-- does not trigger
```
both 1. and 2. should have triggered.
To fix, I use a slightly different regex that uses a negative lookahead
to ensure that the `#` is not followed by a space. I also added spec
cases to cover this situation.
* Update regex for more robust detection of tags
Update the regex used for more robust detection of tags. Replace the
negative lookahead assertion `\s` with `[ \t]` (allow for `\n`), and
add `#` to the class so that `##` is ignored.
Attempted to add the negation `^[0-9p{L}p{Emoji}p{N}-/]` to the
negative look ahead. This was to exclude items like `#$`, `#&` that
can't be tags. However my regex-fu was insufficient.
Instead, if the regex match is to a single `#`, ensure it is the
character to the left of the cursor. Example
`this is text #%|`
where the `|` represents the cursor. The `TAG_REGEX`
will match the `#` at index 13. However since the cursor is at 15, the
Completion provider will not run.
Update the tests to cover these situations and add them all to a sub-
`describe` block labeled by the bug issue number #1189
* Use regex groups to determine match position
For the case like `here is #my-tag and now # |`, where `|` is the cursor
position after a trailing space, the match on `#my-tag` would allow tag
completion at the cursor position.
Ensure that the last regexp match group covers up to the the cursor
position. This also handles the case of `#$` because the match will only
be `#`.
This commit is contained in:
@@ -95,7 +95,7 @@ describe('Tag Completion', () => {
|
||||
});
|
||||
|
||||
it('should not provide suggestions when inside a markdown heading #1182', async () => {
|
||||
const { uri } = await createFile('# primary heading 1');
|
||||
const { uri } = await createFile('# primary');
|
||||
const { doc } = await showInEditor(uri);
|
||||
const provider = new TagCompletionProvider(foamTags);
|
||||
|
||||
@@ -107,4 +107,110 @@ describe('Tag Completion', () => {
|
||||
expect(foamTags.tags.get('primary')).toBeTruthy();
|
||||
expect(tags).toBeNull();
|
||||
});
|
||||
|
||||
describe('has robust triggering #1189', () => {
|
||||
it('should provide multiple suggestions when typing #', async () => {
|
||||
const { uri } = await createFile(`# Title
|
||||
|
||||
#`);
|
||||
const { doc } = await showInEditor(uri);
|
||||
const provider = new TagCompletionProvider(foamTags);
|
||||
|
||||
const tags = await provider.provideCompletionItems(
|
||||
doc,
|
||||
new vscode.Position(2, 1)
|
||||
);
|
||||
expect(tags.items.length).toEqual(3);
|
||||
});
|
||||
|
||||
it('should provide multiple suggestions when typing # on line with match', async () => {
|
||||
const { uri } = await createFile('Here is #my-tag and #');
|
||||
const { doc } = await showInEditor(uri);
|
||||
const provider = new TagCompletionProvider(foamTags);
|
||||
|
||||
const tags = await provider.provideCompletionItems(
|
||||
doc,
|
||||
new vscode.Position(0, 21)
|
||||
);
|
||||
expect(tags.items.length).toEqual(3);
|
||||
});
|
||||
|
||||
it('should provide multiple suggestions when typing # at EOL', async () => {
|
||||
const { uri } = await createFile(`# Title
|
||||
|
||||
#
|
||||
more text
|
||||
`);
|
||||
const { doc } = await showInEditor(uri);
|
||||
const provider = new TagCompletionProvider(foamTags);
|
||||
|
||||
const tags = await provider.provideCompletionItems(
|
||||
doc,
|
||||
new vscode.Position(2, 1)
|
||||
);
|
||||
expect(tags.items.length).toEqual(3);
|
||||
});
|
||||
|
||||
it('should not provide a suggestion when typing `# `', async () => {
|
||||
const { uri } = await createFile(`# Title
|
||||
|
||||
# `);
|
||||
const { doc } = await showInEditor(uri);
|
||||
const provider = new TagCompletionProvider(foamTags);
|
||||
|
||||
const tags = await provider.provideCompletionItems(
|
||||
doc,
|
||||
new vscode.Position(2, 2)
|
||||
);
|
||||
|
||||
expect(foamTags.tags.get('primary')).toBeTruthy();
|
||||
expect(tags).toBeNull();
|
||||
});
|
||||
|
||||
it('should not provide a suggestion when typing `#{non-match}`', async () => {
|
||||
const { uri } = await createFile(`# Title
|
||||
|
||||
#$`);
|
||||
const { doc } = await showInEditor(uri);
|
||||
const provider = new TagCompletionProvider(foamTags);
|
||||
|
||||
const tags = await provider.provideCompletionItems(
|
||||
doc,
|
||||
new vscode.Position(2, 2)
|
||||
);
|
||||
|
||||
expect(foamTags.tags.get('primary')).toBeTruthy();
|
||||
expect(tags).toBeNull();
|
||||
});
|
||||
|
||||
it('should not provide a suggestion when typing `##`', async () => {
|
||||
const { uri } = await createFile(`# Title
|
||||
|
||||
##`);
|
||||
const { doc } = await showInEditor(uri);
|
||||
const provider = new TagCompletionProvider(foamTags);
|
||||
|
||||
const tags = await provider.provideCompletionItems(
|
||||
doc,
|
||||
new vscode.Position(2, 2)
|
||||
);
|
||||
|
||||
expect(foamTags.tags.get('primary')).toBeTruthy();
|
||||
expect(tags).toBeNull();
|
||||
});
|
||||
|
||||
it('should not provide a suggestion when typing `# ` in a line that already matched', async () => {
|
||||
const { uri } = await createFile('here is #primary and now # ');
|
||||
const { doc } = await showInEditor(uri);
|
||||
const provider = new TagCompletionProvider(foamTags);
|
||||
|
||||
const tags = await provider.provideCompletionItems(
|
||||
doc,
|
||||
new vscode.Position(0, 29)
|
||||
);
|
||||
|
||||
expect(foamTags.tags.get('primary')).toBeTruthy();
|
||||
expect(tags).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { Foam } from '../core/model/foam';
|
||||
import { FoamTags } from '../core/model/tags';
|
||||
import { HASHTAG_REGEX } from '../core/utils/hashtags';
|
||||
import { FoamFeature } from '../types';
|
||||
import { mdDocSelector } from '../utils';
|
||||
|
||||
// this regex is different from HASHTAG_REGEX in that it does not look for a
|
||||
// #+character. It uses a negative look-ahead for `# `
|
||||
const TAG_REGEX =
|
||||
/(?<=^|\s)#(?![ \t#])([0-9]*[\p{L}\p{Emoji_Presentation}\p{N}/_-]*)/dgu;
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
context: vscode.ExtensionContext,
|
||||
@@ -34,12 +38,23 @@ export class TagCompletionProvider
|
||||
.lineAt(position)
|
||||
.text.substr(0, position.character);
|
||||
|
||||
const requiresAutocomplete = cursorPrefix.match(HASHTAG_REGEX);
|
||||
|
||||
const requiresAutocomplete = cursorPrefix.match(TAG_REGEX);
|
||||
if (!requiresAutocomplete) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// check the match group length.
|
||||
// find the last match group, and ensure the end of that group is
|
||||
// at the cursor position.
|
||||
// This excludes both `#%` and also `here is #my-app1 and now # ` with
|
||||
// trailing space
|
||||
const matches = Array.from(cursorPrefix.matchAll(TAG_REGEX));
|
||||
const lastMatch = matches[matches.length - 1];
|
||||
const lastMatchEndIndex = lastMatch[0].length + lastMatch.index;
|
||||
if (lastMatchEndIndex !== position.character) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const completionTags = [];
|
||||
[...this.foamTags.tags].forEach(([tag]) => {
|
||||
const item = new vscode.CompletionItem(
|
||||
|
||||
Reference in New Issue
Block a user