diff --git a/.deadcode-out b/.deadcode-out
index 1b33525a6d..ed1f49da9b 100644
--- a/.deadcode-out
+++ b/.deadcode-out
@@ -292,9 +292,6 @@ package "code.gitea.io/gitea/modules/translation"
 	func (MockLocale).TrN
 	func (MockLocale).PrettyNumber
 
-package "code.gitea.io/gitea/modules/util"
-	func UnsafeStringToBytes
-
 package "code.gitea.io/gitea/modules/util/filebuffer"
 	func CreateFromReader
 
diff --git a/modules/setting/config.go b/modules/setting/config.go
index db189f44ac..03558574c2 100644
--- a/modules/setting/config.go
+++ b/modules/setting/config.go
@@ -15,8 +15,45 @@ type PictureStruct struct {
 	EnableFederatedAvatar *config.Value[bool]
 }
 
+type OpenWithEditorApp struct {
+	DisplayName string
+	OpenURL     string
+}
+
+type OpenWithEditorAppsType []OpenWithEditorApp
+
+func (t OpenWithEditorAppsType) ToTextareaString() string {
+	ret := ""
+	for _, app := range t {
+		ret += app.DisplayName + " = " + app.OpenURL + "\n"
+	}
+	return ret
+}
+
+func DefaultOpenWithEditorApps() OpenWithEditorAppsType {
+	return OpenWithEditorAppsType{
+		{
+			DisplayName: "VS Code",
+			OpenURL:     "vscode://vscode.git/clone?url={url}",
+		},
+		{
+			DisplayName: "VSCodium",
+			OpenURL:     "vscodium://vscode.git/clone?url={url}",
+		},
+		{
+			DisplayName: "Intellij IDEA",
+			OpenURL:     "jetbrains://idea/checkout/git?idea.required.plugins.id=Git4Idea&checkout.repo={url}",
+		},
+	}
+}
+
+type RepositoryStruct struct {
+	OpenWithEditorApps *config.Value[OpenWithEditorAppsType]
+}
+
 type ConfigStruct struct {
-	Picture *PictureStruct
+	Picture    *PictureStruct
+	Repository *RepositoryStruct
 }
 
 var (
@@ -28,8 +65,11 @@ func initDefaultConfig() {
 	config.SetCfgSecKeyGetter(&cfgSecKeyGetter{})
 	defaultConfig = &ConfigStruct{
 		Picture: &PictureStruct{
-			DisableGravatar:       config.Bool(false, config.CfgSecKey{Sec: "picture", Key: "DISABLE_GRAVATAR"}, "picture.disable_gravatar"),
-			EnableFederatedAvatar: config.Bool(false, config.CfgSecKey{Sec: "picture", Key: "ENABLE_FEDERATED_AVATAR"}, "picture.enable_federated_avatar"),
+			DisableGravatar:       config.ValueJSON[bool]("picture.disable_gravatar").WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "DISABLE_GRAVATAR"}),
+			EnableFederatedAvatar: config.ValueJSON[bool]("picture.enable_federated_avatar").WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "ENABLE_FEDERATED_AVATAR"}),
+		},
+		Repository: &RepositoryStruct{
+			OpenWithEditorApps: config.ValueJSON[OpenWithEditorAppsType]("repository.open-with.editor-apps"),
 		},
 	}
 }
