Skip to content

Commit 11828ff

Browse files
committed
gopls/internal/lsp: add OnSave diagnostics
Gopls publishes diagnostics as soon as it observes issues, even while the user is in the middle of typing resulting in transient errors. Some users find this behavior rather distracting. 'diagnosticsDelay' may help avoid wasted work, but time-based decision does not always match users' expectation on when they want to see diagnostics updates. Historically, vscode-go offered two additional ways to diagnose code. * On save * Manual trigger They were implemented by running the go compiler and vet/lint tools outside gopls. Now we are working to move all code analysis logic into the language server (golang/vscode-go#2799). We need replacement for these features (golang/vscode-go#50). This change introduces a new gopls setting 'diagnosticsTrigger'. The default is 'Edit'. The additional option is 'Save', which is implemented by preventing file modification events from triggering diagnosis. This helps migrating users of the legacy "Build/Vet On Save" mode. For users of the manual trigger mode, we can consider to add the "Manual" mode and expose a custom LSP command that triggers diagnosis when we see the demand. Alternatives I explored: * Pull Model Diagnostics - LSP 3.17 introduced client-initiated diagnostics supporta The idea fits well with 'on save' or 'manual trigger' features, but I am afraid this requires non-trivial amount of work in gopls side. https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_pullDiagnostics Moreover, the state of client side implementations is unclear. For example, VS Code does not seem to support all features listed in the DiagnosticClientCapability yet. The interaction between DocumentDiagnostics and WorkspaceDiagnostics, and how mature the vscode implementation is unclear to me at this moment. * Emulate from Client-side - I attempted to buffer diagnostics reports in the LSP client middleware layer, and make them visible only when files are saved. That adds a non-trivial amount of TS/JS code on the extension side which defeats the purpose of our deprecation work. Moreover, that causes the diagnostics diff to be computed in one more place (in addition to VSCode side and Gopls side), and adds complexities. I also think some users in other editors want this feature. For golang/vscode-go#50 Change-Id: If07d3446bee7bed90851ad2272d82d163ae586cd Reviewed-on: https://go-review.googlesource.com/c/tools/+/534861 Reviewed-by: Robert Findley <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 7ca319e commit 11828ff

File tree

6 files changed

+101
-2
lines changed

6 files changed

+101
-2
lines changed

gopls/doc/settings.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,20 @@ This option must be set to a valid duration string, for example `"250ms"`.
354354

355355
Default: `"1s"`.
356356

357+
##### **diagnosticsTrigger** *enum*
358+
359+
**This setting is experimental and may be deleted.**
360+
361+
diagnosticsTrigger controls when to run diagnostics.
362+
363+
Must be one of:
364+
365+
* `"Edit"`: Trigger diagnostics on file edit and save. (default)
366+
* `"Save"`: Trigger diagnostics only on file save. Events like initial workspace load
367+
or configuration change will still trigger diagnostics.
368+
369+
Default: `"Edit"`.
370+
357371
##### **analysisProgressReporting** *bool*
358372

359373
analysisProgressReporting controls whether gopls sends progress

gopls/internal/lsp/diagnostics.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,12 @@ func computeDiagnosticHash(diags ...*source.Diagnostic) string {
155155
return fmt.Sprintf("%x", h.Sum(nil))
156156
}
157157

