// The Problems window: every diagnostic the language server has reported. package app import ( "fmt" "path/filepath" "strings" "codeberg.org/turbo-editors/turbo-core/editor" "codeberg.org/turbo-editors/turbo-core/lsp" "codeberg.org/turbo-editors/turbo-core/ui" ) // ShowProblems lists every problem the language server has reported, for every // file it has spoken about, and goes to the one chosen. // // Every file, not only the one in front. A server publishes diagnostics for // the whole package it has loaded, so the file with the error is very often // not the file being edited — which is exactly the case a list is worth // having for. The status bar already shows the first error in the current // file; this is the rest of what the editor already knows. func (a *App) ShowProblems() { problems := a.language.AllDiagnostics() if len(problems) == 0 { a.Message(a.noProblemsMessage()) return } labels := make([]string, 0, len(problems)) for _, problem := range problems { labels = append(labels, problemLabel(problem)) } dialog, list := NewChoiceDialog(fmt.Sprintf("Problems (%d)", len(problems)), labels, 0, a.screenRect()) a.pushModal(dialog, func(result ui.Result) { if result == ui.ResultOK && list.Selected() >= 0 { problem := problems[list.Selected()] a.jumpTo(lsp.Location{URI: lsp.PathToURI(problem.Path), Range: problem.Diagnostic.Range}) } }) } // noProblemsMessage tells "there is nothing wrong" apart from "nothing has // looked yet", which are the same empty list and very different news. func (a *App) noProblemsMessage() string { if !a.language.Ready() { return a.language.Status() } return "No problems reported" } // problemLabel is how one problem reads in the list: how bad it is, where it // is, and what it says. // // The message is put last and left whole. Servers write long ones — a Rust // borrow-checker error runs to a paragraph — and truncating them here would // cut off the part that names the variable. The list scrolls sideways instead. func problemLabel(problem FileDiagnostic) string { return fmt.Sprintf("%s %s:%d %s", severityTag(problem.Diagnostic.Severity), filepath.Base(problem.Path), problem.Diagnostic.Range.Start.Line+1, strings.ReplaceAll(problem.Diagnostic.Message, "\n", " ")) } // severityTag is the fixed-width word in front of a problem, so the column of // messages lines up whatever the severities are. func severityTag(severity lsp.Severity) string { switch severity { case lsp.SeverityError: return "error " case lsp.SeverityWarning: return "warning" case lsp.SeverityInformation: return "info " case lsp.SeverityHint: return "hint " } return " " } // refreshMarks tells every editing window which of its lines the language // server has something to say about. // // It runs whenever the diagnostics change and whenever a window opens, because // both change the answer and neither is the other's business to notice. func (a *App) refreshMarks() { for _, window := range a.desktop.Windows() { view, ok := editorViewOf(window) if !ok { continue } view.SetMarks(marksFor(a.language.Diagnostics(view.Buffer().Path()))) } } // marksFor turns a file's diagnostics into one mark per line. // // A line with several problems is marked with its **worst** one: the gutter // has one column, and a line that is both an error and a hint is a line you // want to know is an error. func marksFor(diagnostics []lsp.Diagnostic) map[int]editor.Severity { if len(diagnostics) == 0 { return nil } marks := map[int]editor.Severity{} for _, diagnostic := range diagnostics { line := diagnostic.Range.Start.Line if severity := markSeverity(diagnostic.Severity); severity > marks[line] { marks[line] = severity } } return marks } // markSeverity translates the protocol's severity into the editor's own. // // A diagnostic with no severity is an error: the specification leaves it to // the client, and a problem nobody graded is not one to draw quietly. func markSeverity(severity lsp.Severity) editor.Severity { switch severity { case lsp.SeverityWarning: return editor.MarkWarning case lsp.SeverityInformation: return editor.MarkInformation case lsp.SeverityHint: return editor.MarkHint } return editor.MarkError }