Skip to content

Commit 3efdb04

Browse files
committed
feat(IntelliSense): Add a new scope suggestion feature
The scope suggestions include the parsed scopes from existing commit history.
1 parent 16eed57 commit 3efdb04

16 files changed

+447
-285
lines changed

README.md

+13-10
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ Edit commit messages via VS Code's editor and Autocomplete for Conventional Comm
1212
- Auto Completions
1313
- [Commit Types completion](#commit-types-completion)
1414
- [Scopes completion](#scopes-completion)
15-
- Includes Workspace level scopes management
15+
- Includes Workspace level user scopes management
16+
- Supports auto grepping the scopes from existing commit history
1617
- [Gitmojis completion](#gitmojis-completion)
1718
- [Footer Types completion](#footer-types-completion)
1819
- [Issues completion](#issues-completion) for the Footer Type `Closes`
@@ -82,6 +83,7 @@ List of available conventional commit types:
8283
#### Scopes completion
8384

8485
You can type a scope manually, select one that is saved, or create a new scope by selecting the `Create New Scope` item in the suggestion list.
86+
> **Note:** If `gitCommitMessageEditor.intelliSense.completion.logScopes.enabled` option is `true`, the scope suggestion list also includes the parsed scopes from existing commit history.
8587
8688
![Demo IntelliSense Summary 2](./images/readme/demo_intellisense_summary_2.gif)
8789

@@ -148,15 +150,16 @@ The CodeLens link will appear only when no commit message is typed.
148150

149151
Table of contributed settings (prefix "gitCommitMessageEditor."):
150152

151-
| Name | Default | Description |
152-
| --------------------------------------- | ------- | -------------------------------------------------------------------------------------- |
153-
| editor.keepAfterSave | `false` | Controls whether the commit message editor tab keep or close, after saving |
154-
| codeLens.recentCommits.enabled | `true` | Controls whether the `Recent commits...` code lens feature is enabled or not |
155-
| codeLens.recentCommits.maxItems | `32` | Specifies the maximum number of commits to show in the quick pick UI |
156-
| intelliSense.completion.enabled | `true` | Controls whether the \"Quick suggestions\" feature is enabled or not |
157-
| intelliSense.completion.scopes | `[]` | Scopes that user created (Scopes will be saved into `workspace/.vscode/settings.json`) |
158-
| intelliSense.completion.issues.pageSize | `20` | Specifies the maximum number of issues per page to show in the suggestions widget |
159-
| intelliSense.hover.enabled | `true` | Controls whether the \"Hover\" feature is enabled or not |
153+
| Name | Default | Description |
154+
| ----------------------------------------- | ------- | ---------------------------------------------------------------------------------------------------- |
155+
| editor.keepAfterSave | `false` | Controls whether the commit message editor tab keep or close, after saving |
156+
| codeLens.recentCommits.enabled | `true` | Controls whether the `Recent commits...` code lens feature is enabled or not |
157+
| codeLens.recentCommits.maxItems | `32` | Specifies the maximum number of commits to show in the quick pick UI |
158+
| intelliSense.completion.enabled | `true` | Controls whether the \"Quick suggestions\" feature is enabled or not |
159+
| intelliSense.completion.scopes | `[]` | Scopes that user created (Scopes will be saved into `workspace/.vscode/settings.json`) |
160+
| intelliSense.completion.logScopes.enabled | `false` | Controls whether the scope suggestions include or not the parsed scopes from existing commit history |
161+
| intelliSense.completion.issues.pageSize | `20` | Specifies the maximum number of issues per page to show in the suggestions widget |
162+
| intelliSense.hover.enabled | `true` | Controls whether the \"Hover\" feature is enabled or not |
160163

161164
And recommends adding a setting below into your Global or Workspace `settings.json`, if you want to follow the **Git 50/72 rule**.
162165

package-lock.json

+243-202
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+15-10
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@
110110
"default": [],
111111
"description": "Scopes that user created (Scopes will be saved into `workspace/.vscode/settings.json`)"
112112
},
113+
"gitCommitMessageEditor.intelliSense.completion.logScopes.enabled": {
114+
"type": "boolean",
115+
"default": false,
116+
"description": "Controls whether the scope suggestions include or not the parsed scopes from existing commit history"
117+
},
113118
"gitCommitMessageEditor.intelliSense.completion.issues.pageSize": {
114119
"type": "number",
115120
"default": 20,
@@ -149,30 +154,30 @@
149154
"devDependencies": {
150155
"@octokit/openapi-types": "^10.1.1",
151156
"@octokit/request-error": "^2.1.0",
152-
"@types/glob": "^7.1.4",
157+
"@types/glob": "^7.2.0",
153158
"@types/marked": "^0.7.4",
154159
"@types/mocha": "^8.2.3",
155-
"@types/node": "^14.17.15",
156-
"@types/papaparse": "^5.2.6",
160+
"@types/node": "^14.18.0",
161+
"@types/papaparse": "^5.3.1",
157162
"@types/vscode": "^1.49.0",
158-
"@typescript-eslint/eslint-plugin": "^4.31.0",
159-
"@typescript-eslint/parser": "^4.31.0",
163+
"@typescript-eslint/eslint-plugin": "^4.33.0",
164+
"@typescript-eslint/parser": "^4.33.0",
160165
"eslint": "^7.32.0",
161-
"glob": "^7.1.7",
166+
"glob": "^7.2.0",
162167
"mocha": "^8.4.0",
163168
"replace": "^1.2.1",
164-
"standard-version": "^9.3.1",
169+
"standard-version": "^9.3.2",
165170
"ts-loader": "^8.3.0",
166171
"typescript": "^4.0.2",
167172
"vscode-test": "^1.4.0",
168-
"webpack": "^5.52.0",
169-
"webpack-cli": "^4.8.0"
173+
"webpack": "^5.65.0",
174+
"webpack-cli": "^4.9.1"
170175
},
171176
"dependencies": {
172177
"@octokit/rest": "^18.10.0",
173178
"@phoihos/vsce-util": "https://github.com./phoihos/vsce-util.git#v0.0.1",
174179
"conventional-commit-types": "^3.0.0",
175-
"gitmojis": "^3.5.0",
180+
"gitmojis": "^3.8.0",
176181
"marked": "^0.8.0"
177182
}
178183
}

src/configuration.ts

+5
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export interface IConfiguration extends vsceUtil.IDisposable {
1313
readonly recentCommitsMaxItems: number;
1414
readonly completionEnabled: boolean;
1515
readonly userScopes: ISummaryScope[];
16+
readonly logScopesEnabled: boolean;
1617
readonly issuesPageSize: number;
1718
readonly hoverEnabled: boolean;
1819

@@ -54,6 +55,10 @@ class Configuration extends vsceUtil.Disposable implements IConfiguration {
5455
return this._getConfigValue<ISummaryScope[]>('intelliSense.completion.scopes', []);
5556
}
5657

58+
get logScopesEnabled(): boolean {
59+
return this._getConfigValue<boolean>('intelliSense.completion.logScopes.enabled', false);
60+
}
61+
5762
get issuesPageSize(): number {
5863
return this._getConfigValue<number>('intelliSense.completion.issues.pageSize', 20);
5964
}

src/features/codeLens/recentCommitsResolver.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import * as vscode from 'vscode';
33
import { IGitService } from '../../gitService';
44
import { IGitCommit } from '../../gitService/interface';
55
import { IConfiguration } from '../../configuration';
6+
67
import { findSummaryLine } from '../parser/textDocumentParser';
8+
import { EOL_REGEX } from '../parser/syntaxRegex';
79
import { makeCommitDescription } from '../helper/commitHelper';
810

911
export interface ICommitPickItem extends vscode.QuickPickItem {
@@ -15,8 +17,6 @@ export interface IRecentCommits {
1517
readonly insertRange: vscode.Range;
1618
}
1719

18-
const _EOL_REGEX = /\r?\n/;
19-
2020
export class RecentCommitsResolver {
2121
private readonly _git: IGitService;
2222
private readonly _config: IConfiguration;
@@ -49,7 +49,7 @@ export class RecentCommitsResolver {
4949
}
5050

5151
private _makePickItem(commit: IGitCommit): ICommitPickItem {
52-
const lines = commit.message.split(_EOL_REGEX);
52+
const lines = commit.message.split(EOL_REGEX);
5353

5454
return {
5555
label: lines[0] + (lines.length > 1 ? ` $(more)(+${lines.length - 1})` : ''),

src/features/intelliSense/createNewScopeCommand.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import * as vscode from 'vscode';
22

33
import { ISummaryScope, IConfiguration } from '../../configuration';
44

5+
import { SUMMARY_TOKEN_SCOPE_REGEX } from '../parser/syntaxRegex';
6+
57
import { ICommand } from '@phoihos/vsce-util';
68

79
export class CreateNewScopeCommand implements ICommand {
@@ -56,7 +58,7 @@ export class CreateNewScopeCommand implements ICommand {
5658
if (lowerScope === '$') return 'Not allow only $';
5759
if (lowerScopes.includes(lowerScope)) return 'Already exists';
5860

59-
return /^\$?[\w\-\.]+$/.test(lowerScope)
61+
return SUMMARY_TOKEN_SCOPE_REGEX.test(lowerScope)
6062
? ''
6163
: 'Allow only words, underscores, hyphens and dots (can optionally begin with $)';
6264
}

src/features/intelliSense/footerCompletionItemManager.ts

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as vscode from 'vscode';
33
import { IGitService } from '../../gitService';
44
import { IGitIssue } from '../../gitService/interface';
55
import { IConfiguration } from '../../configuration';
6+
67
import { makeCommitDescription, makeCommitMarkdown } from '../helper/commitHelper';
78

89
import { TokenCompletionItem, IrregularCompletionItem } from './tokenCompletionItem';

src/features/intelliSense/gitCommitHoverProvider.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as vscode from 'vscode';
22

33
import { IGitService } from '../../gitService';
44
import { IConfiguration } from '../../configuration';
5+
56
import { makeIssueMarkdown } from '../helper/issueHelper';
67
import { makeCommitMarkdown } from '../helper/commitHelper';
78

src/features/intelliSense/gitCommitIntelliSenseProvider.ts

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export class GitCommitIntelliSenseProvider extends vsceUtil.Disposable {
2929

3030
const summaryCompletionItemManager = new SummaryCompletionItemManager(
3131
createNewScopeCommand.id,
32+
git,
3233
config
3334
);
3435
const footerCompletionItemManager = new FooterCompletionItemManager(

src/features/intelliSense/summaryCompletionItemManager.ts

+93-21
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,38 @@
11
import * as vscode from 'vscode';
22

3-
import { IConfiguration } from '../../configuration';
3+
import { IGitService } from '../../gitService';
4+
import { IGitCommit } from '../../gitService/interface';
5+
import { IConfiguration, ISummaryScope } from '../../configuration';
46

5-
import {
6-
TokenCompletionItem,
7-
IrregularCompletionItem,
8-
EmojiCompletionItem
9-
} from './tokenCompletionItem';
7+
import { SUMMARY_SYNTAX_REGEX, SUMMARY_TOKEN_SCOPE_REGEX, EOL_REGEX } from '../parser/syntaxRegex';
8+
9+
import { TokenCompletionItem, EmojiCompletionItem } from './tokenCompletionItem';
1010
import constants from './constants';
1111

1212
export const SCOPE_RANGE_REGEX = /\(\$?[\w\-\.]*[\(\) ]*/;
1313
export const EMOJI_RANGE_REGEX = /:[-+_a-z0-9]*(((?<=[-+_a-z0-9]):)? *)/;
1414

1515
export class SummaryCompletionItemManager {
1616
public readonly typeItems: TokenCompletionItem[];
17-
public get scopeItems(): IrregularCompletionItem[] {
18-
return this._makeScopeItems();
19-
}
20-
public readonly emojiItems: EmojiCompletionItem[];
2117

22-
private readonly _defaultScopeItems: IrregularCompletionItem[];
18+
private readonly _emojiItems: EmojiCompletionItem[];
19+
private readonly _defaultScopeItems: TokenCompletionItem[];
2320

21+
private readonly _git: IGitService;
2422
private readonly _config: IConfiguration;
2523

26-
constructor(createNewScopeCommandId: string, config: IConfiguration) {
24+
constructor(createNewScopeCommandId: string, git: IGitService, config: IConfiguration) {
25+
//#region Type completion items
2726
this.typeItems = constants.summaryTypes.map((e) => {
2827
const item = new TokenCompletionItem(e.type);
2928
item.detail = e.title;
3029
item.documentation = new vscode.MarkdownString(e.emojis[0] + ' ' + e.description);
3130
item.sortText = e.sort.toString().padStart(2, '0');
3231
return item;
3332
});
33+
//#endregion
3434

35+
//#region Emoji completion items
3536
const getEmojiFilter = (emoji: string): { token: string; sortText: string } => {
3637
// reserved emoji for BREAKING CHANGE
3738
if (emoji === '💥') return { token: '!', sortText: '1000' };
@@ -50,28 +51,64 @@ export class SummaryCompletionItemManager {
5051
};
5152
const emojiIndexPadding = (constants.summaryEmojis.length - 1).toString().length;
5253

53-
this.emojiItems = constants.summaryEmojis.map((e, i) => {
54+
this._emojiItems = constants.summaryEmojis.map((e, i) => {
5455
const filter = getEmojiFilter(e.emoji);
5556

5657
const item = new EmojiCompletionItem(e.code, filter.token);
5758
item.detail = e.emoji;
5859
item.sortText = filter.sortText + i.toString().padStart(emojiIndexPadding, '0');
5960
item.insertText = e.emoji + ' ';
6061
item.filterDoc = new vscode.MarkdownString(
61-
e.description + ` \n$(pinned)Suggested by **${filter.token}**`,
62+
e.description + `\n\n$(pinned)Suggested by *${filter.token}*`,
6263
true
6364
);
6465
item.nonFilterDoc = new vscode.MarkdownString(e.description);
6566
return item;
6667
});
68+
//#endregion
6769

6870
this._defaultScopeItems = this._createDefaultScopeItems(createNewScopeCommandId);
6971

72+
this._git = git;
7073
this._config = config;
7174
}
7275

73-
private _createDefaultScopeItems(createNewScopeCommandId: string): IrregularCompletionItem[] {
74-
const item = new IrregularCompletionItem('Create New Scope', vscode.CompletionItemKind.Event);
76+
public getScopeItems(
77+
uri: vscode.Uri,
78+
scopeRange: vscode.Range | undefined
79+
): Thenable<TokenCompletionItem[]> {
80+
const logScopesLoading = this._config.logScopesEnabled
81+
? this._git
82+
.getCommits(uri, this._config.recentCommitsMaxItems)
83+
.then((commits): ISummaryScope[] => {
84+
return this._grepLogScopes(commits);
85+
})
86+
: Promise.resolve<ISummaryScope[]>([]);
87+
88+
return logScopesLoading.then((logScopes): TokenCompletionItem[] => {
89+
return this._makeScopeItems(logScopes, scopeRange);
90+
});
91+
}
92+
93+
public getEmojiItems(emojiRange: vscode.Range, filterToken: string): EmojiCompletionItem[] {
94+
const fallbackItems: EmojiCompletionItem[] = [];
95+
96+
const items = this._emojiItems.filter((e) => {
97+
e.range = emojiRange;
98+
if (e.filterToken === filterToken) {
99+
e.documentation = e.filterDoc;
100+
return true;
101+
}
102+
e.documentation = e.nonFilterDoc;
103+
fallbackItems.push(e);
104+
return false;
105+
});
106+
107+
return items.length > 0 ? items : fallbackItems;
108+
}
109+
110+
private _createDefaultScopeItems(createNewScopeCommandId: string): TokenCompletionItem[] {
111+
const item = new TokenCompletionItem('Create New Scope', vscode.CompletionItemKind.Event);
75112
item.documentation = new vscode.MarkdownString(
76113
'New scope created will be saved into `.vscode/settings.json`'
77114
);
@@ -83,18 +120,53 @@ export class SummaryCompletionItemManager {
83120
return [item];
84121
}
85122

86-
private _makeScopeItems(): IrregularCompletionItem[] {
123+
private _grepLogScopes(commits: IGitCommit[]): ISummaryScope[] {
124+
const logScopes: ISummaryScope[] = [];
125+
126+
commits.forEach((e) => {
127+
const lines = e.message.split(EOL_REGEX);
128+
const match = lines[0].match(SUMMARY_SYNTAX_REGEX);
129+
const scope = match?.[3] ?? ''; // (\$?[\w\-\.]*)
130+
131+
if (SUMMARY_TOKEN_SCOPE_REGEX.test(scope)) {
132+
const isExist = logScopes.some((ee) => ee.scope === scope);
133+
if (isExist === false) {
134+
let description = lines[0].replace(`(${scope})`, `(**${scope}**)`);
135+
description += lines.length > 1 ? ` $(more)(+${lines.length - 1})` : '';
136+
description += `\n\n$(pinned)Suggested from$(git-commit)*${e.hashShort}*`;
137+
logScopes.push({ scope, description });
138+
}
139+
}
140+
});
141+
142+
return logScopes;
143+
}
144+
145+
private _makeScopeItems(
146+
logScopes: ISummaryScope[],
147+
scopeRange: vscode.Range | undefined
148+
): TokenCompletionItem[] {
87149
const items = this._defaultScopeItems.slice(0);
88150

89-
this._config.userScopes.forEach((e) => {
90-
const item = new IrregularCompletionItem(e.scope, vscode.CompletionItemKind.Text);
151+
const scopes = logScopes.reduce(
152+
(acc, e) => {
153+
if (acc.some((ee) => ee.scope === e.scope) === false) {
154+
acc.push(e);
155+
}
156+
return acc;
157+
},
158+
[...this._config.userScopes]
159+
);
160+
161+
scopes.forEach((e) => {
162+
const item = new TokenCompletionItem(e.scope, vscode.CompletionItemKind.Text);
91163
if (e.description !== undefined) {
92-
item.documentation = new vscode.MarkdownString(e.description);
164+
item.documentation = new vscode.MarkdownString(e.description, true);
93165
}
94166
item.sortText = (items.length % 1000).toString().padStart(3, '0');
95167
item.filterText = '(' + e.scope;
96168
item.insertText = '(' + e.scope + ')';
97-
item.rangeRegex = SCOPE_RANGE_REGEX;
169+
item.range = scopeRange;
98170

99171
items.push(item);
100172
});

0 commit comments

Comments
 (0)