@@ -42,6 +82,9 @@ func Config() *ConfigStruct {
 type cfgSecKeyGetter struct{}
 
 func (c cfgSecKeyGetter) GetValue(sec, key string) (v string, has bool) {
+	if key == "" {
+		return "", false
+	}
 	cfgSec, err := CfgProvider.GetSection(sec)
 	if err != nil {
 		log.Error("Unable to get config section: %q", sec)
diff --git a/modules/setting/config/value.go b/modules/setting/config/value.go
index 817fcdb786..f0ec120544 100644
--- a/modules/setting/config/value.go
+++ b/modules/setting/config/value.go
@@ -5,8 +5,11 @@ package config
 
 import (
 	"context"
-	"strconv"
 	"sync"
+
+	"code.gitea.io/gitea/modules/json"
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/util"
 )
 
 type CfgSecKey struct {
@@ -23,14 +26,14 @@ type Value[T any] struct {
 	revision   int
 }
 
-func (value *Value[T]) parse(s string) (v T) {
-	switch any(v).(type) {
-	case bool:
-		b, _ := strconv.ParseBool(s)
-		return any(b).(T)
-	default:
-		panic("unsupported config type, please complete the code")
+func (value *Value[T]) parse(key, valStr string) (v T) {
+	v = value.def
+	if valStr != "" {
+		if err := json.Unmarshal(util.UnsafeStringToBytes(valStr), &v); err != nil {
+			log.Error("Unable to unmarshal json config for key %q, err: %v", key, err)
+		}
 	}
+	return v
 }
 
 func (value *Value[T]) Value(ctx context.Context) (v T) {
@@ -62,7 +65,7 @@ func (value *Value[T]) Value(ctx context.Context) (v T) {
 	if valStr == nil {
 		v = value.def
 	} else {
-		v = value.parse(*valStr)
+		v = value.parse(value.dynKey, *valStr)
 	}
 
 	value.mu.Lock()
@@ -76,6 +79,16 @@ func (value *Value[T]) DynKey() string {
 	return value.dynKey
 }
 
-func Bool(def bool, cfgSecKey CfgSecKey, dynKey string) *Value[bool] {
-	return &Value[bool]{def: def, cfgSecKey: cfgSecKey, dynKey: dynKey}
+func (value *Value[T]) WithDefault(def T) *Value[T] {
+	value.def = def
+	return value
+}
+
+func (value *Value[T]) WithFileConfig(cfgSecKey CfgSecKey) *Value[T] {
+	value.cfgSecKey = cfgSecKey
+	return value
+}
+
+func ValueJSON[T any](dynKey string) *Value[T] {
+	return &Value[T]{dynKey: dynKey}
 }
diff --git a/modules/setting/repository.go b/modules/setting/repository.go
index 65f8d11b8d..50f0fd704c 100644
--- a/modules/setting/repository.go
+++ b/modules/setting/repository.go
@@ -7,7 +7,6 @@ import (
 	"os/exec"
 	"path"
 	"path/filepath"
-	"slices"
 	"strings"
 
 	"code.gitea.io/gitea/modules/log"
@@ -20,8 +19,6 @@ const (
 	RepoCreatingPublic             = "public"
 )
 
-var RecognisedRepositoryDownloadOrCloneMethods = []string{"download-zip", "download-targz", "download-bundle", "vscode-clone", "vscodium-clone", "cite"}
-
 // MaxUserCardsPerPage sets maximum amount of watchers and stargazers shown per page
 // those pages use 2 or 3 column layout, so the value should be divisible by 2 and 3
 var MaxUserCardsPerPage = 36
@@ -50,7 +47,6 @@ var (
 		DisabledRepoUnits                       []string
 		DefaultRepoUnits                        []string
 		DefaultForkRepoUnits                    []string
-		DownloadOrCloneMethods                  []string
 		PrefixArchiveFiles                      bool
 		DisableMigrations                       bool
 		DisableStars                            bool
@@ -173,7 +169,6 @@ var (
 		DisabledRepoUnits:                       []string{},
 		DefaultRepoUnits:                        []string{},
 		DefaultForkRepoUnits:                    []string{},
-		DownloadOrCloneMethods:                  []string{"download-zip", "download-targz", "download-bundle", "vscode-clone"},
 		PrefixArchiveFiles:                      true,
 		DisableMigrations:                       false,
 		DisableStars:                            false,
@@ -377,12 +372,5 @@ func loadRepositoryFrom(rootCfg ConfigProvider) {
 	if err := loadRepoArchiveFrom(rootCfg); err != nil {
 		log.Fatal("loadRepoArchiveFrom: %v", err)
 	}
-
-	for _, method := range Repository.DownloadOrCloneMethods {
-		if !slices.Contains(RecognisedRepositoryDownloadOrCloneMethods, method) {
-			log.Error("Unrecognised repository download or clone method: %s", method)
-		}
-	}
-
 	Repository.EnableFlags = sec.Key("ENABLE_FLAGS").MustBool()
 }
diff --git a/modules/web/middleware/data.go b/modules/web/middleware/data.go
index c1d1af8528..08d83f94be 100644
--- a/modules/web/middleware/data.go
+++ b/modules/web/middleware/data.go
@@ -53,7 +53,6 @@ func CommonTemplateContextData() ContextData {
 		"ShowMilestonesDashboardPage":   setting.Service.ShowMilestonesDashboardPage,
 		"ShowFooterVersion":             setting.Other.ShowFooterVersion,
 		"DisableDownloadSourceArchives": setting.Repository.DisableDownloadSourceArchives,
-		"DownloadOrCloneMethods":        setting.Repository.DownloadOrCloneMethods,
 
 		"EnableSwagger":      setting.API.EnableSwagger,
 		"EnableOpenIDSignIn": setting.Service.EnableOpenIDSignIn,
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 5233c737ba..50871be86d 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1015,8 +1015,7 @@ fork_branch = Branch to be cloned to the fork
 all_branches = All branches
 fork_no_valid_owners = This repository can not be forked because there are no valid owners.
 use_template = Use this template
-clone_in_vsc = Clone in VS Code
-clone_in_vscodium = Clone in VSCodium
+open_with_editor = Open with %s
 download_zip = Download ZIP
 download_tar = Download TAR.GZ
 download_bundle = Download BUNDLE
@@ -2833,6 +2832,8 @@ authentication = Authentication sources
 emails = User emails
 config = Configuration
 notices = System notices
+config_summary = Summary
+config_settings = Settings
 monitor = Monitoring
 first_page = First
 last_page = Last
@@ -3271,6 +3272,7 @@ config.picture_config = Picture and avatar configuration
 config.picture_service = Picture service
 config.disable_gravatar = Disable Gravatar
 config.enable_federated_avatar = Enable federated avatars
+config.open_with_editor_app_help = The "Open with" editors for the clone menu. If left empty, the default will be used. Expand to see the default.
 
 config.git_config = Git configuration
 config.git_disable_diff_highlight = Disable diff syntax highlighting
diff --git a/public/assets/img/svg/gitea-open-with-jetbrains.svg b/public/assets/img/svg/gitea-open-with-jetbrains.svg
new file mode 100644
index 0000000000..2b1491b541
--- /dev/null
+++ b/public/assets/img/svg/gitea-open-with-jetbrains.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 70 70" class="svg gitea-open-with-jetbrains" width="16" height="16" aria-hidden="true"><linearGradient id="gitea-open-with-jetbrains__a" x1=".79" x2="33.317" y1="40.089" y2="40.089" gradientUnits="userSpaceOnUse"><stop offset=".258" style="stop-color:#f97a12"/><stop offset=".459" style="stop-color:#b07b58"/><stop offset=".724" style="stop-color:#577bae"/><stop offset=".91" style="stop-color:#1e7ce5"/><stop offset="1" style="stop-color:#087cfa"/></linearGradient><path d="M17.7 54.6.8 41.2l8.4-15.6L33.3 35z" style="fill:url(#gitea-open-with-jetbrains__a)"/><linearGradient id="gitea-open-with-jetbrains__b" x1="25.767" x2="79.424" y1="24.88" y2="54.57" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#f97a12"/><stop offset=".072" style="stop-color:#cb7a3e"/><stop offset=".154" style="stop-color:#9e7b6a"/><stop offset=".242" style="stop-color:#757b91"/><stop offset=".334" style="stop-color:#537bb1"/><stop offset=".432" style="stop-color:#387ccc"/><stop offset=".538" style="stop-color:#237ce0"/><stop offset=".655" style="stop-color:#147cef"/><stop offset=".792" style="stop-color:#0b7cf7"/><stop offset="1" style="stop-color:#087cfa"/></linearGradient><path d="m70 18.7-1.3 40.5L41.8 70 25.6 59.6 49.3 35 38.9 12.3l9.3-11.2z" style="fill:url(#gitea-open-with-jetbrains__b)"/><linearGradient id="gitea-open-with-jetbrains__c" x1="63.228" x2="48.29" y1="42.915" y2="-1.719" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#fe315d"/><stop offset=".078" style="stop-color:#cb417e"/><stop offset=".16" style="stop-color:#9e4e9b"/><stop offset=".247" style="stop-color:#755bb4"/><stop offset=".339" style="stop-color:#5365ca"/><stop offset=".436" style="stop-color:#386ddb"/><stop offset=".541" style="stop-color:#2374e9"/><stop offset=".658" style="stop-color:#1478f3"/><stop offset=".794" style="stop-color:#0b7bf8"/><stop offset="1" style="stop-color:#087cfa"/></linearGradient><path d="M70 18.7 48.7 43.9l-9.8-31.6 9.3-11.2z" style="fill:url(#gitea-open-with-jetbrains__c)"/><linearGradient id="gitea-open-with-jetbrains__d" x1="10.72" x2="55.524" y1="16.473" y2="90.58" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#fe315d"/><stop offset=".04" style="stop-color:#f63462"/><stop offset=".104" style="stop-color:#df3a71"/><stop offset=".167" style="stop-color:#c24383"/><stop offset=".291" style="stop-color:#ad4a91"/><stop offset=".55" style="stop-color:#755bb4"/><stop offset=".917" style="stop-color:#1d76ed"/><stop offset="1" style="stop-color:#087cfa"/></linearGradient><path d="M33.7 58.1 5.6 68.3l4.5-15.8L16 33.1 0 27.7 10.1 0l22 2.7 21.6 24.7z" style="fill:url(#gitea-open-with-jetbrains__d)"/><path d="M13.7 13.5h43.2v43.2H13.7z" style="fill:#000"/><path d="M17.7 48.6h16.2v2.7H17.7zM29.4 22.4v-3.3h-9v3.3H23v11.3h-2.6V37h9v-3.3h-2.5V22.4zM38 37.3c-1.4 0-2.6-.3-3.5-.8s-1.7-1.2-2.3-1.9l2.5-2.8c.5.6 1 1 1.5 1.3s1.1.5 1.7.5c.7 0 1.3-.2 1.8-.7.4-.5.6-1.2.6-2.3V19.1h4v11.7c0 1.1-.1 2-.4 2.8s-.7 1.4-1.3 2c-.5.5-1.2 1-2 1.2-.8.3-1.6.5-2.6.5" style="fill:#fff"/></svg>
\ No newline at end of file
diff --git a/public/assets/img/svg/gitea-open-with-vscode.svg b/public/assets/img/svg/gitea-open-with-vscode.svg
new file mode 100644
index 0000000000..151c45e210
--- /dev/null
+++ b/public/assets/img/svg/gitea-open-with-vscode.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="-1 -1 34 34" class="svg gitea-open-with-vscode" width="16" height="16" aria-hidden="true"><path d="M30.9 3.4 24.3.3a2 2 0 0 0-2.3.4L9.4 12.2 3.9 8c-.5-.4-1.2-.4-1.7 0L.4 9.8c-.5.5-.5 1.4 0 2L5.2 16 .4 20.3c-.5.6-.5 1.5 0 2L2.2 24c.5.5 1.2.5 1.7 0l5.5-4L22 31.2a2 2 0 0 0 2.3.4l6.6-3.2a2 2 0 0 0 1.1-1.8V5.2a2 2 0 0 0-1.1-1.8M24 23.3 14.4 16 24 8.7z"/></svg>
\ No newline at end of file
diff --git a/public/assets/img/svg/gitea-open-with-vscodium.svg b/public/assets/img/svg/gitea-open-with-vscodium.svg
new file mode 100644
index 0000000000..9f70878ba6
--- /dev/null
+++ b/public/assets/img/svg/gitea-open-with-vscodium.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" clip-rule="evenodd" viewBox="0 0 16 16" class="svg gitea-open-with-vscodium" width="16" height="16" aria-hidden="true"><path fill-rule="nonzero" d="m10.2.2.5-.3c.3 0 .5.2.7.4l.2.8-.2 1-.8 2.4c-.3 1-.4 2 0 2.9l.8-2c.2 0 .4.1.4.3l-.3 1L9.2 13l3.1-2.9c.3-.2.7-.5.8-1a2 2 0 0 0-.3-1c-.2-.5-.5-.9-.6-1.4l.1-.7c.1-.1.3-.2.5-.1.2 0 .3.2.4.4.3.5.4 1.2.5 1.8l.6-1.2c0-.2.2-.4.4-.6l.4-.2c.2 0 .4.3.4.4v.6l-.8 1.6-1.4 1.8 1-.4c.2 0 .6.2.7.5 0 .2 0 .4-.2.5-.3.2-.6.2-1 .2-1 0-2.2.6-2.9 1.4L9.6 15c-.4.4-.9 1-1.4.8-.8-.1-.8-1.3-1-1.8 0-.3-.2-.6-.4-.7-.3-.2-.5-.3-.8-.3-.6-.1-1.2 0-1.8-.2l-.8-.4-.4-.7c-.3-.6-.3-1.2-.5-1.8A4 4 0 0 0 1 8l-.4-.4v-.4c.2-.2.5-.2.7 0 .5.2.5.8 1 1.1V6.2s.3-.1.4 0l.2.5L3 9c.4-.4.6-1 .5-1.5L3.4 7l.3-.2c.2 0 .3.2.4.3v.7c0 .6-.3 1.1-.4 1.7-.2.4-.3 1-.1 1.4.1.5.5.9.9 1 .5.3 1.1.4 1.7.4-.4-.6-.7-1.2-.7-2 0-.7.4-1.3.6-2C6.3 7 5.7 5.8 4.8 5l-1.5-.7c-.4-.2-.7-.7-.7-1.2.3-.1.7 0 1 .1L5 4.5l.6.1c.2-.3 0-.6-.2-.8-.3-.5-1-.6-1.3-1a.9.9 0 0 1-.2-.8c0-.2.3-.4.5-.4.4 0 .7.3.9.5.8.8 1.2 1.8 1.4 3s0 2.5-.2 3.7c0 .3-.2.5-.1.8l.2.2c.2 0 .4 0 .5-.2.4-.3.8-.8.9-1.3l.1-1.2.1-.6.4-.2.3.3v.6c-.1.5-.2 1-.5 1.6a2 2 0 0 1-.6 1l-1 1c-.1.2-.2.6-.1.9 0 .2.2.4.4.5.4.2.8.2 1 0 .3-.1.5-.4.7-.6l.5-1.4.4-2.5C9.7 7 9.6 6 9 5.2c-.2-.4-.5-.7-1-1l-1-.8c-.2-.3-.4-.7-.3-1.2h.6c.4.1.7.4.9.8s.4.8.9 1l-1-2c-.1-.3-.3-.5-.2-.8 0-.2.2-.4.4-.4s.4.1.5.3l.2.5 1 3.1a4 4 0 0 0 .4-2.3L10 1V.2Z"/></svg>
\ No newline at end of file
diff --git a/public/assets/img/svg/gitea-vscode.svg b/public/assets/img/svg/gitea-vscode.svg
deleted file mode 100644
index 453b9befcc..0000000000
--- a/public/assets/img/svg/gitea-vscode.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="-1 -1 34 34" class="svg gitea-vscode" width="16" height="16" aria-hidden="true"><path d="M30.9 3.4 24.3.3a2 2 0 0 0-2.3.4L9.4 12.2 3.9 8c-.5-.4-1.2-.4-1.7 0L.4 9.8c-.5.5-.5 1.4 0 2L5.2 16 .4 20.3c-.5.6-.5 1.5 0 2L2.2 24c.5.5 1.2.5 1.7 0l5.5-4L22 31.2a2 2 0 0 0 2.3.4l6.6-3.2a2 2 0 0 0 1.1-1.8V5.2a2 2 0 0 0-1.1-1.8M24 23.3 14.4 16 24 8.7z"/></svg>
\ No newline at end of file
diff --git a/routers/web/admin/config.go b/routers/web/admin/config.go
index d9b1973332..2f5f17e201 100644
--- a/routers/web/admin/config.go
+++ b/routers/web/admin/config.go
@@ -7,11 +7,11 @@ package admin
 import (
 	"net/http"
 	"net/url"
+	"strconv"
 	"strings"
 
 	system_model "code.gitea.io/gitea/models/system"
 	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
@@ -24,7 +24,10 @@ import (
 	"gitea.com/go-chi/session"
 )
 
-const tplConfig base.TplName = "admin/config"
+const (
+	tplConfig         base.TplName = "admin/config"
+	tplConfigSettings base.TplName = "admin/config_settings"
+)
 
 // SendTestMail send test mail to confirm mail service is OK
 func SendTestMail(ctx *context.Context) {
@@ -98,8 +101,9 @@ func shadowPassword(provider, cfgItem string) string {
 
 // Config show admin config page
 func Config(ctx *context.Context) {
-	ctx.Data["Title"] = ctx.Tr("admin.config")
+	ctx.Data["Title"] = ctx.Tr("admin.config_summary")
 	ctx.Data["PageIsAdminConfig"] = true
+	ctx.Data["PageIsAdminConfigSummary"] = true
 
 	ctx.Data["CustomConf"] = setting.CustomConf
 	ctx.Data["AppUrl"] = setting.AppURL
@@ -161,23 +165,70 @@ func Config(ctx *context.Context) {
 
 	ctx.Data["Loggers"] = log.GetManager().DumpLoggers()
 	config.GetDynGetter().InvalidateCache()
-	ctx.Data["SystemConfig"] = setting.Config()
 	prepareDeprecatedWarningsAlert(ctx)
 
 	ctx.HTML(http.StatusOK, tplConfig)
 }
 
+func ConfigSettings(ctx *context.Context) {
+	ctx.Data["Title"] = ctx.Tr("admin.config_settings")
+	ctx.Data["PageIsAdminConfig"] = true
+	ctx.Data["PageIsAdminConfigSettings"] = true
+	ctx.Data["DefaultOpenWithEditorAppsString"] = setting.DefaultOpenWithEditorApps().ToTextareaString()
+	ctx.HTML(http.StatusOK, tplConfigSettings)
+}
+
 func ChangeConfig(ctx *context.Context) {
 	key := strings.TrimSpace(ctx.FormString("key"))
 	value := ctx.FormString("value")
 	cfg := setting.Config()
-	allowedKeys := container.SetOf(cfg.Picture.DisableGravatar.DynKey(), cfg.Picture.EnableFederatedAvatar.DynKey())
-	if !allowedKeys.Contains(key) {
+
+	marshalBool := func(v string) (string, error) {
+		if b, _ := strconv.ParseBool(v); b {
+			return "true", nil
+		}
+		return "false", nil
+	}
+	marshalOpenWithApps := func(value string) (string, error) {
+		lines := strings.Split(value, "\n")
+		var openWithEditorApps setting.OpenWithEditorAppsType
+		for _, line := range lines {
+			line = strings.TrimSpace(line)
+			if line == "" {
+				continue
+			}
+			displayName, openURL, ok := strings.Cut(line, "=")
+			displayName, openURL = strings.TrimSpace(displayName), strings.TrimSpace(openURL)
+			if !ok || displayName == "" || openURL == "" {
+				continue
+			}
+			openWithEditorApps = append(openWithEditorApps, setting.OpenWithEditorApp{
+				DisplayName: strings.TrimSpace(displayName),
+				OpenURL:     strings.TrimSpace(openURL),
+			})
+		}
+		b, err := json.Marshal(openWithEditorApps)
+		if err != nil {
+			return "", err
+		}
+		return string(b), nil
+	}
+	marshallers := map[string]func(string) (string, error){
+		cfg.Picture.DisableGravatar.DynKey():       marshalBool,
+		cfg.Picture.EnableFederatedAvatar.DynKey(): marshalBool,
+		cfg.Repository.OpenWithEditorApps.DynKey(): marshalOpenWithApps,
+	}
+	marshaller, hasMarshaller := marshallers[key]
+	if !hasMarshaller {
 		ctx.JSONError(ctx.Tr("admin.config.set_setting_failed", key))
 		return
 	}
-	if err := system_model.SetSettings(ctx, map[string]string{key: value}); err != nil {
-		log.Error("set setting failed: %v", err)
+	marshaledValue, err := marshaller(value)
+	if err != nil {
+		ctx.JSONError(ctx.Tr("admin.config.set_setting_failed", key))
+		return
+	}
+	if err = system_model.SetSettings(ctx, map[string]string{key: marshaledValue}); err != nil {
 		ctx.JSONError(ctx.Tr("admin.config.set_setting_failed", key))
 		return
 	}
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index 842d14232e..8ddfd92aa1 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -44,6 +44,7 @@ import (
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/modules/svg"
 	"code.gitea.io/gitea/modules/typesniffer"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/web/feed"
@@ -812,7 +813,7 @@ func Home(ctx *context.Context) {
 		return
 	}
 
-	renderCode(ctx)
+	renderHomeCode(ctx)
 }
 
 // LastCommit returns lastCommit data for the provided branch/tag/commit and directory (in url) and filenames in body
@@ -932,9 +933,33 @@ func renderRepoTopics(ctx *context.Context) {
 	ctx.Data["Topics"] = topics
 }
 
-func renderCode(ctx *context.Context) {
+func prepareOpenWithEditorApps(ctx *context.Context) {
+	var tmplApps []map[string]any
+	apps := setting.Config().Repository.OpenWithEditorApps.Value(ctx)
+	if len(apps) == 0 {
+		apps = setting.DefaultOpenWithEditorApps()
+	}
+	for _, app := range apps {
+		schema, _, _ := strings.Cut(app.OpenURL, ":")
+		var iconHTML template.HTML
+		if schema == "vscode" || schema == "vscodium" || schema == "jetbrains" {
+			iconHTML = svg.RenderHTML(fmt.Sprintf("gitea-open-with-%s", schema), 16, "gt-mr-3")
+		} else {
+			iconHTML = svg.RenderHTML("gitea-git", 16, "gt-mr-3") // TODO: it could support user's customized icon in the future
+		}
+		tmplApps = append(tmplApps, map[string]any{
+			"DisplayName": app.DisplayName,
+			"OpenURL":     app.OpenURL,
+			"IconHTML":    iconHTML,
+		})
+	}
+	ctx.Data["OpenWithEditorApps"] = tmplApps
+}
+
+func renderHomeCode(ctx *context.Context) {
 	ctx.Data["PageIsViewCode"] = true
 	ctx.Data["RepositoryUploadEnabled"] = setting.Repository.Upload.Enabled
+	prepareOpenWithEditorApps(ctx)
 
 	if ctx.Repo.Commit == nil || ctx.Repo.Repository.IsEmpty || ctx.Repo.Repository.IsBroken() {
 		showEmpty := true
diff --git a/routers/web/web.go b/routers/web/web.go
index 2e42d7d63b..5cd7d112b0 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -691,6 +691,7 @@ func registerRoutes(m *web.Route) {
 			m.Get("", admin.Config)
 			m.Post("", admin.ChangeConfig)
 			m.Post("/test_mail", admin.SendTestMail)
+			m.Get("/settings", admin.ConfigSettings)
 		})
 
 		m.Group("/monitor", func() {
diff --git a/services/context/context.go b/services/context/context.go
index a06ebfb0dc..3e113e76ba 100644
--- a/services/context/context.go
+++ b/services/context/context.go
@@ -192,6 +192,7 @@ func Contexter() func(next http.Handler) http.Handler {
 			httpcache.SetCacheControlInHeader(ctx.Resp.Header(), 0, "no-transform")
 			ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
 
+			ctx.Data["SystemConfig"] = setting.Config()
 			ctx.Data["CsrfToken"] = ctx.Csrf.GetToken()
 			ctx.Data["CsrfTokenHtml"] = template.HTML(`<input type="hidden" name="_csrf" value="` + ctx.Data["CsrfToken"].(string) + `">`)
 
diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl
index ce6edf8a97..0c944fcb8f 100644
--- a/templates/admin/config.tmpl
+++ b/templates/admin/config.tmpl
@@ -285,27 +285,6 @@
 			</dl>
 		</div>
 
-		<h4 class="ui top attached header">
-			{{ctx.Locale.Tr "admin.config.picture_config"}}
-		</h4>
-		<div class="ui attached table segment">
-			<dl class="admin-dl-horizontal">
-				<dt>{{ctx.Locale.Tr "admin.config.disable_gravatar"}}</dt>
-				<dd>
-					<div class="ui toggle checkbox" data-tooltip-content="{{ctx.Locale.Tr "admin.config.disable_gravatar"}}">
-						<input type="checkbox" data-config-dyn-key="picture.disable_gravatar" {{if .SystemConfig.Picture.DisableGravatar.Value ctx}}checked{{end}}>
-					</div>
-				</dd>
-				<div class="divider"></div>
-				<dt>{{ctx.Locale.Tr "admin.config.enable_federated_avatar"}}</dt>
-				<dd>
-					<div class="ui toggle checkbox" data-tooltip-content="{{ctx.Locale.Tr "admin.config.enable_federated_avatar"}}">
-						<input type="checkbox" data-config-dyn-key="picture.enable_federated_avatar" {{if .SystemConfig.Picture.EnableFederatedAvatar.Value ctx}}checked{{end}}>
-					</div>
-				</dd>
-			</dl>
-		</div>
-
 		<h4 class="ui top attached header">
 			{{ctx.Locale.Tr "admin.config.git_config"}}
 		</h4>
diff --git a/templates/admin/config_settings.tmpl b/templates/admin/config_settings.tmpl
new file mode 100644
index 0000000000..22ad5c24ac
--- /dev/null
+++ b/templates/admin/config_settings.tmpl
@@ -0,0 +1,42 @@
+{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin config")}}
+<h4 class="ui top attached header">
+	{{ctx.Locale.Tr "admin.config.picture_config"}}
+</h4>
+<div class="ui attached table segment">
+	<dl class="admin-dl-horizontal">
+		<dt>{{ctx.Locale.Tr "admin.config.disable_gravatar"}}</dt>
+		<dd>
+			<div class="ui toggle checkbox" data-tooltip-content="{{ctx.Locale.Tr "admin.config.disable_gravatar"}}">
+				<input type="checkbox" data-config-dyn-key="picture.disable_gravatar" {{if .SystemConfig.Picture.DisableGravatar.Value ctx}}checked{{end}}>
+			</div>
+		</dd>
+		<div class="divider"></div>
+		<dt>{{ctx.Locale.Tr "admin.config.enable_federated_avatar"}}</dt>
+		<dd>
+			<div class="ui toggle checkbox" data-tooltip-content="{{ctx.Locale.Tr "admin.config.enable_federated_avatar"}}">
+				<input type="checkbox" data-config-dyn-key="picture.enable_federated_avatar" {{if .SystemConfig.Picture.EnableFederatedAvatar.Value ctx}}checked{{end}}>
+			</div>
+		</dd>
+	</dl>
+</div>
+
+<h4 class="ui top attached header">
+	{{ctx.Locale.Tr "repository"}}
+</h4>
+<div class="ui attached segment">
+	<form class="ui form form-fetch-action" method="post" action="{{AppSubUrl}}/admin/config?key={{.SystemConfig.Repository.OpenWithEditorApps.DynKey}}">
+		<div class="field">
+			<details>
+				<summary>{{ctx.Locale.Tr "admin.config.open_with_editor_app_help"}}</summary>
+				<pre class="gt-px-4">{{.DefaultOpenWithEditorAppsString}}</pre>
+			</details>
+		</div>
+		<div class="field">
+			<textarea name="value">{{(.SystemConfig.Repository.OpenWithEditorApps.Value ctx).ToTextareaString}}</textarea>
+		</div>
+		<div class="field">
+			<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
+		</div>
+	</form>
+</div>
+{{template "admin/layout_footer" .}}
diff --git a/templates/admin/navbar.tmpl b/templates/admin/navbar.tmpl
index f23bdee124..16ec1b4b5b 100644
--- a/templates/admin/navbar.tmpl
+++ b/templates/admin/navbar.tmpl
@@ -77,9 +77,17 @@
 			</div>
 		</details>
 		{{end}}
-		<a class="{{if .PageIsAdminConfig}}active {{end}}item" href="{{AppSubUrl}}/admin/config">
-			{{ctx.Locale.Tr "admin.config"}}
-		</a>
+		<details class="item toggleable-item" {{if or .PageIsAdminConfig}}open{{end}}>
+			<summary>{{ctx.Locale.Tr "admin.config"}}</summary>
+			<div class="menu">
+				<a class="{{if .PageIsAdminConfigSummary}}active {{end}}item" href="{{AppSubUrl}}/admin/config">
+					{{ctx.Locale.Tr "admin.config_summary"}}
+				</a>
+				<a class="{{if .PageIsAdminConfigSettings}}active {{end}}item" href="{{AppSubUrl}}/admin/config/settings">
+					{{ctx.Locale.Tr "admin.config_settings"}}
+				</a>
+			</div>
+		</details>
 		<a class="{{if .PageIsAdminNotices}}active {{end}}item" href="{{AppSubUrl}}/admin/notices">
 			{{ctx.Locale.Tr "admin.notices"}}
 		</a>
diff --git a/templates/repo/clone_script.tmpl b/templates/repo/clone_script.tmpl
index 46e49e7f85..40dae76dc7 100644
--- a/templates/repo/clone_script.tmpl
+++ b/templates/repo/clone_script.tmpl
@@ -43,11 +43,8 @@
 		for (const el of document.getElementsByClassName('js-clone-url')) {
 			el[el.nodeName === 'INPUT' ? 'value' : 'textContent'] = link;
 		}
-		for (const el of document.getElementsByClassName('js-clone-url-vsc')) {
-			el['href'] = 'vscode://vscode.git/clone?url=' + encodeURIComponent(link);
-		}
-		for (const el of document.getElementsByClassName('js-clone-url-vscodium')) {
-			el['href'] = 'vscodium://vscode.git/clone?url=' + encodeURIComponent(link);
+		for (const el of document.getElementsByClassName('js-clone-url-editor')) {
+			el.href = el.getAttribute('data-href-template').replace('{url}', encodeURIComponent(link));
 		}
 	})();
 </script>
diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl
index 6f0a996841..1e157c5664 100644
--- a/templates/repo/home.tmpl
+++ b/templates/repo/home.tmpl
@@ -137,31 +137,16 @@
 						<button id="more-btn" class="ui basic small compact jump dropdown icon button" data-tooltip-content="{{ctx.Locale.Tr "repo.more_operations"}}">
 							{{svg "octicon-kebab-horizontal"}}
 							<div class="menu">
-								{{$citation := .CitationExist}}
-								{{$originLink := .CloneButtonOriginLink}}
-								{{range $.DownloadOrCloneMethods}}
-									{{if not $.DisableDownloadSourceArchives}}
-										{{if eq . "download-zip"}}
-										<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.zip" rel="nofollow">{{svg "octicon-file-zip" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.download_zip"}}</a>
-										{{end}}
-										{{if eq . "download-targz"}}
-										<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.download_tar"}}</a>
-										{{end}}
-										{{if eq . "download-bundle"}}
-										<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.bundle" rel="nofollow">{{svg "octicon-package" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.download_bundle"}}</a>
-										{{end}}
-										{{if $citation}}
-											{{if eq . "cite"}}
-											<a class="item" id="cite-repo-button">{{svg "octicon-cross-reference" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.cite_this_repo"}}</a>
-											{{end}}
-										{{end}}
-									{{end}}
-									{{if eq . "vscode-clone"}}
-									<a class="item js-clone-url-vsc" href="vscode://vscode.git/clone?url={{$originLink.HTTPS}}">{{svg "gitea-vscode" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.clone_in_vsc"}}</a>
-									{{end}}
-									{{if eq . "vscodium-clone"}}
-									<a class="item js-clone-url-vscodium" href="vscodium://vscode.git/clone?url={{$originLink.HTTPS}}">{{svg "gitea-vscode" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.clone_in_vscodium"}}</a>
-									{{end}}
+								{{if not $.DisableDownloadSourceArchives}}
+									<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.zip" rel="nofollow">{{svg "octicon-file-zip" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.download_zip"}}</a>
+									<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.download_tar"}}</a>
+									<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.bundle" rel="nofollow">{{svg "octicon-package" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.download_bundle"}}</a>
+								{{end}}
+								{{if .CitiationExist}}
+									<a class="item" id="cite-repo-button">{{svg "octicon-cross-reference" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.cite_this_repo"}}</a>
+								{{end}}
+								{{range .OpenWithEditorApps}}
+									<a class="item js-clone-url-editor" data-href-template="{{.OpenURL}}">{{.IconHTML}}{{ctx.Locale.Tr "repo.open_with_editor" .DisplayName}}</a>
 								{{end}}
 							</div>
 						</button>
diff --git a/tests/integration/repo_test.go b/tests/integration/repo_test.go
index 50022b6a29..5cf9816d22 100644
--- a/tests/integration/repo_test.go
+++ b/tests/integration/repo_test.go
@@ -54,84 +54,6 @@ func TestViewRepo(t *testing.T) {
 	session.MakeRequest(t, req, http.StatusNotFound)
 }
 
-func TestViewRepoCloneMethods(t *testing.T) {
-	defer tests.PrepareTestEnv(t)()
-
-	getCloneMethods := func() []string {
-		req := NewRequest(t, "GET", "/user2/repo1")
-		resp := MakeRequest(t, req, http.StatusOK)
-
-		htmlDoc := NewHTMLParser(t, resp.Body)
-		cloneMoreMethodsHTML := htmlDoc.doc.Find("#more-btn div a")
-
-		var methods []string
-		cloneMoreMethodsHTML.Each(func(i int, s *goquery.Selection) {
-			a, _ := s.Attr("href")
-			methods = append(methods, a)
-		})
-
-		return methods
-	}
-
-	testCloneMethods := func(expected []string) {
-		methods := getCloneMethods()
-
-		assert.Len(t, methods, len(expected))
-		for i, expectedMethod := range expected {
-			assert.Contains(t, methods[i], expectedMethod)
-		}
-	}
-
-	t.Run("Defaults", func(t *testing.T) {
-		defer tests.PrintCurrentTest(t)()
-
-		testCloneMethods([]string{"/master.zip", "/master.tar.gz", "/master.bundle", "vscode://"})
-	})
-
-	t.Run("Customized methods", func(t *testing.T) {
-		defer tests.PrintCurrentTest(t)()
-		defer test.MockVariableValue(&setting.Repository.DownloadOrCloneMethods, []string{"vscodium-clone", "download-targz"})()
-
-		testCloneMethods([]string{"vscodium://", "/master.tar.gz"})
-	})
-
-	t.Run("Individual methods", func(t *testing.T) {
-		defer tests.PrintCurrentTest(t)()
-
-		singleMethodTest := func(method, expectedURLPart string) {
-			t.Run(method, func(t *testing.T) {
-				defer tests.PrintCurrentTest(t)()
-				defer test.MockVariableValue(&setting.Repository.DownloadOrCloneMethods, []string{method})()
-
-				testCloneMethods([]string{expectedURLPart})
-			})
-		}
-
-		cases := map[string]string{
-			"download-zip":    "/master.zip",
-			"download-targz":  "/master.tar.gz",
-			"download-bundle": "/master.bundle",
-			"vscode-clone":    "vscode://",
-			"vscodium-clone":  "vscodium://",
-		}
-		for method, expectedURLPart := range cases {
-			singleMethodTest(method, expectedURLPart)
-		}
-	})
-
-	t.Run("All methods", func(t *testing.T) {
-		defer tests.PrintCurrentTest(t)()
-		defer test.MockVariableValue(&setting.Repository.DownloadOrCloneMethods, setting.RecognisedRepositoryDownloadOrCloneMethods)()
-
-		methods := getCloneMethods()
-		// We compare against
-		// len(setting.RecognisedRepositoryDownloadOrCloneMethods) - 1, because
-		// the test environment does not currently set things up for the cite
-		// method to display.
-		assert.GreaterOrEqual(t, len(methods), len(setting.RecognisedRepositoryDownloadOrCloneMethods)-1)
-	})
-}
-
 func testViewRepo(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 
@@ -1012,3 +934,64 @@ func TestRepoFollowSymlink(t *testing.T) {
 		assertCase(t, "/user2/readme-test/src/branch/master/README.md", "", false)
 	})
 }
+
+func TestViewRepoOpenWith(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	getOpenWith := func() []string {
+		req := NewRequest(t, "GET", "/user2/repo1")
+		resp := MakeRequest(t, req, http.StatusOK)
+
+		htmlDoc := NewHTMLParser(t, resp.Body)
+		openWithHTML := htmlDoc.doc.Find(".js-clone-url-editor")
+
+		var methods []string
+		openWithHTML.Each(func(i int, s *goquery.Selection) {
+			a, _ := s.Attr("data-href-template")
+			methods = append(methods, a)
+		})
+
+		return methods
+	}
+
+	testOpenWith := func(expected []string) {
+		methods := getOpenWith()
+
+		assert.Len(t, methods, len(expected))
+		for i, expectedMethod := range expected {
+			assert.True(t, strings.HasPrefix(methods[i], expectedMethod))
+		}
+	}
+
+	t.Run("Defaults", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		testOpenWith([]string{"vscode://", "vscodium://", "jetbrains://"})
+	})
+
+	t.Run("Customised", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		// Change the methods via the admin settings
+		user := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true})
+		session := loginUser(t, user.Name)
+
+		setEditorApps := func(t *testing.T, apps string) {
+			t.Helper()
+
+			req := NewRequestWithValues(t, "POST", "/admin/config?key=repository.open-with.editor-apps", map[string]string{
+				"value": apps,
+				"_csrf": GetCSRF(t, session, "/admin/config/settings"),
+			})
+			session.MakeRequest(t, req, http.StatusOK)
+		}
+
+		defer func() {
+			setEditorApps(t, "")
+		}()
+
+		setEditorApps(t, "test = test://?url={url}")
+
+		testOpenWith([]string{"test://"})
+	})
+}
diff --git a/web_src/svg/gitea-open-with-jetbrains.svg b/web_src/svg/gitea-open-with-jetbrains.svg
new file mode 100644
index 0000000000..a7884c4289
--- /dev/null
+++ b/web_src/svg/gitea-open-with-jetbrains.svg
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 70 70" style="enable-background:new 0 0 70 70;" xml:space="preserve">
+<g>
+	<g>
+		<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="0.7898" y1="40.0893" x2="33.3172" y2="40.0893">
+			<stop  offset="0.2581" style="stop-color:#F97A12"/>
+      <stop  offset="0.4591" style="stop-color:#B07B58"/>
+      <stop  offset="0.7241" style="stop-color:#577BAE"/>
+      <stop  offset="0.9105" style="stop-color:#1E7CE5"/>
+      <stop  offset="1" style="stop-color:#087CFA"/>
+		</linearGradient>
+    <polygon style="fill:url(#SVGID_1_);" points="17.7,54.6 0.8,41.2 9.2,25.6 33.3,35 		"/>
+    <linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="25.7674" y1="24.88" x2="79.424" y2="54.57">
+			<stop  offset="0" style="stop-color:#F97A12"/>
+      <stop  offset="7.179946e-002" style="stop-color:#CB7A3E"/>
+      <stop  offset="0.1541" style="stop-color:#9E7B6A"/>
+      <stop  offset="0.242" style="stop-color:#757B91"/>
+      <stop  offset="0.3344" style="stop-color:#537BB1"/>
+      <stop  offset="0.4324" style="stop-color:#387CCC"/>
+      <stop  offset="0.5381" style="stop-color:#237CE0"/>
+      <stop  offset="0.6552" style="stop-color:#147CEF"/>
+      <stop  offset="0.7925" style="stop-color:#0B7CF7"/>
+      <stop  offset="1" style="stop-color:#087CFA"/>
+		</linearGradient>
+    <polygon style="fill:url(#SVGID_2_);" points="70,18.7 68.7,59.2 41.8,70 25.6,59.6 49.3,35 38.9,12.3 48.2,1.1 		"/>
+    <linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="63.2277" y1="42.9153" x2="48.2903" y2="-1.7191">
+			<stop  offset="0" style="stop-color:#FE315D"/>
+      <stop  offset="7.840246e-002" style="stop-color:#CB417E"/>
+      <stop  offset="0.1601" style="stop-color:#9E4E9B"/>
+      <stop  offset="0.2474" style="stop-color:#755BB4"/>
+      <stop  offset="0.3392" style="stop-color:#5365CA"/>
+      <stop  offset="0.4365" style="stop-color:#386DDB"/>
+      <stop  offset="0.5414" style="stop-color:#2374E9"/>
+      <stop  offset="0.6576" style="stop-color:#1478F3"/>
+      <stop  offset="0.794" style="stop-color:#0B7BF8"/>
+      <stop  offset="1" style="stop-color:#087CFA"/>
+		</linearGradient>
+    <polygon style="fill:url(#SVGID_3_);" points="70,18.7 48.7,43.9 38.9,12.3 48.2,1.1 		"/>
+    <linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="10.7204" y1="16.473" x2="55.5237" y2="90.58">
+			<stop  offset="0" style="stop-color:#FE315D"/>
+      <stop  offset="4.023279e-002" style="stop-color:#F63462"/>
+      <stop  offset="0.1037" style="stop-color:#DF3A71"/>
+      <stop  offset="0.1667" style="stop-color:#C24383"/>
+      <stop  offset="0.2912" style="stop-color:#AD4A91"/>
+      <stop  offset="0.5498" style="stop-color:#755BB4"/>
+      <stop  offset="0.9175" style="stop-color:#1D76ED"/>
+      <stop  offset="1" style="stop-color:#087CFA"/>
+		</linearGradient>
+    <polygon style="fill:url(#SVGID_4_);" points="33.7,58.1 5.6,68.3 10.1,52.5 16,33.1 0,27.7 10.1,0 32.1,2.7 53.7,27.4 		"/>
+	</g>
+  <g>
+		<rect x="13.7" y="13.5" style="fill:#000000;" width="43.2" height="43.2"/>
+    <rect x="17.7" y="48.6" style="fill:#FFFFFF;" width="16.2" height="2.7"/>
+    <polygon style="fill:#FFFFFF;" points="29.4,22.4 29.4,19.1 20.4,19.1 20.4,22.4 23,22.4 23,33.7 20.4,33.7 20.4,37 29.4,37
+			29.4,33.7 26.9,33.7 26.9,22.4 		"/>
+    <path style="fill:#FFFFFF;" d="M38,37.3c-1.4,0-2.6-0.3-3.5-0.8c-0.9-0.5-1.7-1.2-2.3-1.9l2.5-2.8c0.5,0.6,1,1,1.5,1.3
+			c0.5,0.3,1.1,0.5,1.7,0.5c0.7,0,1.3-0.2,1.8-0.7c0.4-0.5,0.6-1.2,0.6-2.3V19.1h4v11.7c0,1.1-0.1,2-0.4,2.8c-0.3,0.8-0.7,1.4-1.3,2
+			c-0.5,0.5-1.2,1-2,1.2C39.8,37.1,39,37.3,38,37.3"/>
+	</g>
+</g>
+</svg>
\ No newline at end of file
diff --git a/web_src/svg/gitea-vscode.svg b/web_src/svg/gitea-open-with-vscode.svg
similarity index 100%
rename from web_src/svg/gitea-vscode.svg
rename to web_src/svg/gitea-open-with-vscode.svg
diff --git a/web_src/svg/gitea-open-with-vscodium.svg b/web_src/svg/gitea-open-with-vscodium.svg
new file mode 100644
index 0000000000..483676fe71
--- /dev/null
+++ b/web_src/svg/gitea-open-with-vscodium.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" width="100%" height="100%" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" clip-rule="evenodd" version="1.1" viewBox="0 0 16 16"><path fill-rule="nonzero" d="m10.2.2.5-.3c.3 0 .5.2.7.4l.2.8-.2 1-.8 2.4c-.3 1-.4 2 0 2.9a1046.4 1046.4 0 0 0 .8-2c.2 0 .4.1.4.3l-.3 1L9.2 13l3.1-2.9c.3-.2.7-.5.8-1a2 2 0 0 0-.3-1c-.2-.5-.5-.9-.6-1.4l.1-.7c.1-.1.3-.2.5-.1.2 0 .3.2.4.4.3.5.4 1.2.5 1.8l.6-1.2c0-.2.2-.4.4-.6l.4-.2c.2 0 .4.3.4.4v.6l-.8 1.6-1.4 1.8 1-.4c.2 0 .6.2.7.5 0 .2 0 .4-.2.5-.3.2-.6.2-1 .2-1 0-2.2.6-2.9 1.4L9.6 15c-.4.4-.9 1-1.4.8-.8-.1-.8-1.3-1-1.8 0-.3-.2-.6-.4-.7-.3-.2-.5-.3-.8-.3-.6-.1-1.2 0-1.8-.2l-.8-.4-.4-.7c-.3-.6-.3-1.2-.5-1.8A4 4 0 0 0 1 8l-.4-.4v-.4c.2-.2.5-.2.7 0 .5.2.5.8 1 1.1V6.2s.3-.1.4 0l.2.5L3 9c.4-.4.6-1 .5-1.5L3.4 7l.3-.2c.2 0 .3.2.4.3v.7c0 .6-.3 1.1-.4 1.7-.2.4-.3 1-.1 1.4.1.5.5.9.9 1 .5.3 1.1.4 1.7.4-.4-.6-.7-1.2-.7-2 0-.7.4-1.3.6-2C6.3 7 5.7 5.8 4.8 5l-1.5-.7c-.4-.2-.7-.7-.7-1.2.3-.1.7 0 1 .1L5 4.5l.6.1c.2-.3 0-.6-.2-.8-.3-.5-1-.6-1.3-1a.9.9 0 0 1-.2-.8c0-.2.3-.4.5-.4.4 0 .7.3.9.5.8.8 1.2 1.8 1.4 3 .2 1.2 0 2.5-.2 3.7 0 .3-.2.5-.1.8l.2.2c.2 0 .4 0 .5-.2.4-.3.8-.8.9-1.3l.1-1.2.1-.6.4-.2.3.3v.6c-.1.5-.2 1-.5 1.6a2 2 0 0 1-.6 1l-1 1c-.1.2-.2.6-.1.9 0 .2.2.4.4.5.4.2.8.2 1 0 .3-.1.5-.4.7-.6l.5-1.4.4-2.5C9.7 7 9.6 6 9 5.2c-.2-.4-.5-.7-1-1l-1-.8c-.2-.3-.4-.7-.3-1.2h.6c.4.1.7.4.9.8.2.4.4.8.9 1l-1-2c-.1-.3-.3-.5-.2-.8 0-.2.2-.4.4-.4s.4.1.5.3l.2.5 1 3.1a4 4 0 0 0 .4-2.3L10 1V.2Z"/></svg>
\ No newline at end of file