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:
Jim Graham
2023-04-15 16:34:55 -04:00
committed by GitHub
parent 89c9bb5a7f
commit 0cda6aed50
2 changed files with 125 additions and 4 deletions

View File

@@ -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();
});
});
});

View File

@@ -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(