From cffad36fb5989e473f5232b8ee9ab5dd83ccf295 Mon Sep 17 00:00:00 2001 From: Dominic Black Date: Fri, 8 Sep 2023 18:08:10 +0100 Subject: [PATCH] Get editor opening working with dash updates --- cli/daemon/dash/dash.go | 31 ++++++++++---- pkg/editors/launch.go | 18 +++++--- pkg/editors/lookup.go | 5 +++ pkg/editors/scheme_protocols.go | 73 +++++++++++++++++++++++++++++++++ 4 files changed, 114 insertions(+), 13 deletions(-) create mode 100644 pkg/editors/scheme_protocols.go diff --git a/cli/daemon/dash/dash.go b/cli/daemon/dash/dash.go index fad03ff2acd..e13ec76a208 100644 --- a/cli/daemon/dash/dash.go +++ b/cli/daemon/dash/dash.go @@ -207,11 +207,16 @@ func (h *handler) Handle(ctx context.Context, reply jsonrpc2.Replier, r jsonrpc2 case "editors/open": var params struct { - Editor string `json:"editor"` - File string `json:"file"` - Line int `json:"line,omitempty"` + AppID string `json:"app_id"` + Editor string `json:"editor"` + File string `json:"file"` + StartLine int `json:"start_line,omitempty"` + StartCol int `json:"start_col,omitempty"` + EndLine int `json:"end_line,omitempty"` + EndCol int `json:"end_col,omitempty"` } if err := unmarshal(¶ms); err != nil { + log.Warn().Err(err).Msg("dash: could not parse open command") return reply(ctx, nil, err) } @@ -221,13 +226,23 @@ func (h *handler) Handle(ctx context.Context, reply jsonrpc2.Replier, r jsonrpc2 return reply(ctx, nil, err) } - if err := editors.LaunchExternalEditor(params.File, editor); err != nil { - log.Err(err).Str("editor", params.Editor).Msg("dash: could not open file") - return reply(ctx, nil, err) + runs := h.run.ListRuns() + for _, r := range runs { + if r.App.PlatformOrLocalID() == params.AppID { + params.File = filepath.Join(r.App.Root(), params.File) + + if err := editors.LaunchExternalEditor(params.File, params.StartLine, params.StartCol, editor); err != nil { + log.Err(err).Str("editor", params.Editor).Msg("dash: could not open file") + return reply(ctx, nil, err) + } + + type openResp struct{} + return reply(ctx, openResp{}, nil) + } } - type openResp struct{} - return reply(ctx, openResp{}, nil) + log.Warn().Str("app_id", params.AppID).Msg("dash: could not find app") + return reply(ctx, nil, fmt.Errorf("could not find app")) } return jsonrpc2.MethodNotFound(ctx, reply, r) diff --git a/pkg/editors/launch.go b/pkg/editors/launch.go index e56c82643a5..bae0d64f90e 100644 --- a/pkg/editors/launch.go +++ b/pkg/editors/launch.go @@ -6,28 +6,34 @@ import ( "runtime" "github.com/cockroachdb/errors" + "github.com/rs/zerolog/log" ) // LaunchExternalEditor opens a given file or folder in the desired external editor. -func LaunchExternalEditor(fullPath string, editor FoundEditor) error { +func LaunchExternalEditor(fullPath string, startLine int, startCol int, editor FoundEditor) error { _, err := os.Stat(editor.Path) if os.IsNotExist(err) { return errors.Wrapf(err, "editor %s not found", editor.Editor) } + // Encore patch to allow opening to a specific line and column in the file + // if supported by the IDE + toExecute := convertFilePathToURLScheme(editor.Editor, fullPath, startLine, startCol) + var cmd *exec.Cmd //goland:noinspection GoBoolExpressions if editor.UsesShell { if runtime.GOOS == "windows" { - cmd = exec.Command("cmd.exe", "/c", editor.Path, fullPath) + cmd = exec.Command("cmd.exe", "/c", editor.Path, toExecute) } else { - cmd = exec.Command("sh", "-c", editor.Path+" "+fullPath) + cmd = exec.Command("sh", "-c", editor.Path+" "+toExecute) } } else if runtime.GOOS == "darwin" { - cmd = exec.Command("open", "-a", editor.Path, fullPath) + // open has different semantics on macOS, so we need to handle it differently + cmd = exec.Command("open", "-a", editor.Path, toExecute) } else { - cmd = exec.Command(editor.Path, fullPath) + cmd = exec.Command(editor.Path, toExecute) } // Make sure the editor processes are detached from the Encore daemon. @@ -35,5 +41,7 @@ func LaunchExternalEditor(fullPath string, editor FoundEditor) error { // Encore daemon shutsdown. cmd.SysProcAttr = detachSysProcAttr() + log.Info().Str("full_path", fullPath).Str("editor", editor.Editor).Str("cmd", cmd.String()).Msg("attempting to open file") + return errors.Wrap(cmd.Start(), "failed to start editor") } diff --git a/pkg/editors/lookup.go b/pkg/editors/lookup.go index ca803da6ac6..9c527f87592 100644 --- a/pkg/editors/lookup.go +++ b/pkg/editors/lookup.go @@ -3,6 +3,7 @@ package editors import ( "context" goerrors "errors" + "sort" "strings" "sync" @@ -42,6 +43,10 @@ func Resolve(ctx context.Context) ([]FoundEditor, error) { if err != nil { return nil, errors.Wrap(err, "unable to get available editors") } + + sort.Slice(editorCache, func(i, j int) bool { + return editorCache[i].Editor < editorCache[j].Editor + }) } return editorCache, nil diff --git a/pkg/editors/scheme_protocols.go b/pkg/editors/scheme_protocols.go new file mode 100644 index 00000000000..f23f200ea10 --- /dev/null +++ b/pkg/editors/scheme_protocols.go @@ -0,0 +1,73 @@ +package editors + +import ( + "fmt" + "net/url" + "strings" +) + +/* +This file was added by Encore and is not part of the original GitHub Desktop codebase +*/ + +func convertFilePathToURLScheme(editorName string, fullPath string, startLine int, startCol int) string { + switch strings.ToLower(editorName) { + case "vscode", "visual studio code", "visual studio code (insiders)": + if startLine > 0 { + fullPath = fmt.Sprintf("%s:%d", fullPath, startLine) + } + return toURLScheme("vscode", "file", fullPath, "", "", 0, 0) + case "jetbrains goland", "goland": + return toJetBrainsScheme("goland", fullPath, startLine, startCol) + case "jetbrains phpstorm", "phpstorm": + return toJetBrainsScheme("phpstorm", fullPath, startLine, startCol) + case "jetbrains pycharm", "pycharm", "pycharm community edition": + return toJetBrainsScheme("pycharm", fullPath, startLine, startCol) + case "jetbrains rubymine", "rubymine": + return toJetBrainsScheme("rubymine", fullPath, startLine, startCol) + case "jetbrains webstorm", "webstorm": + return toJetBrainsScheme("webstorm", fullPath, startLine, startCol) + case "jetbrains intellij", "intellij", "idea", "intellij idea", "intellij idea community edition": + return toJetBrainsScheme("idea", fullPath, startLine, startCol) + case "jetbrains clion", "clion": + return toJetBrainsScheme("clion", fullPath, startLine, startCol) + case "textmate", "mate": + return toOpenURLScheme("txmt", "", fullPath, startLine, startCol) + case "bbedit": + return toOpenURLScheme("bbedit", "", fullPath, startLine, startCol) + default: + return fullPath + } +} + +func toJetBrainsScheme(scheme string, file string, line int, col int) string { + return toURLScheme(scheme, "open", "", "file", file, line, col) +} + +func toOpenURLScheme(scheme string, basePath string, file string, line int, col int) string { + return toURLScheme(scheme, "open", basePath, "url", fmt.Sprintf("file://%s", file), line, col) +} + +func toURLScheme(scheme string, host string, basePath string, fileKey string, file string, line int, col int) string { + u := &url.URL{ + Scheme: scheme, + Host: host, + Path: basePath, + } + + q := u.Query() + if fileKey != "" && file != "" { + q.Set(fileKey, file) + } + if line > 0 { + q.Set("line", fmt.Sprintf("%d", line)) + + if col > 0 { + q.Set("col", fmt.Sprintf("%d", col)) + } + } + + u.RawQuery = q.Encode() + + return u.String() +}