158-
func (s *Server) diagnoseSnapshots(snapshots map[source.Snapshot][]span.URI, onDisk bool) {
158+
func (s *Server) diagnoseSnapshots(snapshots map[source.Snapshot][]span.URI, onDisk bool, cause ModificationSource) {
159159
var diagnosticWG sync.WaitGroup
160160
for snapshot, uris := range snapshots {
161+
if snapshot.Options().DiagnosticsTrigger == source.DiagnosticsOnSave && cause == FromDidChange {
162+
continue // user requested to update the diagnostics only on save. do not diagnose yet.
163+
}
161164
diagnosticWG.Add(1)
162165
go func(snapshot source.Snapshot, uris []span.URI) {
163166
defer diagnosticWG.Done()

gopls/internal/lsp/source/api_json.go

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gopls/internal/lsp/source/options.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ func DefaultOptions(overrides ...func(*Options)) *Options {
137137
},
138138
Vulncheck: ModeVulncheckOff,
139139
DiagnosticsDelay: 1 * time.Second,
140+
DiagnosticsTrigger: DiagnosticsOnEdit,
140141
AnalysisProgressReporting: true,
141142
},
142143
InlayHintOptions: InlayHintOptions{},
@@ -468,6 +469,9 @@ type DiagnosticOptions struct {
468469
// This option must be set to a valid duration string, for example `"250ms"`.
469470
DiagnosticsDelay time.Duration `status:"advanced"`
470471

472+
// DiagnosticsTrigger controls when to run diagnostics.
473+
DiagnosticsTrigger DiagnosticsTrigger `status:"experimental"`
474+
471475
// AnalysisProgressReporting controls whether gopls sends progress
472476
// notifications when construction of its index of analysis facts is taking a
473477
// long time. Cancelling these notifications will cancel the indexing task,
@@ -810,6 +814,17 @@ const (
810814
// TODO: VulncheckRequire, VulncheckCallgraph
811815
)
812816

817+
type DiagnosticsTrigger string
818+
819+
const (
820+
// Trigger diagnostics on file edit and save. (default)
821+
DiagnosticsOnEdit DiagnosticsTrigger = "Edit"
822+
// Trigger diagnostics only on file save. Events like initial workspace load
823+
// or configuration change will still trigger diagnostics.
824+
DiagnosticsOnSave DiagnosticsTrigger = "Save"
825+
// TODO: support "Manual"?
826+
)
827+
813828
type OptionResults []OptionResult
814829

815830
type OptionResult struct {
@@ -1244,6 +1259,14 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{})
12441259
case "diagnosticsDelay":
12451260
result.setDuration(&o.DiagnosticsDelay)
12461261

1262+
case "diagnosticsTrigger":
1263+
if s, ok := result.asOneOf(
1264+
string(DiagnosticsOnEdit),
1265+
string(DiagnosticsOnSave),
1266+
); ok {
1267+
o.DiagnosticsTrigger = DiagnosticsTrigger(s)
1268+
}
1269+
12471270
case "analysisProgressReporting":
12481271
result.setBool(&o.AnalysisProgressReporting)
12491272

gopls/internal/lsp/text_synchronization.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ func (s *Server) didModifyFiles(ctx context.Context, modifications []source.File
289289

290290
wg.Add(1)
291291
go func() {
292-
s.diagnoseSnapshots(snapshots, onDisk)
292+
s.diagnoseSnapshots(snapshots, onDisk, cause)
293293
release()
294294
wg.Done()
295295
}()

gopls/internal/regtest/diagnostics/diagnostics_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2112,3 +2112,44 @@ func (B) New() {}
21122112
)
21132113
})
21142114
}
2115+
2116+
func TestDiagnosticsOnlyOnSaveFile(t *testing.T) {
2117+
const onlyMod = `
2118+
-- go.mod --
2119+
module mod.com
2120+
2121+
go 1.12
2122+
-- main.go --
2123+
package main
2124+
2125+
func main() {
2126+
Foo()
2127+
}
2128+
-- foo.go --
2129+
package main
2130+
2131+
func Foo() {}
2132+
`
2133+
WithOptions(
2134+
Settings{
2135+
"diagnosticsTrigger": "Save",
2136+
},
2137+
).Run(t, onlyMod, func(t *testing.T, env *Env) {
2138+
env.OpenFile("foo.go")
2139+
env.RegexpReplace("foo.go", "(Foo)", "Bar") // Makes reference to Foo undefined/undeclared.
2140+
env.AfterChange(NoDiagnostics()) // No diagnostics update until file save.
2141+
2142+
env.SaveBuffer("foo.go")
2143+
// Compiler's error message about undeclared names vary depending on the version,
2144+
// but must be explicit about the problematic name.
2145+
env.AfterChange(Diagnostics(env.AtRegexp("main.go", "Foo"), WithMessage("Foo")))
2146+
2147+
env.OpenFile("main.go")
2148+
env.RegexpReplace("main.go", "(Foo)", "Bar")
2149+
// No diagnostics update until file save. That results in outdated diagnostic.
2150+
env.AfterChange(Diagnostics(env.AtRegexp("main.go", "Bar"), WithMessage("Foo")))
2151+
2152+
env.SaveBuffer("main.go")
2153+
env.AfterChange(NoDiagnostics())
2154+
})
2155+
}

0 commit comments

Comments
 (0)