Skip to content

Commit f488809

Browse files
authored
✨ Customize the set of gitmojis available in the plugin : Gitmoji, Conventional Gitmoji, or a custom source
* ✨ Implement remote configuration settings * ✨ Upgrade remote configuration settings with select box * ✨ Improve source type in settings and added tooltip * ✨ Move texts to bundle * ✨ Integrate source configs * 🐛 Fix plugin initialization * ✨ Add translations for bundle * 📝 Add documentation for the custom gitmojis * ✨ Allow empty localization * 🐛 Use ConfigUtil for properties * 🐛 Fix chinese translations * 🐛 Fix html tag in bundle message * 🐛 Fix fr translation * 🐛 Fix url for conventional gitmoji * ♻️ Change url field width * 🐛 Handle issue with finding the correct source type * ✨ Add verification of the url * 🐛 Fix path for custom gitmojis * 🐛 Fix issue with index * 🐛 Fix issue with concurrent array * 🐛 Fix url verification
1 parent 67e0eff commit f488809

13 files changed

Lines changed: 408 additions & 23 deletions

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ This plug-in will help you to choose and add the gitmoji, via a button in the Co
1616
* Load the list of gitmoji on startup from the repos of gitmoji (fallback to a local list if error).
1717
* Allow to display the gitmoji in a new column in the commit history
1818
* Translate the gitmoji description in your chinese and french language.
19+
* Custom gitmoji list from url or predefined default [Gitmojis](https://gitmoji.dev/) or [Conventional Gitmojis](https://conventional-gitmoji.web.app/)
1920

2021
See the [gitmoji website](https://gitmoji.dev/) for have the list of Emoji and their signification.
2122
<!-- Plugin description end -->
@@ -34,6 +35,10 @@ https://plugins.jetbrains.com/plugin/12383-gitmoji/
3435
In IntelliJ, go to preference, then Plugins, and search Gitmoji by Patrice de Saint Steban.
3536
After install, and restart, you will have a button on the commit dialog.
3637

38+
## Customization
39+
40+
See [custom gitmojis](custom_gitmojis.md) documentation.
41+
3742
## Contrib
3843

3944
You can contrib by adding [issues](https://github.com/patou/gitmoji-plus-commit-button/issues/new), or create pull request.

custom_gitmojis.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Custom Gitmojis
2+
3+
This document explains how to customize which Gitmojis the plugin shows, where the plugin looks for the default data, how to provide your own JSON source, and how localization works.
4+
5+
## Configuring a custom Gitmoji source
6+
1. Open the plugin settings (Settings / Preferences → GitMoji).
7+
2. Set source type to "Custom".
8+
3. Enter the JSON URL for your custom gitmoji list in the JSON URL field.
9+
4. Enter a localization URL template in the Localization URL field. Use `{locale}` as a placeholder that will be replaced with the selected locale (see below).
10+
11+
The plugin expects a JSON object with a `gitmojis` array. Each element must contain `emoji`, `code`, `description`, and `name` fields. Example:
12+
13+
```json
14+
{
15+
"gitmojis": [
16+
{
17+
"emoji": "😄",
18+
"code": ":smile:",
19+
"description": "A happy smile",
20+
"name": "smile"
21+
},
22+
{
23+
"emoji": "",
24+
"code": ":sparkles:",
25+
"description": "Add new features",
26+
"name": "sparkles"
27+
}
28+
]
29+
}
30+
```
31+
32+
If the HTTP request fails or returns an invalid response, the plugin will silently fall back to the bundled `gitmojis.json` file.
33+
34+
### Localization
35+
- Localized translations are provided as YAML files mapping gitmoji `name` → localized description.
36+
- Provide YAML translations keyed by `name` (not `code`). The plugin looks up translations by the `name` field from the JSON.
37+
- If the field is left empty, description from the JSON source is used.
38+
- The plugin uses a localization URL template that can be included with the `{locale}` token. Example:
39+
40+
```
41+
https://mydomain.com/gitmojis-{locale}.yaml
42+
```
43+
44+
- When the plugin loads translations it will replace `{locale}` with the selected language code and try to download that YAML file. Example replacements:
45+
- `en_US` → https://.../gitmojis-en_US.yaml
46+
- `fr_FR` → https://.../gitmojis-fr_FR.yaml
47+
- `zh_CN` → https://.../gitmojis-zh_CN.yaml
48+
49+
- Supported config language values:
50+
- `auto` (use system locale if supported, otherwise falls back to `en_US`)
51+
- `en_US`, `zh_CN`, `fr_FR`, `ru_RU`, `pt_BR`
52+
53+
YAML structure example:
54+
55+
```yaml
56+
gitmojis:
57+
smile: "Sourire"
58+
sparkles: "Ajouter de nouvelles fonctionnalités"
59+
```
60+
61+
The plugin will try to download remote YAML translations. If the network fetch or parsing fails, it falls back to bundled local YAML resources named `gitmojis-<locale>.yaml` that is shipped with the plugin or description from the json directly.
62+
63+
## Default data
64+
- The plugin ships a bundled default file at [gitmojis.json](./src/main/resources/gitmojis.json). If an HTTP fetch of the configured JSON URL fails, the plugin falls back to this embedded file.
65+
- Default localization example file can be found at [gitmojis.yaml](./src/main/resources/gitmojis.yaml).
66+
- The default remote URL used by the plugin for Gitmoji source is https://gitmoji.dev/api/gitmojis
67+
- There is also a built-in [Conventional Gitmoji](https://conventional-gitmoji.web.app/) option, which is reduced set of Gitmojis matching the [conventional commit](https://www.conventionalcommits.org) specification.
68+
69+
70+
## Practical example — host custom JSON and YAML on GitHub
71+
1. Create a repository containing `gitmojis.json` in the root and localization files named `gitmojis-fr_FR.yaml`, `gitmojis-zh_CN.yaml`, etc.
72+
2. Use GitHub raw URLs for the two fields in the plugin settings. Example:
73+
- JSON URL: `https://raw.githubusercontent.com/<you>/<repo>/main/gitmojis.json`
74+
- Localization template: `https://raw.githubusercontent.com/<you>/<repo>/main/gitmojis-{locale}.yaml`
75+
76+
## Further reference
77+
Example of conventional config repo for inspiration: https://github.com/glazrtom/conventional-gitmoji-config

src/main/kotlin/com/github/patou/gitmoji/GitMojiConfig.kt

Lines changed: 139 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
11
package com.github.patou.gitmoji
22

3+
import com.github.patou.gitmoji.source.GitmojiSourceType
4+
import com.github.patou.gitmoji.source.GitmojiSourceTypeMapper
35
import com.intellij.ide.util.PropertiesComponent
46
import com.intellij.openapi.options.Configurable
7+
import com.intellij.openapi.options.ConfigurationException
58
import com.intellij.openapi.options.SearchableConfigurable
69
import com.intellij.openapi.project.Project
710
import com.intellij.openapi.ui.ComboBox
811
import com.intellij.openapi.util.Comparing
12+
import java.awt.Color
13+
import java.awt.Cursor
14+
import java.awt.Desktop
915
import java.awt.FlowLayout
1016
import java.awt.GridLayout
17+
import java.awt.event.MouseAdapter
18+
import java.awt.event.MouseEvent
19+
import java.net.URI
1120
import javax.swing.JCheckBox
1221
import javax.swing.JComponent
1322
import javax.swing.JLabel
1423
import javax.swing.JPanel
24+
import javax.swing.JTextField
1525

1626
class GitMojiConfig(private val project: Project) : SearchableConfigurable {
1727
private val mainPanel: JPanel
@@ -33,19 +43,37 @@ class GitMojiConfig(private val project: Project) : SearchableConfigurable {
3343
private var textAfterUnicodeConfig: String = " "
3444
private var languagesConfig:String = "auto"
3545

46+
private val gitmojiSourceField = ComboBox(GitmojiSourceType.OPTIONS)
47+
private val gitmojiJsonUrlField = JTextField(40)
48+
private val localizationUrlField = JTextField(40)
49+
private var gitmojiJsonPanel: JPanel
50+
private var localizationPanel: JPanel
51+
private val sourceTooltipLabel: JLabel = JLabel()
52+
private var gitmojiSourceConfig: GitmojiSourceType = GitmojiSourceType.Gitmoji
53+
private var gitmojiJsonUrlConfig: String = ""
54+
private var localizationUrlConfig: String = ""
55+
3656
override fun isModified(): Boolean =
3757
Configurable.isCheckboxModified(useProjectSettings, useProjectSettingsConfig) ||
3858
Configurable.isCheckboxModified(displayEmoji, displayEmojiConfig == "emoji") ||
3959
Configurable.isCheckboxModified(useUnicode, useUnicodeConfig) ||
4060
isModified(textAfterUnicode, textAfterUnicodeConfig) ||
4161
isModified(languages, languagesConfig) ||
4262
Configurable.isCheckboxModified(insertInCursorPosition, insertInCursorPositionConfig) ||
43-
Configurable.isCheckboxModified(includeGitMojiDescription, includeGitMojiDescriptionConfig)
63+
Configurable.isCheckboxModified(includeGitMojiDescription, includeGitMojiDescriptionConfig) ||
64+
isModified(gitmojiSourceField, gitmojiSourceConfig.id) ||
65+
gitmojiJsonUrlField.text != gitmojiJsonUrlConfig ||
66+
localizationUrlField.text != localizationUrlConfig
4467

4568
private fun isModified(comboBox: ComboBox<String>, value: String): Boolean {
4669
return !Comparing.equal(comboBox.selectedItem, value)
4770
}
4871

72+
private fun <T> isModified(comboBox: ComboBox<OptionItem<T>>, value: T): Boolean {
73+
val selectedItem = comboBox.selectedItem as? OptionItem<*>
74+
return !Comparing.equal(selectedItem?.id, value)
75+
}
76+
4977
override fun getDisplayName(): String = GitmojiBundle.message("projectName")
5078
override fun getId(): String = "com.github.patou.gitmoji.config"
5179

@@ -66,9 +94,72 @@ class GitMojiConfig(private val project: Project) : SearchableConfigurable {
6694
languageJPanel.add(JLabel(GitmojiBundle.message("config.language")))
6795
languageJPanel.add(languages, null)
6896
mainPanel.add(languageJPanel)
97+
98+
// Gitmoji Source Type panel
99+
val gitmojiSourcePanel = JPanel(FlowLayout(FlowLayout.LEADING))
100+
gitmojiSourcePanel.add(JLabel(GitmojiBundle.message("config.source.type")))
101+
gitmojiSourceField.renderer = OptionItemRenderer()
102+
gitmojiSourcePanel.add(gitmojiSourceField, null)
103+
104+
sourceTooltipLabel.text = gitmojiSourceConfig.tooltipText
105+
sourceTooltipLabel.font = sourceTooltipLabel.font.deriveFont((sourceTooltipLabel.font.size - 2).toFloat())
106+
sourceTooltipLabel.foreground = Color(120, 120, 120)
107+
sourceTooltipLabel.toolTipText = gitmojiSourceConfig.tooltipUrl
108+
sourceTooltipLabel.cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
109+
sourceTooltipLabel.addMouseListener(object : MouseAdapter() {
110+
override fun mouseClicked(e: MouseEvent) {
111+
try {
112+
val url = sourceTooltipLabel.toolTipText
113+
if (!url.isNullOrBlank() && Desktop.isDesktopSupported()) {
114+
Desktop.getDesktop().browse(URI(url))
115+
}
116+
} catch (_: Exception) {
117+
// ignore errors opening link
118+
}
119+
}
120+
})
121+
122+
gitmojiSourcePanel.add(sourceTooltipLabel)
123+
mainPanel.add(gitmojiSourcePanel)
124+
125+
gitmojiSourceField.addItemListener {
126+
val selectedId = getCurrentGitmojiSourceId()
127+
val type = GitmojiSourceTypeMapper.fromId(selectedId, gitmojiJsonUrlField.text.trim(), localizationUrlField.text.trim())
128+
setGitmojiSourceFieldsVisibility(type)
129+
updateSourceTooltip(type)
130+
}
131+
132+
// Gitmoji JSON URL panel
133+
gitmojiJsonPanel = JPanel(FlowLayout(FlowLayout.LEADING))
134+
gitmojiJsonPanel.add(JLabel(GitmojiBundle.message("config.source.jsonUrl")))
135+
gitmojiJsonPanel.add(gitmojiJsonUrlField, null)
136+
mainPanel.add(gitmojiJsonPanel)
137+
138+
// Localization URL panel
139+
localizationPanel = JPanel(FlowLayout(FlowLayout.LEADING))
140+
localizationPanel.add(JLabel(GitmojiBundle.message("config.source.localizationUrl")))
141+
localizationPanel.add(localizationUrlField, null)
142+
mainPanel.add(localizationPanel)
69143
}
70144

71145
override fun apply() {
146+
val gitmojiSourceConfigId = getCurrentGitmojiSourceId()
147+
val jsonUrlCandidate = gitmojiJsonUrlField.text.trim()
148+
val localizationCandidate = localizationUrlField.text.trim()
149+
val gitmojiSource = GitmojiSourceTypeMapper.fromId(gitmojiSourceConfigId, gitmojiJsonUrlConfig, localizationUrlConfig)
150+
151+
if (gitmojiSource is GitmojiSourceType.Custom) {
152+
if (jsonUrlCandidate.isBlank()) {
153+
throw ConfigurationException(GitmojiBundle.message("config.source.error.jsonUrl.empty"))
154+
}
155+
if (!isValidHttpUrl(jsonUrlCandidate)) {
156+
throw ConfigurationException(GitmojiBundle.message("config.source.error.jsonUrl.invalid"))
157+
}
158+
if (localizationCandidate.isNotBlank() && !isValidHttpUrl(localizationCandidate)) {
159+
throw ConfigurationException(GitmojiBundle.message("config.source.error.localizationUrl.invalid"))
160+
}
161+
}
162+
72163
val wasProjectSettings = useProjectSettingsConfig
73164
useProjectSettingsConfig = useProjectSettings.isSelected
74165

@@ -82,6 +173,9 @@ class GitMojiConfig(private val project: Project) : SearchableConfigurable {
82173
else -> textAfterUnicodeOptions[textAfterUnicode.selectedIndex]
83174
}
84175
languagesConfig = languageOptions[languages.selectedIndex]
176+
gitmojiJsonUrlConfig = jsonUrlCandidate
177+
localizationUrlConfig = localizationCandidate
178+
gitmojiSourceConfig = gitmojiSource
85179

86180
val projectProps = PropertiesComponent.getInstance(project)
87181
val appProps = PropertiesComponent.getInstance()
@@ -96,13 +190,18 @@ class GitMojiConfig(private val project: Project) : SearchableConfigurable {
96190
propsToSave.setValue(CONFIG_INCLUDE_GITMOJI_DESCRIPTION, includeGitMojiDescriptionConfig)
97191
propsToSave.setValue(CONFIG_AFTER_UNICODE, textAfterUnicodeConfig)
98192
propsToSave.setValue(CONFIG_LANGUAGE, languagesConfig)
193+
propsToSave.setValue(CONFIG_GITMOJI_SOURCE_TYPE, gitmojiSourceConfig.id.value)
194+
propsToSave.setValue(CONFIG_GITMOJI_JSON_URL, gitmojiJsonUrlConfig)
195+
propsToSave.setValue(CONFIG_LOCALIZATION_URL, localizationUrlConfig)
99196

100197
// If we just unchecked, remove project settings
101198
if (wasProjectSettings && !useProjectSettingsConfig) {
102199
clearProjectSettings(projectProps)
103200
}
104201

105-
GitmojiLocale.loadTranslations()
202+
GitmojiLocale.loadTranslations(project)
203+
Gitmojis.gitmojis.clear()
204+
Gitmojis.ensureGitmojisLoaded(project)
106205
}
107206

108207
private fun clearProjectSettings(props: PropertiesComponent) {
@@ -113,6 +212,9 @@ class GitMojiConfig(private val project: Project) : SearchableConfigurable {
113212
props.unsetValue(CONFIG_INCLUDE_GITMOJI_DESCRIPTION)
114213
props.unsetValue(CONFIG_AFTER_UNICODE)
115214
props.unsetValue(CONFIG_LANGUAGE)
215+
props.unsetValue(CONFIG_GITMOJI_SOURCE_TYPE)
216+
props.unsetValue(CONFIG_GITMOJI_JSON_URL)
217+
props.unsetValue(CONFIG_LOCALIZATION_URL)
116218
} catch (_: Exception) {
117219
// Ignore errors during cleanup
118220
}
@@ -135,6 +237,10 @@ class GitMojiConfig(private val project: Project) : SearchableConfigurable {
135237
includeGitMojiDescriptionConfig = props.getBoolean(CONFIG_INCLUDE_GITMOJI_DESCRIPTION, false)
136238
textAfterUnicodeConfig = props.getValue(CONFIG_AFTER_UNICODE, " ")
137239
languagesConfig = props.getValue(CONFIG_LANGUAGE, "auto")
240+
gitmojiJsonUrlConfig = props.getValue(CONFIG_GITMOJI_JSON_URL, "")
241+
localizationUrlConfig = props.getValue(CONFIG_LOCALIZATION_URL, "")
242+
val gitmojiSourceConfigId = (props.getValue(CONFIG_GITMOJI_SOURCE_TYPE, GitmojiSourceType.Gitmoji.id.value)).let(GitmojiSourceType::Id)
243+
gitmojiSourceConfig = GitmojiSourceTypeMapper.fromId(gitmojiSourceConfigId, gitmojiJsonUrlConfig, localizationUrlConfig)
138244

139245
displayEmoji.isSelected = displayEmojiConfig == "emoji"
140246
useUnicode.isSelected = useUnicodeConfig
@@ -144,7 +250,37 @@ class GitMojiConfig(private val project: Project) : SearchableConfigurable {
144250
-1 -> if (textAfterUnicodeConfig == " ") 1 else 0
145251
else -> textAfterUnicodeOptions.indexOf(textAfterUnicodeConfig)
146252
}
147-
languages.selectedIndex = languageOptions.indexOf(languagesConfig)
253+
languages.selectedIndex = languageOptions.indexOf(languagesConfig).coerceAtLeast(0)
254+
gitmojiSourceField.selectedIndex = GitmojiSourceType.OPTIONS.indexOfFirst { it.id == gitmojiSourceConfig.id }.coerceAtLeast(0)
255+
gitmojiJsonUrlField.text = gitmojiJsonUrlConfig
256+
localizationUrlField.text = localizationUrlConfig
257+
setGitmojiSourceFieldsVisibility(gitmojiSourceConfig)
258+
updateSourceTooltip(gitmojiSourceConfig)
259+
}
260+
261+
private fun setGitmojiSourceFieldsVisibility(type: GitmojiSourceType) {
262+
val gitmojiSourceFieldsVisibility = type.id == GitmojiSourceType.Custom.ID
263+
gitmojiJsonPanel.isVisible = gitmojiSourceFieldsVisibility
264+
localizationPanel.isVisible = gitmojiSourceFieldsVisibility
265+
}
266+
267+
private fun updateSourceTooltip(type: GitmojiSourceType) {
268+
sourceTooltipLabel.text = type.tooltipText
269+
sourceTooltipLabel.toolTipText = type.tooltipUrl
270+
}
271+
272+
private fun getCurrentGitmojiSourceId(): GitmojiSourceType.Id {
273+
return (gitmojiSourceField.selectedItem as OptionItem<*>).id as GitmojiSourceType.Id
274+
}
275+
276+
private fun isValidHttpUrl(url: String): Boolean {
277+
return try {
278+
val uri = URI(url.replace("{locale}", "en_US"))
279+
val scheme = uri.scheme?.lowercase()
280+
(scheme == "http" || scheme == "https") && !uri.host.isNullOrBlank()
281+
} catch (_: Exception) {
282+
false
283+
}
148284
}
149285

150286
override fun createComponent(): JComponent = mainPanel

src/main/kotlin/com/github/patou/gitmoji/GitmojiData.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.github.patou.gitmoji
22

3+
import com.github.patou.gitmoji.source.GitmojiSourceType
34
import com.intellij.openapi.util.IconLoader
45
import javax.swing.Icon
56

@@ -10,6 +11,9 @@ const val CONFIG_LANGUAGE: String = "com.github.patou.gitmoji.language"
1011
const val CONFIG_INSERT_IN_CURSOR_POSITION: String = "com.github.patou.gitmoji.insert-in-cursor-position"
1112
const val CONFIG_INCLUDE_GITMOJI_DESCRIPTION: String = "com.github.patou.gitmoji.include-gitmoji-description"
1213
const val CONFIG_USE_PROJECT_SETTINGS: String = "com.github.patou.gitmoji.use-project-settings"
14+
const val CONFIG_GITMOJI_SOURCE_TYPE: String = "com.github.patou.gitmoji.gitmoji-source-type"
15+
const val CONFIG_GITMOJI_JSON_URL: String = "com.github.patou.gitmoji.gitmoji-json-url"
16+
const val CONFIG_LOCALIZATION_URL: String = "com.github.patou.gitmoji.localization-url"
1317

1418
data class GitmojiData(val code: String, val emoji: String, val description: String, val name: String) {
1519
private lateinit var _icon: Icon

src/main/kotlin/com/github/patou/gitmoji/GitmojiLocale.kt

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package com.github.patou.gitmoji
22

3+
import com.github.patou.gitmoji.source.GitmojiSourceTypeProvider
34
import com.intellij.ide.util.PropertiesComponent
45
import com.intellij.openapi.diagnostic.Logger
6+
import com.intellij.openapi.project.Project
57
import okhttp3.*
68
import org.yaml.snakeyaml.Yaml
79
import java.io.IOException
@@ -22,10 +24,11 @@ object GitmojiLocale {
2224
return translations[name] ?: return description
2325
}
2426

25-
fun loadTranslations() {
27+
fun loadTranslations(project: Project) {
2628
translations.clear()
27-
val instance = PropertiesComponent.getInstance()
28-
var language = instance.getValue(CONFIG_LANGUAGE) ?: "auto"
29+
val sourceType = GitmojiSourceTypeProvider.provide(project)
30+
val props = ConfigUtil.propsFor(project)
31+
var language = props.getValue(CONFIG_LANGUAGE) ?: "auto"
2932
if (language == "auto") {
3033
val defaultLanguage = Locale.getDefault().toString()
3134
if (LANGUAGE_CONFIG_LIST.contains(defaultLanguage)) {
@@ -35,13 +38,14 @@ object GitmojiLocale {
3538
language = "en_US"
3639
}
3740
}
38-
if (language == "en_US") {
41+
val requestUrl = sourceType.getLocalizedUrl(language)
42+
if (language == "en_US" || requestUrl.isBlank()) {
3943
// no need to load english translations, as they are the default
4044
return
4145
}
4246
val client = OkHttpClient().newBuilder().addInterceptor(SafeGuardInterceptor()).build()
4347
val request: Request = Request.Builder()
44-
.url("https://raw.githubusercontent.com/patou/gitmoji-plus-commit-button/master/src/main/resources/gitmojis-$language.yaml")
48+
.url(requestUrl)
4549
.build()
4650
client.newCall(request).enqueue(object : Callback {
4751
override fun onFailure(call: Call, e: IOException) {

src/main/kotlin/com/github/patou/gitmoji/GitmojiStartupActivity.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class GitmojiStartupActivity : ProjectActivity {
2424

2525
override suspend fun execute(project: Project) {
2626
offerMigrationIfNeeded(project)
27-
Gitmojis.ensureGitmojisLoaded()
27+
Gitmojis.ensureGitmojisLoaded(project)
2828
}
2929

3030
private fun offerMigrationIfNeeded(project: Project) {

0 commit comments

Comments
 (0)