gopls/internal/server: add memoryLimit setting

This setting imposes a soft limit on memory usage of the gopls
process via the runtime/debug.SetMemoryLimit mechanism.
This feature is experimental. It may permit users to trade
more CPU for lower steady state memory usage; or it may cause
GC thrashing if set too low.

Thanks to Brian Dillmann at Cockroach labs for contributing it.
(See https://github.com/golang/tools/pull/651.)

+ relnote

Change-Id: Idaf6390787a8895336ec27071e67014f4a17982a
Reviewed-on: https://go-review.googlesource.com/c/tools/+/794882
Reviewed-by: Hongxiang Jiang <hxjiang@golang.org>
Auto-Submit: Alan Donovan <adonovan@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/gopls/doc/release/v0.24.0.md b/gopls/doc/release/v0.24.0.md
new file mode 100644
index 0000000..4b248c7
--- /dev/null
+++ b/gopls/doc/release/v0.24.0.md
@@ -0,0 +1,31 @@
+---
+title: "Gopls release v0.24.0 (expected Sep 2026)"
+---
+
+In this release:
+
+- [commits](https://go.googlesource.com/tools/+log/refs/heads/gopls-release-branch.0.23..refs/heads/gopls-release-branch.0.24)
+- [issues closed](https://github.com/golang/go/milestone/440?closed=1)
+
+Key features are described below.
+
+<!-- TODO: 1-line overview of release -->
+
+## Configuration changes
+
+The `memoryLimit` setting allows clients to set a soft memory limit on
+the gopls process using
+[runtime/debug.SetMemoryLimit](https://pkg.go.dev/runtime/debug#SetMemoryLimit).
+This feature is experimental. It may permit users to trade more CPU
+for lower steady state memory usage; but it may cause GC thrashing if
+set too low.
+
+## Web-based features
+
+## Editing features
+
+## Analysis features
+
+<!-- TODO Gopls is now using staticcheck [v0.8.0-rc1](https://github.com/dominikh/go-tools/releases/tag/2026.2rc1). -->
+
+## Code transformation features
diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md
index 93e3e87..8e0602c 100644
--- a/gopls/doc/settings.md
+++ b/gopls/doc/settings.md
@@ -683,6 +683,31 @@
 
 Default: `0`.
 
+<a id='memoryLimit'></a>
+### `memoryLimit int64`
+
+**This setting is experimental and may be deleted.**
+
+memoryLimit sets a soft memory limit (in bytes) for the gopls process, via
+runtime/debug.SetMemoryLimit. If non-positive (the default), no limit is set.
+
+On large workspaces, a single edit that invalidates many
+packages (for example a syntax error in a widely-imported
+package) can make the heap briefly grow well above the
+steady-state working set before the garbage collector
+catches up, spiking memory and, on memory-constrained
+machines, causing swapping. A soft limit makes the GC work
+harder to stay near the limit, trading some CPU for a lower
+memory peak.
+
+The limit is soft and may be exceeded. Set it comfortably above the
+steady-state working set, as too low a value causes excessive GC.
+
+Unlike the GOMEMLIMIT environment variable, this setting is
+strictly numeric; SI suffixes are not permitted.
+
+Default: `0`.
+
 <a id='verboseOutput'></a>
 ### `verboseOutput bool`
 
diff --git a/gopls/internal/doc/api.json b/gopls/internal/doc/api.json
index 24efa51..c01dbca 100644
--- a/gopls/internal/doc/api.json
+++ b/gopls/internal/doc/api.json
@@ -2349,6 +2349,20 @@
 				"DeprecationMessage": ""
 			},
 			{
+				"Name": "memoryLimit",
+				"Type": "int64",
+				"Doc": "memoryLimit sets a soft memory limit (in bytes) for the gopls process, via\nruntime/debug.SetMemoryLimit. If non-positive (the default), no limit is set.\n\nOn large workspaces, a single edit that invalidates many\npackages (for example a syntax error in a widely-imported\npackage) can make the heap briefly grow well above the\nsteady-state working set before the garbage collector\ncatches up, spiking memory and, on memory-constrained\nmachines, causing swapping. A soft limit makes the GC work\nharder to stay near the limit, trading some CPU for a lower\nmemory peak.\n\nThe limit is soft and may be exceeded. Set it comfortably above the\nsteady-state working set, as too low a value causes excessive GC.\n\nUnlike the GOMEMLIMIT environment variable, this setting is\nstrictly numeric; SI suffixes are not permitted.\n",
+				"EnumKeys": {
+					"ValueType": "",
+					"Keys": null
+				},
+				"EnumValues": null,
+				"Default": "0",
+				"Status": "experimental",
+				"Hierarchy": "",
+				"DeprecationMessage": ""
+			},
+			{
 				"Name": "verboseOutput",
 				"Type": "bool",
 				"Doc": "verboseOutput enables additional debug logging.\n",
diff --git a/gopls/internal/protocol/generate/main.go b/gopls/internal/protocol/generate/main.go
index a879646..9deeb36 100644
--- a/gopls/internal/protocol/generate/main.go
+++ b/gopls/internal/protocol/generate/main.go
@@ -270,8 +270,11 @@
 func writejsons() {
 	out := new(bytes.Buffer)
 	fmt.Fprintln(out, fileHdr)
-	out.WriteString("import \"encoding/json\"\n\n")
-	out.WriteString("import \"fmt\"\n")
+	out.WriteString(`import (
+	"encoding/json"
+	"fmt"
+)
+`)
 
 	out.WriteString(`
 // UnmarshalError indicates that a JSON value did not conform to
diff --git a/gopls/internal/server/general.go b/gopls/internal/server/general.go
index 420a702..37846a0 100644
--- a/gopls/internal/server/general.go
+++ b/gopls/internal/server/general.go
@@ -16,6 +16,7 @@
 	"os"
 	"path"
 	"path/filepath"
+	runtimedebug "runtime/debug"
 	"slices"
 	"sort"
 	"strings"
@@ -71,9 +72,7 @@
 	}
 	options.ForClientCapabilities(params.ClientInfo, params.Capabilities)
 
-	if options.MaxFileCacheBytes > 0 {
-		filecache.SetBudget(options.MaxFileCacheBytes)
-	}
+	updateGlobalOptions(options)
 
 	if options.ShowBugReports {
 		// Report the next bug that occurs on the server.
@@ -277,6 +276,17 @@
 	}, nil
 }
 
+// updateGlobalOptions updates process-wide settings
+// received by a DidChangeConfiguration or Initialize request.
+func updateGlobalOptions(options *settings.Options) {
+	if options.MaxFileCacheBytes > 0 {
+		filecache.SetBudget(options.MaxFileCacheBytes)
+	}
+	if options.MemoryLimit > 0 {
+		runtimedebug.SetMemoryLimit(options.MemoryLimit)
+	}
+}
+
 func (s *server) Initialized(ctx context.Context, params *protocol.InitializedParams) error {
 	ctx, done := event.Start(ctx, "server.Initialized")
 	defer done()
diff --git a/gopls/internal/server/workspace.go b/gopls/internal/server/workspace.go
index 66d682a..7e6a7dc 100644
--- a/gopls/internal/server/workspace.go
+++ b/gopls/internal/server/workspace.go
@@ -13,7 +13,6 @@
 
 	"golang.org/x/tools/gopls/internal/cache"
 	"golang.org/x/tools/gopls/internal/file"
-	"golang.org/x/tools/gopls/internal/filecache"
 	"golang.org/x/tools/gopls/internal/golang/completion"
 	"golang.org/x/tools/gopls/internal/protocol"
 	"golang.org/x/tools/gopls/internal/settings"
@@ -144,9 +143,7 @@
 	// An options change may have affected the detected Go version.
 	s.checkViewGoVersions()
 
-	if options.MaxFileCacheBytes > 0 {
-		filecache.SetBudget(options.MaxFileCacheBytes)
-	}
+	updateGlobalOptions(options)
 
 	return nil
 }
diff --git a/gopls/internal/settings/settings.go b/gopls/internal/settings/settings.go
index b0a6477..19bf723 100644
--- a/gopls/internal/settings/settings.go
+++ b/gopls/internal/settings/settings.go
@@ -699,6 +699,25 @@
 	// as measured by du(1) may be significantly higher.
 	MaxFileCacheBytes int64 `status:"experimental"`
 
+	// MemoryLimit sets a soft memory limit (in bytes) for the gopls process, via
+	// runtime/debug.SetMemoryLimit. If non-positive (the default), no limit is set.
+	//
+	// On large workspaces, a single edit that invalidates many
+	// packages (for example a syntax error in a widely-imported
+	// package) can make the heap briefly grow well above the
+	// steady-state working set before the garbage collector
+	// catches up, spiking memory and, on memory-constrained
+	// machines, causing swapping. A soft limit makes the GC work
+	// harder to stay near the limit, trading some CPU for a lower
+	// memory peak.
+	//
+	// The limit is soft and may be exceeded. Set it comfortably above the
+	// steady-state working set, as too low a value causes excessive GC.
+	//
+	// Unlike the GOMEMLIMIT environment variable, this setting is
+	// strictly numeric; SI suffixes are not permitted.
+	MemoryLimit int64 `status:"experimental"`
+
 	// VerboseOutput enables additional debug logging.
 	VerboseOutput bool `status:"debug"`
 }
@@ -1336,6 +1355,9 @@
 	case "maxFileCacheBytes":
 		return setInt64(&o.MaxFileCacheBytes, value)
 
+	case "memoryLimit":
+		return setInt64(&o.MemoryLimit, value)
+
 	case "verboseOutput":
 		return setBool(&o.VerboseOutput, value)