godoc: remove more global variables

More moves into Corpus and Presentation.

R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/11492043
diff --git a/godoc/corpus.go b/godoc/corpus.go
index 87d33bd..fb88944 100644
--- a/godoc/corpus.go
+++ b/godoc/corpus.go
@@ -7,6 +7,7 @@
 import (
 	"errors"
 	pathpkg "path"
+	"time"
 
 	"code.google.com/p/go.tools/godoc/util"
 	"code.google.com/p/go.tools/godoc/vfs"
@@ -46,6 +47,8 @@
 	fsModified  util.RWValue // timestamp of last call to invalidateIndex
 	docMetadata util.RWValue // mapping from paths to *Metadata
 
+	// SearchIndex is the search index in use.
+	searchIndex util.RWValue
 }
 
 // NewCorpus returns a new Corpus from a filesystem.
@@ -61,6 +64,17 @@
 	return c
 }
 
+func (c *Corpus) CurrentIndex() (*Index, time.Time) {
+	v, t := c.searchIndex.Get()
+	idx, _ := v.(*Index)
+	return idx, t
+}
+
+func (c *Corpus) FSModifiedTime() time.Time {
+	_, ts := c.fsModified.Get()
+	return ts
+}
+
 // Init initializes Corpus, once options on Corpus are set.
 // It must be called before any subsequent method calls.
 func (c *Corpus) Init() error {
diff --git a/godoc/dirtrees.go b/godoc/dirtrees.go
index edf36b8..9316c19 100644
--- a/godoc/dirtrees.go
+++ b/godoc/dirtrees.go
@@ -86,7 +86,7 @@
 			// though the directory doesn't contain any real package files - was bug)
 			if synopses[0] == "" {
 				// no "optimal" package synopsis yet; continue to collect synopses
-				file, err := parseFile(fset, pathpkg.Join(path, d.Name()),
+				file, err := b.c.parseFile(fset, pathpkg.Join(path, d.Name()),
 					parser.ParseComments|parser.PackageClauseOnly)
 				if err == nil {
 					hasPkgFiles = true
diff --git a/godoc/godoc.go b/godoc/godoc.go
index e400f6d..093f007 100644
--- a/godoc/godoc.go
+++ b/godoc/godoc.go
@@ -27,46 +27,10 @@
 	"time"
 	"unicode"
 	"unicode/utf8"
-
-	"code.google.com/p/go.tools/godoc/util"
-	"code.google.com/p/go.tools/godoc/vfs"
 )
 
-// FS is the file system that godoc reads from and serves.
-// It is a virtual file system that operates on slash-separated paths,
-// and its root corresponds to the Go distribution root: /src/pkg
-// holds the source tree, and so on.  This means that the URLs served by
-// the godoc server are the same as the paths in the virtual file
-// system, which helps keep things simple.
-// TODO(bradfitz): delete this global
-var FS = vfs.NameSpace{}
-
-// Old flags
-var (
-	// DeclLinks controls whether identifers are linked to their declaration.
-	DeclLinks = true
-
-	// ShowExamples controls whether to show examples in command-line mode.
-	// TODO(bradfitz,adg): delete this flag
-	ShowExamples = false
-
-	// ShowPlayground controls whether to enable the playground in
-	// the web interface.
-	// TODO(bradfitz,adg): delete this flag
-	ShowPlayground = false
-
-	ShowTimestamps = false
-
-	Verbose = false
-
-	TabWidth = 4
-
-	// regular expression matching note markers to show
-	NotesRx = "BUG"
-)
-
-// SearchIndex is the search index in use.
-var SearchIndex util.RWValue
+// Verbose controls logging verbosity.
+var Verbose = false
 
 // Fake relative package path for built-ins. Documentation for all globals
 // (not just exported ones) will be shown for packages in this directory.
@@ -77,39 +41,57 @@
 // Convention: template function names ending in "_html" or "_url" produce
 //             HTML- or URL-escaped strings; all other function results may
 //             require explicit escaping in the template.
-var FuncMap = template.FuncMap{
-	// various helpers
-	"filename": filenameFunc,
-	"repeat":   strings.Repeat,
+func (p *Presentation) FuncMap() template.FuncMap {
+	p.initFuncMapOnce.Do(p.initFuncMap)
+	return p.funcMap
+}
 
-	// access to FileInfos (directory listings)
-	"fileInfoName": fileInfoNameFunc,
-	"fileInfoTime": fileInfoTimeFunc,
+func (p *Presentation) TemplateFuncs() template.FuncMap {
+	p.initFuncMapOnce.Do(p.initFuncMap)
+	return p.templateFuncs
+}
 
-	// access to search result information
-	"infoKind_html":    infoKind_htmlFunc,
-	"infoLine":         infoLineFunc,
-	"infoSnippet_html": infoSnippet_htmlFunc,
+func (p *Presentation) initFuncMap() {
+	if p.Corpus == nil {
+		panic("nil Presentation.Corpus")
+	}
+	p.templateFuncs = template.FuncMap{
+		"code": p.code,
+	}
+	p.funcMap = template.FuncMap{
+		// various helpers
+		"filename": filenameFunc,
+		"repeat":   strings.Repeat,
 
-	// formatting of AST nodes
-	"node":         nodeFunc,
-	"node_html":    node_htmlFunc,
-	"comment_html": comment_htmlFunc,
-	"comment_text": comment_textFunc,
+		// access to FileInfos (directory listings)
+		"fileInfoName": fileInfoNameFunc,
+		"fileInfoTime": fileInfoTimeFunc,
 
-	// support for URL attributes
-	"pkgLink":     pkgLinkFunc,
-	"srcLink":     srcLinkFunc,
-	"posLink_url": posLink_urlFunc,
+		// access to search result information
+		"infoKind_html":    infoKind_htmlFunc,
+		"infoLine":         p.infoLineFunc,
+		"infoSnippet_html": p.infoSnippet_htmlFunc,
 
-	// formatting of Examples
-	"example_html":   example_htmlFunc,
-	"example_text":   example_textFunc,
-	"example_name":   example_nameFunc,
-	"example_suffix": example_suffixFunc,
+		// formatting of AST nodes
+		"node":         p.nodeFunc,
+		"node_html":    p.node_htmlFunc,
+		"comment_html": comment_htmlFunc,
+		"comment_text": comment_textFunc,
 
-	// formatting of Notes
-	"noteTitle": noteTitle,
+		// support for URL attributes
+		"pkgLink":     pkgLinkFunc,
+		"srcLink":     srcLinkFunc,
+		"posLink_url": posLink_urlFunc,
+
+		// formatting of Examples
+		"example_html":   p.example_htmlFunc,
+		"example_text":   p.example_textFunc,
+		"example_name":   p.example_nameFunc,
+		"example_suffix": p.example_suffixFunc,
+
+		// formatting of Notes
+		"noteTitle": noteTitle,
+	}
 }
 
 func filenameFunc(path string) string {
@@ -148,10 +130,10 @@
 	return infoKinds[info.Kind()] // infoKind entries are html-escaped
 }
 
-func infoLineFunc(info SpotInfo) int {
+func (p *Presentation) infoLineFunc(info SpotInfo) int {
 	line := info.Lori()
 	if info.IsIndex() {
-		index, _ := SearchIndex.Get()
+		index, _ := p.Corpus.searchIndex.Get()
 		if index != nil {
 			line = index.(*Index).Snippet(line).Line
 		} else {
@@ -165,27 +147,27 @@
 	return line
 }
 
-func infoSnippet_htmlFunc(info SpotInfo) string {
+func (p *Presentation) infoSnippet_htmlFunc(info SpotInfo) string {
 	if info.IsIndex() {
-		index, _ := SearchIndex.Get()
+		index, _ := p.Corpus.searchIndex.Get()
 		// Snippet.Text was HTML-escaped when it was generated
 		return index.(*Index).Snippet(info.Lori()).Text
 	}
 	return `<span class="alert">no snippet text available</span>`
 }
 
-func nodeFunc(info *PageInfo, node interface{}) string {
+func (p *Presentation) nodeFunc(info *PageInfo, node interface{}) string {
 	var buf bytes.Buffer
-	writeNode(&buf, info.FSet, node)
+	p.writeNode(&buf, info.FSet, node)
 	return buf.String()
 }
 
-func node_htmlFunc(info *PageInfo, node interface{}, linkify bool) string {
+func (p *Presentation) node_htmlFunc(info *PageInfo, node interface{}, linkify bool) string {
 	var buf1 bytes.Buffer
-	writeNode(&buf1, info.FSet, node)
+	p.writeNode(&buf1, info.FSet, node)
 
 	var buf2 bytes.Buffer
-	if n, _ := node.(ast.Node); n != nil && linkify && DeclLinks {
+	if n, _ := node.(ast.Node); n != nil && linkify && p.DeclLinks {
 		LinkifyText(&buf2, buf1.Bytes(), n)
 	} else {
 		FormatText(&buf2, buf1.Bytes(), -1, true, "", nil)
@@ -307,8 +289,8 @@
 	return pathpkg.Clean("/" + s)
 }
 
-func example_textFunc(info *PageInfo, funcName, indent string) string {
-	if !ShowExamples {
+func (p *Presentation) example_textFunc(info *PageInfo, funcName, indent string) string {
+	if !p.ShowExamples {
 		return ""
 	}
 
@@ -328,7 +310,7 @@
 		// print code
 		cnode := &printer.CommentedNode{Node: eg.Code, Comments: eg.Comments}
 		var buf1 bytes.Buffer
-		writeNode(&buf1, info.FSet, cnode)
+		p.writeNode(&buf1, info.FSet, cnode)
 		code := buf1.String()
 		// Additional formatting if this is a function body.
 		if n := len(code); n >= 2 && code[0] == '{' && code[n-1] == '}' {
@@ -348,7 +330,7 @@
 	return buf.String()
 }
 
-func example_htmlFunc(info *PageInfo, funcName string) string {
+func (p *Presentation) example_htmlFunc(info *PageInfo, funcName string) string {
 	var buf bytes.Buffer
 	for _, eg := range info.Examples {
 		name := stripExampleSuffix(eg.Name)
@@ -359,7 +341,7 @@
 
 		// print code
 		cnode := &printer.CommentedNode{Node: eg.Code, Comments: eg.Comments}
-		code := node_htmlFunc(info, cnode, true)
+		code := p.node_htmlFunc(info, cnode, true)
 		out := eg.Output
 		wholeFile := true
 
@@ -379,7 +361,7 @@
 		// Write out the playground code in standard Go style
 		// (use tabs, no comment highlight, etc).
 		play := ""
-		if eg.Play != nil && ShowPlayground {
+		if eg.Play != nil && p.ShowPlayground {
 			var buf bytes.Buffer
 			if err := format.Node(&buf, info.FSet, eg.Play); err != nil {
 				log.Print(err)
@@ -410,7 +392,7 @@
 
 // example_nameFunc takes an example function name and returns its display
 // name. For example, "Foo_Bar_quux" becomes "Foo.Bar (Quux)".
-func example_nameFunc(s string) string {
+func (p *Presentation) example_nameFunc(s string) string {
 	name, suffix := splitExampleName(s)
 	// replace _ with . for method names
 	name = strings.Replace(name, "_", ".", 1)
@@ -423,7 +405,7 @@
 
 // example_suffixFunc takes an example function name and returns its suffix in
 // parenthesized form. For example, "Foo_Bar_quux" becomes " (Quux)".
-func example_suffixFunc(name string) string {
+func (p *Presentation) example_suffixFunc(name string) string {
 	_, suffix := splitExampleName(name)
 	return suffix
 }
@@ -462,7 +444,7 @@
 }
 
 // Write an AST node to w.
-func writeNode(w io.Writer, fset *token.FileSet, x interface{}) {
+func (p *Presentation) writeNode(w io.Writer, fset *token.FileSet, x interface{}) {
 	// convert trailing tabs into spaces using a tconv filter
 	// to ensure a good outcome in most browsers (there may still
 	// be tabs in comments and strings, but converting those into
@@ -472,10 +454,13 @@
 	//           with an another printer mode (which is more efficiently
 	//           implemented in the printer than here with another layer)
 	mode := printer.TabIndent | printer.UseSpaces
-	err := (&printer.Config{Mode: mode, Tabwidth: TabWidth}).Fprint(&tconv{output: w}, fset, x)
+	err := (&printer.Config{Mode: mode, Tabwidth: p.TabWidth}).Fprint(&tconv{p: p, output: w}, fset, x)
 	if err != nil {
 		log.Print(err)
 	}
 }
 
-var WriteNode = writeNode
+// WriteNote writes x to w.
+func (p *Presentation) WriteNode(w io.Writer, fset *token.FileSet, x interface{}) {
+	p.writeNode(w, fset, x)
+}
diff --git a/godoc/index.go b/godoc/index.go
index d7c7920..3040a35 100644
--- a/godoc/index.go
+++ b/godoc/index.go
@@ -62,19 +62,6 @@
 	"code.google.com/p/go.tools/godoc/util"
 )
 
-// TODO(bradfitz,adg): legacy flag vars. clean up.
-var (
-	MaxResults = 1000
-
-	// index throttle value; 0.0 = no time allocated, 1.0 = full throttle
-	IndexThrottle float64 = 0.75
-
-	// IndexFiles is a glob pattern specifying index files; if
-	// not empty, the index is read from these files in sorted
-	// order")
-	IndexFiles string
-)
-
 // ----------------------------------------------------------------------------
 // InterfaceSlice is a helper type for sorting interface
 // slices according to some slice-specific sort criteria.
@@ -384,6 +371,7 @@
 // interface for walking file trees, and the ast.Visitor interface for
 // walking Go ASTs.
 type Indexer struct {
+	c        *Corpus
 	fset     *token.FileSet          // file set for all indexed files
 	sources  bytes.Buffer            // concatenated sources
 	packages map[string]*Pak         // map of canonicalized *Paks
@@ -549,7 +537,7 @@
 // failed (that is, if the file was not added), it returns file == nil.
 func (x *Indexer) addFile(filename string, goFile bool) (file *token.File, ast *ast.File) {
 	// open file
-	f, err := FS.Open(filename)
+	f, err := x.c.fs.Open(filename)
 	if err != nil {
 		return
 	}
@@ -716,19 +704,20 @@
 // NewIndex creates a new index for the .go files
 // in the directories given by dirnames.
 //
-func NewIndex(dirnames <-chan string, fulltextIndex bool, throttle float64) *Index {
+func NewIndex(c *Corpus, dirnames <-chan string, fulltextIndex bool, throttle float64) *Index {
 	var x Indexer
 	th := util.NewThrottle(throttle, 100*time.Millisecond) // run at least 0.1s at a time
 
 	// initialize Indexer
 	// (use some reasonably sized maps to start)
+	x.c = c
 	x.fset = token.NewFileSet()
 	x.packages = make(map[string]*Pak, 256)
 	x.words = make(map[string]*IndexResult, 8192)
 
 	// index all files in the directories given by dirnames
 	for dirname := range dirnames {
-		list, err := FS.ReadDir(dirname)
+		list, err := c.fs.ReadDir(dirname)
 		if err != nil {
 			continue // ignore this directory
 		}
@@ -1046,19 +1035,19 @@
 // indexUpToDate() returns true if the search index is not older
 // than any of the file systems under godoc's observation.
 //
-func indexUpToDate() bool {
-	_, fsTime := FSModified.Get()
-	_, siTime := SearchIndex.Get()
+func (c *Corpus) indexUpToDate() bool {
+	_, fsTime := c.fsModified.Get()
+	_, siTime := c.searchIndex.Get()
 	return !fsTime.After(siTime)
 }
 
 // feedDirnames feeds the directory names of all directories
 // under the file system given by root to channel c.
 //
-func feedDirnames(root *util.RWValue, c chan<- string) {
-	if dir, _ := root.Get(); dir != nil {
+func (c *Corpus) feedDirnames(ch chan<- string) {
+	if dir, _ := c.fsTree.Get(); dir != nil {
 		for d := range dir.(*Directory).iter(false) {
-			c <- d.Path
+			ch <- d.Path
 		}
 	}
 }
@@ -1066,16 +1055,16 @@
 // fsDirnames() returns a channel sending all directory names
 // of all the file systems under godoc's observation.
 //
-func fsDirnames() <-chan string {
-	c := make(chan string, 256) // buffered for fewer context switches
+func (c *Corpus) fsDirnames() <-chan string {
+	ch := make(chan string, 256) // buffered for fewer context switches
 	go func() {
-		feedDirnames(&FSTree, c)
-		close(c)
+		c.feedDirnames(ch)
+		close(ch)
 	}()
-	return c
+	return ch
 }
 
-func readIndex(filenames string) error {
+func (c *Corpus) readIndex(filenames string) error {
 	matches, err := filepath.Glob(filenames)
 	if err != nil {
 		return err
@@ -1096,18 +1085,18 @@
 	if err := x.Read(io.MultiReader(files...)); err != nil {
 		return err
 	}
-	SearchIndex.Set(x)
+	c.searchIndex.Set(x)
 	return nil
 }
 
-func UpdateIndex() {
+func (c *Corpus) UpdateIndex() {
 	if Verbose {
 		log.Printf("updating index...")
 	}
 	start := time.Now()
-	index := NewIndex(fsDirnames(), MaxResults > 0, IndexThrottle)
+	index := NewIndex(c, c.fsDirnames(), c.MaxResults > 0, c.IndexThrottle)
 	stop := time.Now()
-	SearchIndex.Set(index)
+	c.searchIndex.Set(index)
 	if Verbose {
 		secs := stop.Sub(start).Seconds()
 		stats := index.Stats()
@@ -1123,19 +1112,19 @@
 }
 
 // RunIndexer runs forever, indexing.
-func RunIndexer() {
+func (c *Corpus) RunIndexer() {
 	// initialize the index from disk if possible
-	if IndexFiles != "" {
-		if err := readIndex(IndexFiles); err != nil {
+	if c.IndexFiles != "" {
+		if err := c.readIndex(c.IndexFiles); err != nil {
 			log.Printf("error reading index: %s", err)
 		}
 	}
 
 	// repeatedly update the index when it goes out of date
 	for {
-		if !indexUpToDate() {
+		if !c.indexUpToDate() {
 			// index possibly out of date - make a new one
-			UpdateIndex()
+			c.UpdateIndex()
 		}
 		delay := 60 * time.Second // by default, try every 60s
 		if false {                // TODO(bradfitz): was: *testDir != "" {
diff --git a/godoc/meta.go b/godoc/meta.go
index 86f25e6..ae8ee5a 100644
--- a/godoc/meta.go
+++ b/godoc/meta.go
@@ -99,14 +99,14 @@
 		}
 	}
 	scan("/doc")
-	DocMetadata.Set(metadata)
+	c.docMetadata.Set(metadata)
 }
 
 // MetadataFor returns the *Metadata for a given relative path or nil if none
 // exists.
 //
-func MetadataFor(relpath string) *Metadata {
-	if m, _ := DocMetadata.Get(); m != nil {
+func (c *Corpus) MetadataFor(relpath string) *Metadata {
+	if m, _ := c.docMetadata.Get(); m != nil {
 		meta := m.(map[string]*Metadata)
 		// If metadata for this relpath exists, return it.
 		if p := meta[relpath]; p != nil {
diff --git a/godoc/page.go b/godoc/page.go
index f8ee60e..1893411 100644
--- a/godoc/page.go
+++ b/godoc/page.go
@@ -42,7 +42,7 @@
 		page.Tabtitle = page.Title
 	}
 	page.SearchBox = p.Corpus.IndexEnabled
-	page.Playground = ShowPlayground
+	page.Playground = p.ShowPlayground
 	page.Version = runtime.Version()
 	if err := GodocHTML.Execute(w, page); err != nil && err != http.ErrBodyNotAllowed {
 		// Only log if there's an error that's not about writing on HEAD requests.
diff --git a/godoc/parser.go b/godoc/parser.go
index d8b5779..a27d4fd 100644
--- a/godoc/parser.go
+++ b/godoc/parser.go
@@ -16,19 +16,19 @@
 	"code.google.com/p/go.tools/godoc/vfs"
 )
 
-func parseFile(fset *token.FileSet, filename string, mode parser.Mode) (*ast.File, error) {
-	src, err := vfs.ReadFile(FS, filename)
+func (c *Corpus) parseFile(fset *token.FileSet, filename string, mode parser.Mode) (*ast.File, error) {
+	src, err := vfs.ReadFile(c.fs, filename)
 	if err != nil {
 		return nil, err
 	}
 	return parser.ParseFile(fset, filename, src, mode)
 }
 
-func parseFiles(fset *token.FileSet, abspath string, localnames []string) (map[string]*ast.File, error) {
+func (c *Corpus) parseFiles(fset *token.FileSet, abspath string, localnames []string) (map[string]*ast.File, error) {
 	files := make(map[string]*ast.File)
 	for _, f := range localnames {
 		absname := pathpkg.Join(abspath, f)
-		file, err := parseFile(fset, absname, parser.ParseComments)
+		file, err := c.parseFile(fset, absname, parser.ParseComments)
 		if err != nil {
 			return nil, err
 		}
diff --git a/godoc/pres.go b/godoc/pres.go
index a002b3a..3191ab8 100644
--- a/godoc/pres.go
+++ b/godoc/pres.go
@@ -5,6 +5,8 @@
 package godoc
 
 import (
+	"regexp"
+	"sync"
 	"text/template"
 )
 
@@ -19,6 +21,12 @@
 	ShowPlayground bool
 	ShowExamples   bool
 	DeclLinks      bool
+
+	NotesRx *regexp.Regexp
+
+	initFuncMapOnce sync.Once
+	funcMap         template.FuncMap
+	templateFuncs   template.FuncMap
 }
 
 // NewPresentation returns a new Presentation from a corpus.
@@ -33,7 +41,3 @@
 		DeclLinks:    true,
 	}
 }
-
-func (p *Presentation) FuncMap() template.FuncMap {
-	panic("")
-}
diff --git a/godoc/server.go b/godoc/server.go
index c4793ed..9b2149b 100644
--- a/godoc/server.go
+++ b/godoc/server.go
@@ -19,7 +19,6 @@
 	"os"
 	pathpkg "path"
 	"path/filepath"
-	"regexp"
 	"strings"
 	"text/template"
 	"time"
@@ -35,11 +34,6 @@
 	FileServer http.Handler // default file server
 	CmdHandler Server
 	PkgHandler Server
-
-	// file system information
-	FSTree      util.RWValue // *Directory tree of packages, updated with each sync (but sync code is removed now)
-	FSModified  util.RWValue // timestamp of last call to invalidateIndex
-	DocMetadata util.RWValue // mapping from paths to *Metadata
 )
 
 func InitHandlers(p *Presentation) {
@@ -83,8 +77,17 @@
 	// set ctxt.GOOS and ctxt.GOARCH before calling ctxt.ImportDir.
 	ctxt := build.Default
 	ctxt.IsAbsPath = pathpkg.IsAbs
-	ctxt.ReadDir = fsReadDir
-	ctxt.OpenFile = fsOpenFile
+	ctxt.ReadDir = func(dir string) ([]os.FileInfo, error) {
+		return h.c.fs.ReadDir(filepath.ToSlash(dir))
+	}
+	ctxt.OpenFile = func(name string) (r io.ReadCloser, err error) {
+		data, err := vfs.ReadFile(h.c.fs, filepath.ToSlash(name))
+		if err != nil {
+			return nil, err
+		}
+		return ioutil.NopCloser(bytes.NewReader(data)), nil
+	}
+
 	pkginfo, err := ctxt.ImportDir(abspath, 0)
 	// continue if there are no Go source files; we still want the directory info
 	if _, nogo := err.(*build.NoGoError); err != nil && !nogo {
@@ -109,7 +112,7 @@
 	if len(pkgfiles) > 0 {
 		// build package AST
 		fset := token.NewFileSet()
-		files, err := parseFiles(fset, abspath, pkgfiles)
+		files, err := h.c.parseFiles(fset, abspath, pkgfiles)
 		if err != nil {
 			info.Err = err
 			return info
@@ -133,7 +136,7 @@
 
 			// collect examples
 			testfiles := append(pkginfo.TestGoFiles, pkginfo.XTestGoFiles...)
-			files, err = parseFiles(fset, abspath, testfiles)
+			files, err = h.c.parseFiles(fset, abspath, testfiles)
 			if err != nil {
 				log.Println("parsing examples:", err)
 			}
@@ -142,7 +145,7 @@
 			// collect any notes that we want to show
 			if info.PDoc.Notes != nil {
 				// could regexp.Compile only once per godoc, but probably not worth it
-				if rx, err := regexp.Compile(NotesRx); err == nil {
+				if rx := h.p.NotesRx; rx != nil {
 					for m, n := range info.PDoc.Notes {
 						if rx.MatchString(m) {
 							if info.Notes == nil {
@@ -169,7 +172,7 @@
 	// get directory information, if any
 	var dir *Directory
 	var timestamp time.Time
-	if tree, ts := FSTree.Get(); tree != nil && tree.(*Directory) != nil {
+	if tree, ts := h.c.fsTree.Get(); tree != nil && tree.(*Directory) != nil {
 		// directory tree is present; lookup respective directory
 		// (may still fail if the file system was updated and the
 		// new directory tree has not yet been computed)
@@ -223,7 +226,7 @@
 	default:
 		tabtitle = info.Dirname
 		title = "Directory "
-		if ShowTimestamps {
+		if h.p.ShowTimestamps {
 			subtitle = "Last update: " + info.DirTime.String()
 		}
 	}
@@ -292,20 +295,6 @@
 	return mode
 }
 
-// fsReadDir implements ReadDir for the go/build package.
-func fsReadDir(dir string) ([]os.FileInfo, error) {
-	return FS.ReadDir(filepath.ToSlash(dir))
-}
-
-// fsOpenFile implements OpenFile for the go/build package.
-func fsOpenFile(name string) (r io.ReadCloser, err error) {
-	data, err := vfs.ReadFile(FS, filepath.ToSlash(name))
-	if err != nil {
-		return nil, err
-	}
-	return ioutil.NopCloser(bytes.NewReader(data)), nil
-}
-
 // poorMansImporter returns a (dummy) package object named
 // by the last path component of the provided package path
 // (as is the convention for packages). This is sufficient
@@ -465,7 +454,7 @@
 		return
 	}
 
-	list, err := FS.ReadDir(abspath)
+	list, err := p.Corpus.fs.ReadDir(abspath)
 	if err != nil {
 		p.ServeError(w, r, relpath, err)
 		return
@@ -480,7 +469,7 @@
 
 func (p *Presentation) ServeHTMLDoc(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
 	// get HTML body contents
-	src, err := vfs.ReadFile(FS, abspath)
+	src, err := vfs.ReadFile(p.Corpus.fs, abspath)
 	if err != nil {
 		log.Printf("ReadFile: %s", err)
 		p.ServeError(w, r, relpath, err)
@@ -502,7 +491,7 @@
 
 	// evaluate as template if indicated
 	if meta.Template {
-		tmpl, err := template.New("main").Funcs(TemplateFuncs).Parse(string(src))
+		tmpl, err := template.New("main").Funcs(p.TemplateFuncs()).Parse(string(src))
 		if err != nil {
 			log.Printf("parsing template %s: %v", relpath, err)
 			p.ServeError(w, r, relpath, err)
@@ -531,11 +520,15 @@
 	})
 }
 
+func (p *Presentation) ServeFile(w http.ResponseWriter, r *http.Request) {
+	p.serveFile(w, r)
+}
+
 func (p *Presentation) serveFile(w http.ResponseWriter, r *http.Request) {
 	relpath := r.URL.Path
 
 	// Check to see if we need to redirect or serve another file.
-	if m := MetadataFor(relpath); m != nil {
+	if m := p.Corpus.MetadataFor(relpath); m != nil {
 		if m.Path != relpath {
 			// Redirect to canonical path.
 			http.Redirect(w, r, m.Path, http.StatusMovedPermanently)
@@ -564,7 +557,7 @@
 		return
 	}
 
-	dir, err := FS.Lstat(abspath)
+	dir, err := p.Corpus.fs.Lstat(abspath)
 	if err != nil {
 		log.Print(err)
 		p.ServeError(w, r, relpath, err)
@@ -575,7 +568,7 @@
 		if redirect(w, r) {
 			return
 		}
-		if index := pathpkg.Join(abspath, "index.html"); util.IsTextFile(FS, index) {
+		if index := pathpkg.Join(abspath, "index.html"); util.IsTextFile(p.Corpus.fs, index) {
 			p.ServeHTMLDoc(w, r, index, index)
 			return
 		}
@@ -583,7 +576,7 @@
 		return
 	}
 
-	if util.IsTextFile(FS, abspath) {
+	if util.IsTextFile(p.Corpus.fs, abspath) {
 		if redirectFile(w, r) {
 			return
 		}
diff --git a/godoc/snippet.go b/godoc/snippet.go
index 1466d3a..dd9c822 100644
--- a/godoc/snippet.go
+++ b/godoc/snippet.go
@@ -21,10 +21,10 @@
 	Text string // HTML-escaped
 }
 
-func newSnippet(fset *token.FileSet, decl ast.Decl, id *ast.Ident) *Snippet {
+func (p *Presentation) newSnippet(fset *token.FileSet, decl ast.Decl, id *ast.Ident) *Snippet {
 	// TODO instead of pretty-printing the node, should use the original source instead
 	var buf1 bytes.Buffer
-	writeNode(&buf1, fset, decl)
+	p.writeNode(&buf1, fset, decl)
 	// wrap text with <pre> tag
 	var buf2 bytes.Buffer
 	buf2.WriteString("<pre>")
@@ -55,7 +55,7 @@
 	return nil
 }
 
-func genSnippet(fset *token.FileSet, d *ast.GenDecl, id *ast.Ident) *Snippet {
+func (p *Presentation) genSnippet(fset *token.FileSet, d *ast.GenDecl, id *ast.Ident) *Snippet {
 	s := findSpec(d.Specs, id)
 	if s == nil {
 		return nil //  declaration doesn't contain id - exit gracefully
@@ -71,10 +71,10 @@
 		Rparen: d.Rparen,
 	}
 
-	return newSnippet(fset, dd, id)
+	return p.newSnippet(fset, dd, id)
 }
 
-func funcSnippet(fset *token.FileSet, d *ast.FuncDecl, id *ast.Ident) *Snippet {
+func (p *Presentation) funcSnippet(fset *token.FileSet, d *ast.FuncDecl, id *ast.Ident) *Snippet {
 	if d.Name != id {
 		return nil //  declaration doesn't contain id - exit gracefully
 	}
@@ -87,19 +87,30 @@
 		Type: d.Type,
 	}
 
-	return newSnippet(fset, dd, id)
+	return p.newSnippet(fset, dd, id)
 }
 
 // NewSnippet creates a text snippet from a declaration decl containing an
 // identifier id. Parts of the declaration not containing the identifier
 // may be removed for a more compact snippet.
-//
-func NewSnippet(fset *token.FileSet, decl ast.Decl, id *ast.Ident) (s *Snippet) {
+func NewSnippet(fset *token.FileSet, decl ast.Decl, id *ast.Ident) *Snippet {
+	// TODO(bradfitz, adg): remove this function.  But it's used by indexer, which
+	// doesn't have a *Presentation, and NewSnippet needs a TabWidth.
+	var p Presentation
+	p.TabWidth = 4
+	return p.NewSnippet(fset, decl, id)
+}
+
+// NewSnippet creates a text snippet from a declaration decl containing an
+// identifier id. Parts of the declaration not containing the identifier
+// may be removed for a more compact snippet.
+func (p *Presentation) NewSnippet(fset *token.FileSet, decl ast.Decl, id *ast.Ident) *Snippet {
+	var s *Snippet
 	switch d := decl.(type) {
 	case *ast.GenDecl:
-		s = genSnippet(fset, d, id)
+		s = p.genSnippet(fset, d, id)
 	case *ast.FuncDecl:
-		s = funcSnippet(fset, d, id)
+		s = p.funcSnippet(fset, d, id)
 	}
 
 	// handle failure gracefully
@@ -108,5 +119,5 @@
 		fmt.Fprintf(&buf, `<span class="alert">could not generate a snippet for <span class="highlight">%s</span></span>`, id.Name)
 		s = &Snippet{fset.Position(id.Pos()).Line, buf.String()}
 	}
-	return
+	return s
 }
diff --git a/godoc/tab.go b/godoc/tab.go
index 012fab6..7973b74 100644
--- a/godoc/tab.go
+++ b/godoc/tab.go
@@ -16,6 +16,7 @@
 	output io.Writer
 	state  int // indenting or collecting
 	indent int // valid if state == indenting
+	p      *Presentation
 }
 
 func (p *tconv) writeIndent() (err error) {
@@ -44,7 +45,7 @@
 		case indenting:
 			switch b {
 			case '\t':
-				p.indent += TabWidth
+				p.indent += p.p.TabWidth
 			case '\n':
 				p.indent = 0
 				if _, err = p.output.Write(data[n : n+1]); err != nil {
diff --git a/godoc/template.go b/godoc/template.go
index e8c4ba4..325bc8c 100644
--- a/godoc/template.go
+++ b/godoc/template.go
@@ -37,7 +37,6 @@
 	"log"
 	"regexp"
 	"strings"
-	"text/template"
 
 	"code.google.com/p/go.tools/godoc/vfs"
 )
@@ -45,14 +44,10 @@
 // Functions in this file panic on error, but the panic is recovered
 // to an error by 'code'.
 
-var TemplateFuncs = template.FuncMap{
-	"code": code,
-}
-
 // contents reads and returns the content of the named file
 // (from the virtual file system, so for example /doc refers to $GOROOT/doc).
-func contents(name string) string {
-	file, err := vfs.ReadFile(FS, name)
+func (c *Corpus) contents(name string) string {
+	file, err := vfs.ReadFile(c.fs, name)
 	if err != nil {
 		log.Panic(err)
 	}
@@ -75,14 +70,14 @@
 	return ""
 }
 
-func code(file string, arg ...interface{}) (s string, err error) {
+func (p *Presentation) code(file string, arg ...interface{}) (s string, err error) {
 	defer func() {
 		if r := recover(); r != nil {
 			err = fmt.Errorf("%v", r)
 		}
 	}()
 
-	text := contents(file)
+	text := p.Corpus.contents(file)
 	var command string
 	switch len(arg) {
 	case 0:
@@ -90,10 +85,10 @@
 		command = fmt.Sprintf("code %q", file)
 	case 1:
 		command = fmt.Sprintf("code %q %s", file, stringFor(arg[0]))
-		text = oneLine(file, text, arg[0])
+		text = p.Corpus.oneLine(file, text, arg[0])
 	case 2:
 		command = fmt.Sprintf("code %q %s %s", file, stringFor(arg[0]), stringFor(arg[1]))
-		text = multipleLines(file, text, arg[0], arg[1])
+		text = p.Corpus.multipleLines(file, text, arg[0], arg[1])
 	default:
 		return "", fmt.Errorf("incorrect code invocation: code %q %q", file, arg)
 	}
@@ -125,8 +120,8 @@
 }
 
 // oneLine returns the single line generated by a two-argument code invocation.
-func oneLine(file, text string, arg interface{}) string {
-	lines := strings.SplitAfter(contents(file), "\n")
+func (c *Corpus) oneLine(file, text string, arg interface{}) string {
+	lines := strings.SplitAfter(c.contents(file), "\n")
 	line, pattern, isInt := parseArg(arg, file, len(lines))
 	if isInt {
 		return lines[line-1]
@@ -135,8 +130,8 @@
 }
 
 // multipleLines returns the text generated by a three-argument code invocation.
-func multipleLines(file, text string, arg1, arg2 interface{}) string {
-	lines := strings.SplitAfter(contents(file), "\n")
+func (c *Corpus) multipleLines(file, text string, arg1, arg2 interface{}) string {
+	lines := strings.SplitAfter(c.contents(file), "\n")
 	line1, pattern1, isInt1 := parseArg(arg1, file, len(lines))
 	line2, pattern2, isInt2 := parseArg(arg2, file, len(lines))
 	if !isInt1 {