Skip to content
Closed
6 changes: 3 additions & 3 deletions models/user/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions modules/context/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions modules/templates/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ func NewFuncMap() []template.FuncMap {
},
"Safe": Safe,
"SafeJS": SafeJS,
"SafeURL": SafeURL,
"JSEscape": JSEscape,
"Str2html": Str2html,
"TimeSince": timeutil.TimeSince,
Expand Down Expand Up @@ -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)
Expand Down
14 changes: 14 additions & 0 deletions modules/util/slice.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
6 changes: 5 additions & 1 deletion options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions public/img/svg/gitea-idea.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions public/img/svg/gitea-vscodium.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions routers/web/admin/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}

Expand Down
43 changes: 43 additions & 0 deletions routers/web/user/setting/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions routers/web/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
})

Expand Down
136 changes: 136 additions & 0 deletions services/dev/editor.go
Original file line number Diff line number Diff line change
@@ -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
}
27 changes: 27 additions & 0 deletions templates/admin/config.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,33 @@
{{end}}
</dl>
</div>

<h4 class="ui top attached header">
{{.locale.Tr "admin.config.dev_config"}}
</h4>
<div class="ui attached segment">
<div class="ui list">
<div class="item">
{{$.locale.Tr "admin.config.dev_default_editors"}}
</div>
<div class="field">
<div id="dev_editors_config" class="ui floating selection multiple dropdown">
<input type="hidden" name="dev.default_editors" value="{{$.DevDefaultEditorNames}}" version="{{$.DevDefaultEditorVersion}}">
<div class="default text">
{{$.locale.Tr "admin.config.dev_default_editors_desc"}}
</div>
<div class="menu floating">
{{range .DevEditors}}
<div class="item" data-value="{{.Name}}">
{{svg .Icon 14}}
{{.Name}} - {{.URL}}
</div>
{{end}}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{{template "base/footer" .}}
Loading