diff --git a/models/user/setting.go b/models/user/setting.go index aec79b756bf14..2cbd2f75a8720 100644 --- a/models/user/setting.go +++ b/models/user/setting.go @@ -150,18 +150,18 @@ func DeleteUserSetting(userID int64, key string) error { } // SetUserSetting updates a users' setting for a specific key -func SetUserSetting(userID int64, key, value string) error { +func SetUserSetting(uid int64, key, value string) error { if err := validateUserSettingKey(key); err != nil { return err } - if err := upsertUserSettingValue(userID, key, value); err != nil { + if err := upsertUserSettingValue(uid, strings.ToLower(key), value); err != nil { return err } cc := cache.GetCache() if cc != nil { - return cc.Put(genSettingCacheKey(userID, key), value, setting_module.CacheService.TTLSeconds()) + return cc.Put(genSettingCacheKey(uid, key), value, setting_module.CacheService.TTLSeconds()) } return nil diff --git a/modules/context/repo.go b/modules/context/repo.go index a38376ff460f1..90265acc76c93 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -31,6 +31,7 @@ import ( api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" asymkey_service "code.gitea.io/gitea/services/asymkey" + "code.gitea.io/gitea/services/dev" "github.com/editorconfig/editorconfig-core-go/v2" ) @@ -588,6 +589,12 @@ func RepoAssignment(ctx *Context) (cancel context.CancelFunc) { } ctx.Data["CloneButtonShowHTTPS"] = cloneButtonShowHTTPS ctx.Data["CloneButtonShowSSH"] = cloneButtonShowSSH + editors, err := dev.GetUserDefaultEditorsWithFallback(ctx.Doer) + if err != nil { + ctx.ServerError("dev.GetDefaultEditor", err) + return + } + ctx.Data["CloneEditors"] = editors ctx.Data["CloneButtonOriginLink"] = ctx.Data["RepoCloneLink"] // it may be rewritten to the WikiCloneLink by the router middleware ctx.Data["RepoSearchEnabled"] = setting.Indexer.RepoIndexerEnabled diff --git a/modules/templates/helper.go b/modules/templates/helper.go index 7afc3aa59bc02..9531157caedc3 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -110,6 +110,7 @@ func NewFuncMap() []template.FuncMap { }, "Safe": Safe, "SafeJS": SafeJS, + "SafeURL": SafeURL, "JSEscape": JSEscape, "Str2html": Str2html, "TimeSince": timeutil.TimeSince, @@ -671,6 +672,11 @@ func Safe(raw string) template.HTML { return template.HTML(raw) } +// SafeURL render raw as URL +func SafeURL(raw string) template.URL { + return template.URL(raw) +} + // SafeJS renders raw as JS func SafeJS(raw string) template.JS { return template.JS(raw) diff --git a/modules/util/slice.go b/modules/util/slice.go index 74356f5496205..f5c7d3da6d8b1 100644 --- a/modules/util/slice.go +++ b/modules/util/slice.go @@ -88,3 +88,17 @@ func SliceRemoveAllFunc[T comparable](slice []T, targetFunc func(T) bool) []T { } return slice[:idx] } + +func SplitStringWithTrim(s, sep string) []string { + if len(s) == 0 { + return nil + } + var result []string + for _, word := range strings.Split(s, sep) { + word = strings.TrimSpace(word) + if len(word) > 0 { + result = append(result, word) + } + } + return result +} diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 59089fd39bb2a..66b14ab830f0e 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -867,7 +867,7 @@ already_forked = You've already forked %s fork_to_different_account = Fork to a different account fork_visibility_helper = The visibility of a forked repository cannot be changed. use_template = Use this template -clone_in_vsc = Clone in VS Code +clone_in_editor = Clone in %s download_zip = Download ZIP download_tar = Download TAR.GZ download_bundle = Download BUNDLE @@ -2943,6 +2943,10 @@ config.xorm_log_sql = Log SQL config.get_setting_failed = Get setting %s failed config.set_setting_failed = Set setting %s failed +config.dev_config = Development +config.dev_default_editors = Default Editors +config.dev_default_editors_desc = Choose default editors + monitor.cron = Cron Tasks monitor.name = Name monitor.schedule = Schedule diff --git a/public/img/svg/gitea-idea.svg b/public/img/svg/gitea-idea.svg new file mode 100644 index 0000000000000..78d2912fe88fb --- /dev/null +++ b/public/img/svg/gitea-idea.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/svg/gitea-vscodium.svg b/public/img/svg/gitea-vscodium.svg new file mode 100644 index 0000000000000..b12b0e3b13c8a --- /dev/null +++ b/public/img/svg/gitea-vscodium.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/routers/web/admin/config.go b/routers/web/admin/config.go index 1f71e81785a99..e93076b6f34d9 100644 --- a/routers/web/admin/config.go +++ b/routers/web/admin/config.go @@ -20,6 +20,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/dev" "code.gitea.io/gitea/services/mailer" "gitea.com/go-chi/session" @@ -189,6 +190,20 @@ func Config(ctx *context.Context) { ctx.Data["EnableXORMLog"] = setting.EnableXORMLog ctx.Data["LogSQL"] = setting.Database.LogSQL + editors := dev.GetEditors() + + defaultEditorS := systemSettings.Get(dev.KeyDevDefaultEditors) + if defaultEditorS.SettingValue == "" { + defaultEditorS = system_model.Setting{ + SettingKey: dev.KeyDevDefaultEditors, + SettingValue: dev.DefaultEditorsNames(), + } + } + + ctx.Data["DevEditors"] = editors + ctx.Data["DevDefaultEditorNames"] = defaultEditorS.SettingValue + ctx.Data["DevDefaultEditorVersion"] = defaultEditorS.Version + ctx.HTML(http.StatusOK, tplConfig) } diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go index f0f053a514e0b..36c6d99aa61fa 100644 --- a/routers/web/user/setting/profile.go +++ b/routers/web/user/setting/profile.go @@ -28,6 +28,7 @@ import ( "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/services/agit" + "code.gitea.io/gitea/services/dev" "code.gitea.io/gitea/services/forms" container_service "code.gitea.io/gitea/services/packages/container" user_service "code.gitea.io/gitea/services/user" @@ -372,9 +373,51 @@ func Appearance(ctx *context.Context) { return forms.IsUserHiddenCommentTypeGroupChecked(commentTypeGroup, hiddenCommentTypes) } + editors := dev.GetEditors() + + myDefaultEditors, err := dev.GetUserDefaultEditorsWithFallback(ctx.Doer) + if err != nil { + ctx.ServerError("dev.GetEditors", err) + return + } + + var myEditorNames string + for i, editor := range myDefaultEditors { + if i > 0 { + myEditorNames += "," + } + myEditorNames += editor.Name + } + + ctx.Data["DevEditors"] = editors + ctx.Data["DevDefaultEditorNames"] = myEditorNames + ctx.HTML(http.StatusOK, tplSettingsAppearance) } +func ChangeConfig(ctx *context.Context) { + key := strings.TrimSpace(ctx.FormString("key")) + if key == "" { + ctx.JSON(http.StatusOK, map[string]string{ + "redirect": ctx.Req.URL.String(), + }) + return + } + value := ctx.FormString("value") + + if err := user_model.SetUserSetting(ctx.Doer.ID, key, value); err != nil { + log.Error("set setting failed: %v", err) + ctx.JSON(http.StatusOK, map[string]string{ + "err": ctx.Tr("admin.config.set_setting_failed", key), + }) + return + } + + ctx.JSON(http.StatusOK, map[string]interface{}{ + "version": 1, + }) +} + // UpdateUIThemePost is used to update users' specific theme func UpdateUIThemePost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.UpdateThemeForm) diff --git a/routers/web/web.go b/routers/web/web.go index 88e27ad678992..eef76daf8b1fa 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -40,6 +40,7 @@ import ( "code.gitea.io/gitea/routers/web/user/setting/security" auth_service "code.gitea.io/gitea/services/auth" context_service "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/dev" "code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/lfs" @@ -405,6 +406,7 @@ func RegisterRoutes(m *web.Route) { m.Group("/user/settings", func() { m.Get("", user_setting.Profile) m.Post("", web.Bind(forms.UpdateProfileForm{}), user_setting.ProfilePost) + m.Post("/config", user_setting.ChangeConfig) m.Get("/change_password", auth.MustChangePassword) m.Post("/change_password", web.Bind(forms.MustChangePasswordForm{}), auth.MustChangePasswordPost) m.Post("/avatar", web.Bind(forms.AvatarForm{}), user_setting.AvatarPost) @@ -1308,6 +1310,12 @@ func RegisterRoutes(m *web.Route) { m.Get("/commit/{sha:[a-f0-9]{7,40}}.{ext:patch|diff}", repo.RawDiff) }, repo.MustEnableWiki, func(ctx *context.Context) { ctx.Data["PageIsWiki"] = true + editors, err := dev.GetUserDefaultEditorsWithFallback(ctx.Doer) + if err != nil { + ctx.ServerError("dev.GetDefaultEditor", err) + return + } + ctx.Data["CloneEditors"] = editors ctx.Data["CloneButtonOriginLink"] = ctx.Repo.Repository.WikiCloneLink() }) diff --git a/services/dev/editor.go b/services/dev/editor.go new file mode 100644 index 0000000000000..9533ee69c6dcc --- /dev/null +++ b/services/dev/editor.go @@ -0,0 +1,136 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package dev + +import ( + "html/template" + "strings" + + "code.gitea.io/gitea/models/system" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/log" +) + +const KeyDevDefaultEditors = "dev.default_editors" + +type Editor struct { + Name string + URL string + Icon string +} + +func (e *Editor) RenderURL(repoURL string) template.URL { + return template.URL(strings.ReplaceAll(e.URL, "${repo_url}", repoURL)) +} + +var defaultEditors = []Editor{ + { + Name: "VS Code", + URL: "vscode://vscode.git/clone?url=${repo_url}", + Icon: `gitea-vscode`, + }, + { + Name: "VSCodium", + URL: "vscodium://vscode.git/clone?url=${repo_url}", + Icon: `gitea-vscodium`, + }, + { + Name: "IDEA", + URL: "jetbrains://idea/checkout/git?idea.required.plugins.id=Git4Idea&checkout.repo=${repo_url}", + Icon: `gitea-idea`, + }, +} + +func GetEditorByName(name string) *Editor { + for _, editor := range defaultEditors { + if editor.Name == name { + return &editor + } + } + return nil +} + +func GetEditorsByNames(names []string) []*Editor { + editors := make([]*Editor, 0, len(names)) + for _, name := range names { + if editor := GetEditorByName(name); editor != nil { + editors = append(editors, editor) + } else { + log.Error("Unknown editor: %s", name) + } + } + return editors +} + +// GetEditors returns all editors +func GetEditors() []Editor { + return defaultEditors +} + +func DefaultEditorsNames() string { + return defaultEditors[0].Name +} + +func GetDefaultEditors() ([]*Editor, error) { + defaultNames, err := system.GetSetting(KeyDevDefaultEditors) + if err != nil { + if system.IsErrSettingIsNotExist(err) { + return nil, nil + } + return nil, err + } + names := strings.Split(defaultNames, ",") + return GetEditorsByNames(names), nil +} + +func SetDefaultEditors(names []string) error { + var validateNames []string + for _, name := range names { + if editor := GetEditorByName(name); editor != nil { + validateNames = append(validateNames, name) + } + } + + return system.SetSetting(&system.Setting{ + SettingKey: KeyDevDefaultEditors, + SettingValue: strings.Join(validateNames, ","), + }) +} + +func GetUserDefaultEditors(userID int64) ([]*Editor, error) { + defaultNames, err := user_model.GetSetting(userID, KeyDevDefaultEditors) + if err != nil { + if user_model.IsErrUserSettingIsNotExist(err) { + return nil, nil + } + return nil, err + } + names := strings.Split(defaultNames, ",") + return GetEditorsByNames(names), nil +} + +func SetUserDefaultEditors(userID int64, names []string) error { + var validateNames []string + for _, name := range names { + if editor := GetEditorByName(name); editor != nil { + validateNames = append(validateNames, name) + } + } + return user_model.SetUserSetting(userID, KeyDevDefaultEditors, strings.Join(validateNames, ",")) +} + +func GetUserDefaultEditorsWithFallback(user *user_model.User) ([]*Editor, error) { + if user == nil || user.ID <= 0 { + return GetDefaultEditors() + } + editor, err := GetUserDefaultEditors(user.ID) + if err == nil { + return editor, nil + } + + if user_model.IsErrUserSettingIsNotExist(err) { + return GetDefaultEditors() + } + return nil, err +} diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl index 8f572c8396128..4fc15cc0ddcf3 100644 --- a/templates/admin/config.tmpl +++ b/templates/admin/config.tmpl @@ -413,6 +413,33 @@ {{end}} + +