mirror of
https://github.com/golang/go
synced 2024-11-21 18:24:46 -07:00
godoc: support for regular expression full text search
Regular expressions may now be used in conjuction with full text search. Godoc will show the first 10000 occurences in the source code and highlight the respective text segments. - added new flag -testDir to specify a small directory for testing (fast index creation; default = "") - use new FormatText function to format text and Go source code in HTML, supporting multiple kinds of text selections simulatenously); this replaces the uses of go/printer Stylers - for now removed currently unused mechanism for identifier- specific JS popups (will come back in some form once we have type or other useful information) - various typo fixes and minor cleanups throughout Missing: - indexing of non-.go files R=r, r2 CC=golang-dev, rsc https://golang.org/cl/3699041
This commit is contained in:
parent
dd916be3d8
commit
4bdaf59cc9
23
doc/all.css
23
doc/all.css
@ -161,9 +161,30 @@ span.comment {
|
|||||||
color: #002090;
|
color: #002090;
|
||||||
}
|
}
|
||||||
span.highlight {
|
span.highlight {
|
||||||
background: #FFFF90;
|
background: #FF9900;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
span.highlight-comment {
|
||||||
|
background: #FF9900;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #002090;
|
||||||
|
}
|
||||||
|
span.selection {
|
||||||
|
background: #FFFF00
|
||||||
|
}
|
||||||
|
span.selection-comment {
|
||||||
|
color: #002090;
|
||||||
|
background: #FFFF00
|
||||||
|
}
|
||||||
|
span.selection-highlight {
|
||||||
|
background: #FF9900;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
span.selection-highlight-comment {
|
||||||
|
background: #FF9900;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #002090;
|
||||||
|
}
|
||||||
span.alert {
|
span.alert {
|
||||||
color: #D00000;
|
color: #D00000;
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,9 @@
|
|||||||
license that can be found in the LICENSE file.
|
license that can be found in the LICENSE file.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
{.section Accurate}
|
{.section Alert}
|
||||||
{.or}
|
|
||||||
<p>
|
<p>
|
||||||
<span class="alert" style="font-size:120%">Indexing in progress - result may be inaccurate.</span>
|
<span class="alert" style="font-size:120%">{@}</span>
|
||||||
</p>
|
</p>
|
||||||
{.end}
|
{.end}
|
||||||
{.section Alt}
|
{.section Alt}
|
||||||
@ -27,7 +26,7 @@
|
|||||||
{.repeated section Groups}
|
{.repeated section Groups}
|
||||||
{.repeated section Infos}
|
{.repeated section Infos}
|
||||||
<a href="/{File.Path|url-src}?h={Query|urlquery-esc}#L{@|infoLine}">{File.Path|url-src}:{@|infoLine}</a>
|
<a href="/{File.Path|url-src}?h={Query|urlquery-esc}#L{@|infoLine}">{File.Path|url-src}:{@|infoLine}</a>
|
||||||
<pre>{@|infoSnippet}</pre>
|
{@|infoSnippet}
|
||||||
{.end}
|
{.end}
|
||||||
{.end}
|
{.end}
|
||||||
{.end}
|
{.end}
|
||||||
@ -59,11 +58,11 @@
|
|||||||
{.end}
|
{.end}
|
||||||
{.section Textual}
|
{.section Textual}
|
||||||
{.section Complete}
|
{.section Complete}
|
||||||
<h2 id="Textual">{Found|html-esc} textual occurences</h2>
|
<h2 id="Textual">{Found|html-esc} textual occurrences</h2>
|
||||||
{.or}
|
{.or}
|
||||||
<h2 id="Textual">More than {Found|html-esc} textual occurences</h2>
|
<h2 id="Textual">More than {Found|html-esc} textual occurrences</h2>
|
||||||
<p>
|
<p>
|
||||||
<span class="alert" style="font-size:120%">Not all files or lines containing {Query|html-esc} are shown.</span>
|
<span class="alert" style="font-size:120%">Not all files or lines containing "{Query|html-esc}" are shown.</span>
|
||||||
</p>
|
</p>
|
||||||
{.end}
|
{.end}
|
||||||
<p>
|
<p>
|
||||||
@ -71,12 +70,16 @@
|
|||||||
{.repeated section @}
|
{.repeated section @}
|
||||||
<tr>
|
<tr>
|
||||||
<td align="left" valign="top">
|
<td align="left" valign="top">
|
||||||
<a href="/{Filename|url-src}?g={Query|urlquery-esc}">{Filename|url-src}</a>:
|
<a href="/{Filename|url-src}?h={Query|urlquery-esc}">{Filename|url-src}</a>:
|
||||||
</td>
|
</td>
|
||||||
<td align="left" width="4"></td>
|
<td align="left" width="4"></td>
|
||||||
<th align="left" valign="top">{Lines|numlines}</th>
|
<th align="left" valign="top">{Lines|numlines}</th>
|
||||||
<td align="left" width="4"></td>
|
<td align="left" width="4"></td>
|
||||||
<td align="left">{Lines Complete|linelist}</td>
|
<td align="left">
|
||||||
|
{.repeated section Lines}
|
||||||
|
<a href="/{Filename|url-src}?h={Query|urlquery-esc}#L{@|html-esc}">{@|html-esc}</a>
|
||||||
|
{.end}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{.end}
|
{.end}
|
||||||
{.section Complete}
|
{.section Complete}
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
QUERY
|
QUERY
|
||||||
{Query}
|
{Query}
|
||||||
|
|
||||||
{.section Accurate}
|
{.section Alert}
|
||||||
{.or}
|
{@}
|
||||||
INDEXING IN PROGRESS - RESULT MAY BE INACCURATE
|
|
||||||
|
|
||||||
{.end}
|
{.end}
|
||||||
{.section Alt}
|
{.section Alt}
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
<!--
|
|
||||||
Copyright 2009 The Go Authors. All rights reserved.
|
|
||||||
Use of this source code is governed by a BSD-style
|
|
||||||
license that can be found in the LICENSE file.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<script src="http://www.google.com/jsapi"></script>
|
|
||||||
<script src="/doc/popups.js"></script>
|
|
||||||
<script>
|
|
||||||
{# IdList is HTML-escaped by godoc}
|
|
||||||
var popup_data = {IdList}
|
|
||||||
|
|
||||||
google.load("jquery", "1");
|
|
||||||
google.setOnLoadCallback(function() {.meta-left}
|
|
||||||
godocs_bindPopups(popup_data);
|
|
||||||
{.meta-right});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{# Source is HTML-escaped by godoc}
|
|
||||||
<pre>{Source}</pre>
|
|
@ -8,6 +8,7 @@ TARG=godoc
|
|||||||
GOFILES=\
|
GOFILES=\
|
||||||
codewalk.go\
|
codewalk.go\
|
||||||
dirtrees.go\
|
dirtrees.go\
|
||||||
|
format.go\
|
||||||
godoc.go\
|
godoc.go\
|
||||||
index.go\
|
index.go\
|
||||||
main.go\
|
main.go\
|
||||||
|
@ -48,7 +48,7 @@ The flags are:
|
|||||||
-timestamps=true
|
-timestamps=true
|
||||||
show timestamps with directory listings
|
show timestamps with directory listings
|
||||||
-fulltext=false
|
-fulltext=false
|
||||||
build full text index for string search results
|
build full text index for regular expression queries
|
||||||
-path=""
|
-path=""
|
||||||
additional package directories (colon-separated)
|
additional package directories (colon-separated)
|
||||||
-html
|
-html
|
||||||
|
342
src/cmd/godoc/format.go
Normal file
342
src/cmd/godoc/format.go
Normal file
@ -0,0 +1,342 @@
|
|||||||
|
// Copyright 2011 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// This file implements FormatSelections and FormatText.
|
||||||
|
// FormatText is used to HTML-format Go and non-Go source
|
||||||
|
// text with line numbers and highlighted sections. It is
|
||||||
|
// built on top of FormatSelections, a generic formatter
|
||||||
|
// for "selected" text.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"go/scanner"
|
||||||
|
"go/token"
|
||||||
|
"io"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"template"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Implementation of FormatSelections
|
||||||
|
|
||||||
|
// A Selection is a function returning offset pairs []int{a, b}
|
||||||
|
// describing consecutive non-overlapping text segments [a, b).
|
||||||
|
// If there are no more segments, a Selection must return nil.
|
||||||
|
//
|
||||||
|
// TODO It's more efficient to return a pair (a, b int) instead
|
||||||
|
// of creating lots of slices. Need to determine how to
|
||||||
|
// indicate the end of a Selection.
|
||||||
|
//
|
||||||
|
type Selection func() []int
|
||||||
|
|
||||||
|
|
||||||
|
// A LinkWriter writes some start or end "tag" to w for the text offset offs.
|
||||||
|
// It is called by FormatSelections at the start or end of each link segment.
|
||||||
|
//
|
||||||
|
type LinkWriter func(w io.Writer, offs int, start bool)
|
||||||
|
|
||||||
|
|
||||||
|
// A SegmentWriter formats a text according to selections and writes it to w.
|
||||||
|
// The selections parameter is a bit set indicating which selections provided
|
||||||
|
// to FormatSelections overlap with the text segment: If the n'th bit is set
|
||||||
|
// in selections, the n'th selection provided to FormatSelections is overlapping
|
||||||
|
// with the text.
|
||||||
|
//
|
||||||
|
type SegmentWriter func(w io.Writer, text []byte, selections int)
|
||||||
|
|
||||||
|
|
||||||
|
// FormatSelections takes a text and writes it to w using link and segment
|
||||||
|
// writers lw and sw as follows: lw is invoked for consecutive segment starts
|
||||||
|
// and ends as specified through the links selection, and sw is invoked for
|
||||||
|
// consecutive segments of text overlapped by the same selections as specified
|
||||||
|
// by selections. The link writer lw may be nil, in which case the links
|
||||||
|
// Selection is ignored.
|
||||||
|
//
|
||||||
|
func FormatSelections(w io.Writer, text []byte, lw LinkWriter, links Selection, sw SegmentWriter, selections ...Selection) {
|
||||||
|
if lw != nil {
|
||||||
|
selections = append(selections, links)
|
||||||
|
}
|
||||||
|
// compute the sequence of consecutive segment changes
|
||||||
|
changes := newMerger(selections)
|
||||||
|
// The i'th bit in bitset indicates that the text
|
||||||
|
// at the current offset is covered by selections[i].
|
||||||
|
bitset := 0
|
||||||
|
lastOffs := 0
|
||||||
|
for {
|
||||||
|
// get the next segment change
|
||||||
|
index, offs, start := changes.next()
|
||||||
|
if index < 0 || offs > len(text) {
|
||||||
|
// no more segment changes or the next change
|
||||||
|
// is past the end of the text - we're done
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// determine the kind of segment change
|
||||||
|
if index == len(selections)-1 {
|
||||||
|
// we have a link segment change:
|
||||||
|
// format the previous selection segment, write the
|
||||||
|
// link tag and start a new selection segment
|
||||||
|
sw(w, text[lastOffs:offs], bitset)
|
||||||
|
lastOffs = offs
|
||||||
|
lw(w, offs, start)
|
||||||
|
} else {
|
||||||
|
// we have a selection change:
|
||||||
|
// format the previous selection segment, determine
|
||||||
|
// the new selection bitset and start a new segment
|
||||||
|
sw(w, text[lastOffs:offs], bitset)
|
||||||
|
lastOffs = offs
|
||||||
|
mask := 1 << uint(index)
|
||||||
|
if start {
|
||||||
|
bitset |= mask
|
||||||
|
} else {
|
||||||
|
bitset &^= mask
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sw(w, text[lastOffs:], bitset)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// A merger merges a slice of Selections and produces a sequence of
|
||||||
|
// consecutive segment change events through repeated next() calls.
|
||||||
|
//
|
||||||
|
type merger struct {
|
||||||
|
selections []Selection
|
||||||
|
segments [][]int // segments[i] is the next segment of selections[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const infinity int = 2e9
|
||||||
|
|
||||||
|
func newMerger(selections []Selection) *merger {
|
||||||
|
segments := make([][]int, len(selections))
|
||||||
|
for i, sel := range selections {
|
||||||
|
segments[i] = []int{infinity, infinity}
|
||||||
|
if sel != nil {
|
||||||
|
if seg := sel(); seg != nil {
|
||||||
|
segments[i] = seg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &merger{selections, segments}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// next returns the next segment change: index specifies the Selection
|
||||||
|
// to which the segment belongs, offs is the segment start or end offset
|
||||||
|
// as determined by the start value. If there are no more segment changes,
|
||||||
|
// next returns an index value < 0.
|
||||||
|
//
|
||||||
|
func (m *merger) next() (index, offs int, start bool) {
|
||||||
|
// find the next smallest offset where a segment starts or ends
|
||||||
|
offs = infinity
|
||||||
|
index = -1
|
||||||
|
for i, seg := range m.segments {
|
||||||
|
switch {
|
||||||
|
case seg[0] < offs:
|
||||||
|
offs = seg[0]
|
||||||
|
index = i
|
||||||
|
start = true
|
||||||
|
case seg[1] < offs:
|
||||||
|
offs = seg[1]
|
||||||
|
index = i
|
||||||
|
start = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if index < 0 {
|
||||||
|
// no offset found => all selections merged
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// offset found - it's either the start or end offset but
|
||||||
|
// either way it is ok to consume the start offset: set it
|
||||||
|
// to infinity so it won't be considered in the following
|
||||||
|
// next call
|
||||||
|
m.segments[index][0] = infinity
|
||||||
|
if start {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// end offset found - consume it
|
||||||
|
m.segments[index][1] = infinity
|
||||||
|
// advance to the next segment for that selection
|
||||||
|
seg := m.selections[index]()
|
||||||
|
if seg == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.segments[index] = seg
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Implementation of FormatText
|
||||||
|
|
||||||
|
// lineSelection returns the line segments for text as a Selection.
|
||||||
|
func lineSelection(text []byte) Selection {
|
||||||
|
i, j := 0, 0
|
||||||
|
return func() (seg []int) {
|
||||||
|
// find next newline, if any
|
||||||
|
for j < len(text) {
|
||||||
|
j++
|
||||||
|
if text[j-1] == '\n' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if i < j {
|
||||||
|
// text[i:j] constitutes a line
|
||||||
|
seg = []int{i, j}
|
||||||
|
i = j
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// commentSelection returns the sequence of consecutive comments
|
||||||
|
// in the Go src text as a Selection.
|
||||||
|
//
|
||||||
|
func commentSelection(src []byte) Selection {
|
||||||
|
var s scanner.Scanner
|
||||||
|
file := s.Init(token.NewFileSet(), "", src, nil, scanner.ScanComments+scanner.InsertSemis)
|
||||||
|
return func() (seg []int) {
|
||||||
|
for {
|
||||||
|
pos, tok, lit := s.Scan()
|
||||||
|
if tok == token.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
offs := file.Offset(pos)
|
||||||
|
if tok == token.COMMENT {
|
||||||
|
seg = []int{offs, offs + len(lit)}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// makeSelection is a helper function to make a Selection from a slice of pairs.
|
||||||
|
func makeSelection(matches [][]int) Selection {
|
||||||
|
return func() (seg []int) {
|
||||||
|
if len(matches) > 0 {
|
||||||
|
seg = matches[0]
|
||||||
|
matches = matches[1:]
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// regexpSelection computes the Selection for the regular expression expr in text.
|
||||||
|
func regexpSelection(text []byte, expr string) Selection {
|
||||||
|
var matches [][]int
|
||||||
|
if rx, err := regexp.Compile(expr); err == nil {
|
||||||
|
matches = rx.FindAllIndex(text, -1)
|
||||||
|
}
|
||||||
|
return makeSelection(matches)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var selRx = regexp.MustCompile(`^([0-9]+):([0-9]+)`)
|
||||||
|
|
||||||
|
// rangeSelection computes the Selection for a text range described
|
||||||
|
// by the argument str; the range description must match the selRx
|
||||||
|
// regular expression.
|
||||||
|
//
|
||||||
|
func rangeSelection(str string) Selection {
|
||||||
|
m := selRx.FindStringSubmatch(str)
|
||||||
|
if len(m) >= 2 {
|
||||||
|
from, _ := strconv.Atoi(m[1])
|
||||||
|
to, _ := strconv.Atoi(m[2])
|
||||||
|
if from < to {
|
||||||
|
return makeSelection([][]int{[]int{from, to}})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Span tags for all the possible selection combinations that may
|
||||||
|
// be generated by FormatText. Selections are indicated by a bitset,
|
||||||
|
// and the value of the bitset specifies the tag to be used.
|
||||||
|
//
|
||||||
|
// bit 0: comments
|
||||||
|
// bit 1: highlights
|
||||||
|
// bit 2: selections
|
||||||
|
//
|
||||||
|
var startTags = [][]byte{
|
||||||
|
/* 000 */ []byte(``),
|
||||||
|
/* 001 */ []byte(`<span class ="comment">`),
|
||||||
|
/* 010 */ []byte(`<span class="highlight">`),
|
||||||
|
/* 011 */ []byte(`<span class="highlight-comment">`),
|
||||||
|
/* 100 */ []byte(`<span class="selection">`),
|
||||||
|
/* 101 */ []byte(`<span class="selection-comment">`),
|
||||||
|
/* 110 */ []byte(`<span class="selection-highlight">`),
|
||||||
|
/* 111 */ []byte(`<span class="selection-highlight-comment">`),
|
||||||
|
}
|
||||||
|
|
||||||
|
var endTag = []byte(`</span>`)
|
||||||
|
|
||||||
|
|
||||||
|
func selectionTag(w io.Writer, text []byte, selections int) {
|
||||||
|
if len(text) > 0 {
|
||||||
|
if selections < len(startTags) {
|
||||||
|
if tag := startTags[selections]; len(tag) > 0 {
|
||||||
|
w.Write(tag)
|
||||||
|
template.HTMLEscape(w, text)
|
||||||
|
w.Write(endTag)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
template.HTMLEscape(w, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// FormatText HTML-escapes text and returns it wrapped in <pre> tags.
|
||||||
|
// Conscutive text segments are wrapped in HTML spans (with tags as
|
||||||
|
// defined by startTags and endTag) as follows:
|
||||||
|
//
|
||||||
|
// - if line >= 0, line numbers are printed before each line, starting
|
||||||
|
// with the value of line
|
||||||
|
// - if the text is Go source, comments get the "comment" span class
|
||||||
|
// - each occurrence of the regular expression pattern gets the "highlight"
|
||||||
|
// span class
|
||||||
|
// - text segments covered by selection get the "selection" span class
|
||||||
|
//
|
||||||
|
// Comments, highlights, and selections may overlap arbitrarily; the respective
|
||||||
|
// HTML span classes are specified in the startTags variable.
|
||||||
|
//
|
||||||
|
func FormatText(text []byte, line int, goSource bool, pattern string, selection Selection) []byte {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
buf.WriteString("<pre>\n")
|
||||||
|
|
||||||
|
var comments, highlights Selection
|
||||||
|
if goSource {
|
||||||
|
comments = commentSelection(text)
|
||||||
|
}
|
||||||
|
if pattern != "" {
|
||||||
|
highlights = regexpSelection(text, pattern)
|
||||||
|
}
|
||||||
|
if comments != nil || highlights != nil || selection != nil {
|
||||||
|
var lineTag LinkWriter
|
||||||
|
if line >= 0 {
|
||||||
|
lineTag = func(w io.Writer, _ int, start bool) {
|
||||||
|
if start {
|
||||||
|
fmt.Fprintf(w, "<a id=\"L%d\"></a>%5d\t", line, line)
|
||||||
|
line++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FormatSelections(&buf, text, lineTag, lineSelection(text), selectionTag, comments, highlights, selection)
|
||||||
|
} else {
|
||||||
|
template.HTMLEscape(&buf, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.WriteString("</pre>\n")
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
@ -55,6 +55,7 @@ var (
|
|||||||
// file system roots
|
// file system roots
|
||||||
// TODO(gri) consider the invariant that goroot always end in '/'
|
// TODO(gri) consider the invariant that goroot always end in '/'
|
||||||
goroot = flag.String("goroot", runtime.GOROOT(), "Go root directory")
|
goroot = flag.String("goroot", runtime.GOROOT(), "Go root directory")
|
||||||
|
testDir = flag.String("testdir", "", "Go root subdirectory - for testing only (faster startups)")
|
||||||
path = flag.String("path", "", "additional package directories (colon-separated)")
|
path = flag.String("path", "", "additional package directories (colon-separated)")
|
||||||
filter = flag.String("filter", "", "filter file containing permitted package directory paths")
|
filter = flag.String("filter", "", "filter file containing permitted package directory paths")
|
||||||
filterMin = flag.Int("filter_minutes", 0, "filter file update interval in minutes; disabled if <= 0")
|
filterMin = flag.Int("filter_minutes", 0, "filter file update interval in minutes; disabled if <= 0")
|
||||||
@ -63,7 +64,7 @@ var (
|
|||||||
// layout control
|
// layout control
|
||||||
tabwidth = flag.Int("tabwidth", 4, "tab width")
|
tabwidth = flag.Int("tabwidth", 4, "tab width")
|
||||||
showTimestamps = flag.Bool("timestamps", true, "show timestamps with directory listings")
|
showTimestamps = flag.Bool("timestamps", true, "show timestamps with directory listings")
|
||||||
fulltextIndex = flag.Bool("fulltext", false, "build full text index for string search results")
|
fulltextIndex = flag.Bool("fulltext", false, "build full text index for regular expression queries")
|
||||||
|
|
||||||
// file system mapping
|
// file system mapping
|
||||||
fsMap Mapping // user-defined mapping
|
fsMap Mapping // user-defined mapping
|
||||||
@ -95,6 +96,12 @@ func registerPublicHandlers(mux *http.ServeMux) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func initFSTree() {
|
||||||
|
fsTree.set(newDirectory(pathutil.Join(*goroot, *testDir), nil, -1))
|
||||||
|
invalidateIndex()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Directory filters
|
// Directory filters
|
||||||
|
|
||||||
@ -265,181 +272,6 @@ func relativePath(path string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
// HTML formatting support
|
|
||||||
|
|
||||||
// aposescaper implements an io.Writer that escapes single quotes:
|
|
||||||
// ' is written as \' . It is used to escape text such that it can
|
|
||||||
// be used as the content of single-quoted string literals.
|
|
||||||
type aposescaper struct {
|
|
||||||
w io.Writer
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func (e *aposescaper) Write(p []byte) (n int, err os.Error) {
|
|
||||||
backslash := []byte{'\\'}
|
|
||||||
var i, m int
|
|
||||||
for j, b := range p {
|
|
||||||
if b == '\'' {
|
|
||||||
m, err = e.w.Write(p[i:j])
|
|
||||||
n += m
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_, err = e.w.Write(backslash)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
i = j
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m, err = e.w.Write(p[i:])
|
|
||||||
n += m
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Styler implements a printer.Styler.
|
|
||||||
type Styler struct {
|
|
||||||
linetags bool
|
|
||||||
highlight string
|
|
||||||
objmap map[*ast.Object]int
|
|
||||||
idcount int
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func newStyler(highlight string) *Styler {
|
|
||||||
return &Styler{true, highlight, make(map[*ast.Object]int), 0}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// identId returns a number >= 0 identifying the *ast.Object
|
|
||||||
// denoted by name. If no object is denoted, the result is < 0.
|
|
||||||
//
|
|
||||||
// TODO(gri): Consider making this a mapping from popup info
|
|
||||||
// (for that name) to id, instead of *ast.Object
|
|
||||||
// to id. If a lot of the popup info is the same
|
|
||||||
// (e.g. type information), this will reduce the
|
|
||||||
// size of the html generated.
|
|
||||||
func (s *Styler) identId(name *ast.Ident) int {
|
|
||||||
obj := name.Obj
|
|
||||||
if obj == nil || s.objmap == nil {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
id, found := s.objmap[obj]
|
|
||||||
if !found {
|
|
||||||
// first occurence
|
|
||||||
id = s.idcount
|
|
||||||
s.objmap[obj] = id
|
|
||||||
s.idcount++
|
|
||||||
}
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// writeObjInfo writes the popup info corresponding to obj to w.
|
|
||||||
// The text is HTML-escaped and does not contain single quotes.
|
|
||||||
func writeObjInfo(w io.Writer, fset *token.FileSet, obj *ast.Object) {
|
|
||||||
// for now, show object kind and name; eventually
|
|
||||||
// do something more interesting (show declaration,
|
|
||||||
// for instance)
|
|
||||||
if obj.Kind != ast.Bad {
|
|
||||||
fmt.Fprintf(w, "%s ", obj.Kind)
|
|
||||||
}
|
|
||||||
template.HTMLEscape(w, []byte(obj.Name))
|
|
||||||
// show type if we know it
|
|
||||||
if obj.Type != nil && obj.Type.Expr != nil {
|
|
||||||
fmt.Fprint(w, " ")
|
|
||||||
writeNode(&aposescaper{w}, fset, obj.Type.Expr, true, &defaultStyler)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// idList returns a Javascript array (source) with identifier popup
|
|
||||||
// information: The i'th array entry is a single-quoted string with
|
|
||||||
// the popup information for an identifier x with s.identId(x) == i,
|
|
||||||
// for 0 <= i < s.idcount.
|
|
||||||
func (s *Styler) idList(fset *token.FileSet) []byte {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
buf.WriteString("[\n")
|
|
||||||
|
|
||||||
if s.idcount > 0 {
|
|
||||||
// invert objmap: create an array [id]obj from map[obj]id
|
|
||||||
a := make([]*ast.Object, s.idcount)
|
|
||||||
for obj, id := range s.objmap {
|
|
||||||
a[id] = obj
|
|
||||||
}
|
|
||||||
|
|
||||||
// for each id, print object info as single-quoted Javascript string
|
|
||||||
for id, obj := range a {
|
|
||||||
printIndex := false // enable for debugging (but longer html)
|
|
||||||
if printIndex {
|
|
||||||
fmt.Fprintf(&buf, "/* %4d */ ", id)
|
|
||||||
}
|
|
||||||
fmt.Fprint(&buf, "'")
|
|
||||||
writeObjInfo(&buf, fset, obj)
|
|
||||||
fmt.Fprint(&buf, "',\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buf.WriteString("]\n")
|
|
||||||
return buf.Bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Use the defaultStyler when there is no specific styler.
|
|
||||||
// The defaultStyler does not emit line tags since they may
|
|
||||||
// interfere with tags emitted by templates.
|
|
||||||
// TODO(gri): Should emit line tags at the beginning of a line;
|
|
||||||
// never in the middle of code.
|
|
||||||
var defaultStyler Styler
|
|
||||||
|
|
||||||
|
|
||||||
func (s *Styler) LineTag(line int) (text []byte, tag printer.HTMLTag) {
|
|
||||||
if s.linetags {
|
|
||||||
tag = printer.HTMLTag{fmt.Sprintf(`<a id="L%d">`, line), "</a>"}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func (s *Styler) Comment(c *ast.Comment, line []byte) (text []byte, tag printer.HTMLTag) {
|
|
||||||
text = line
|
|
||||||
// minimal syntax-coloring of comments for now - people will want more
|
|
||||||
// (don't do anything more until there's a button to turn it on/off)
|
|
||||||
tag = printer.HTMLTag{`<span class="comment">`, "</span>"}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func (s *Styler) BasicLit(x *ast.BasicLit) (text []byte, tag printer.HTMLTag) {
|
|
||||||
text = x.Value
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func (s *Styler) Ident(name *ast.Ident) (text []byte, tag printer.HTMLTag) {
|
|
||||||
text = []byte(name.Name)
|
|
||||||
var str string
|
|
||||||
if id := s.identId(name); id >= 0 {
|
|
||||||
str = fmt.Sprintf(` id="%d"`, id)
|
|
||||||
}
|
|
||||||
if s.highlight == name.Name {
|
|
||||||
str += ` class="highlight"`
|
|
||||||
}
|
|
||||||
if str != "" {
|
|
||||||
tag = printer.HTMLTag{"<span" + str + ">", "</span>"}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func (s *Styler) Token(tok token.Token) (text []byte, tag printer.HTMLTag) {
|
|
||||||
text = []byte(tok.String())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Tab conversion
|
// Tab conversion
|
||||||
|
|
||||||
@ -516,7 +348,7 @@ func (p *tconv) Write(data []byte) (n int, err os.Error) {
|
|||||||
// Templates
|
// Templates
|
||||||
|
|
||||||
// Write an AST-node to w; optionally html-escaped.
|
// Write an AST-node to w; optionally html-escaped.
|
||||||
func writeNode(w io.Writer, fset *token.FileSet, node interface{}, html bool, styler printer.Styler) {
|
func writeNode(w io.Writer, fset *token.FileSet, node interface{}, html bool) {
|
||||||
mode := printer.TabIndent | printer.UseSpaces
|
mode := printer.TabIndent | printer.UseSpaces
|
||||||
if html {
|
if html {
|
||||||
mode |= printer.GenHTML
|
mode |= printer.GenHTML
|
||||||
@ -525,7 +357,7 @@ func writeNode(w io.Writer, fset *token.FileSet, node interface{}, html bool, st
|
|||||||
// to ensure a good outcome in most browsers (there may still
|
// to ensure a good outcome in most browsers (there may still
|
||||||
// be tabs in comments and strings, but converting those into
|
// be tabs in comments and strings, but converting those into
|
||||||
// the right number of spaces is much harder)
|
// the right number of spaces is much harder)
|
||||||
(&printer.Config{mode, *tabwidth, styler}).Fprint(&tconv{output: w}, fset, node)
|
(&printer.Config{mode, *tabwidth, nil}).Fprint(&tconv{output: w}, fset, node)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -547,7 +379,7 @@ func writeAny(w io.Writer, fset *token.FileSet, html bool, x interface{}) {
|
|||||||
case string:
|
case string:
|
||||||
writeText(w, []byte(v), html)
|
writeText(w, []byte(v), html)
|
||||||
case ast.Decl, ast.Expr, ast.Stmt, *ast.File:
|
case ast.Decl, ast.Expr, ast.Stmt, *ast.File:
|
||||||
writeNode(w, fset, x, html, &defaultStyler)
|
writeNode(w, fset, x, html)
|
||||||
default:
|
default:
|
||||||
if html {
|
if html {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
@ -612,20 +444,26 @@ func urlQueryEscFmt(w io.Writer, format string, x ...interface{}) {
|
|||||||
func urlFmt(w io.Writer, format string, x ...interface{}) {
|
func urlFmt(w io.Writer, format string, x ...interface{}) {
|
||||||
var path string
|
var path string
|
||||||
var line int
|
var line int
|
||||||
|
var low, high int // selection
|
||||||
|
|
||||||
// determine path and position info, if any
|
// determine path and position info, if any
|
||||||
type positioner interface {
|
type positioner interface {
|
||||||
Pos() token.Pos
|
Pos() token.Pos
|
||||||
|
End() token.Pos
|
||||||
}
|
}
|
||||||
switch t := x[0].(type) {
|
switch t := x[0].(type) {
|
||||||
case string:
|
case string:
|
||||||
path = t
|
path = t
|
||||||
case positioner:
|
case positioner:
|
||||||
pos := t.Pos()
|
fset := fileset(x)
|
||||||
if pos.IsValid() {
|
if p := t.Pos(); p.IsValid() {
|
||||||
pos := fileset(x).Position(pos)
|
pos := fset.Position(p)
|
||||||
path = pos.Filename
|
path = pos.Filename
|
||||||
line = pos.Line
|
line = pos.Line
|
||||||
|
low = pos.Offset
|
||||||
|
}
|
||||||
|
if p := t.End(); p.IsValid() {
|
||||||
|
high = fset.Position(p).Offset
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
// we should never reach here, but be resilient
|
// we should never reach here, but be resilient
|
||||||
@ -655,10 +493,22 @@ func urlFmt(w io.Writer, format string, x ...interface{}) {
|
|||||||
case "url-src":
|
case "url-src":
|
||||||
template.HTMLEscape(w, []byte(relpath))
|
template.HTMLEscape(w, []byte(relpath))
|
||||||
case "url-pos":
|
case "url-pos":
|
||||||
|
template.HTMLEscape(w, []byte(relpath))
|
||||||
|
// selection ranges are of form "s=low:high"
|
||||||
|
if low < high {
|
||||||
|
fmt.Fprintf(w, "?s=%d:%d", low, high)
|
||||||
|
// if we have a selection, position the page
|
||||||
|
// such that the selection is a bit below the top
|
||||||
|
line -= 10
|
||||||
|
if line < 1 {
|
||||||
|
line = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
// line id's in html-printed source are of the
|
// line id's in html-printed source are of the
|
||||||
// form "L%d" where %d stands for the line number
|
// form "L%d" where %d stands for the line number
|
||||||
template.HTMLEscape(w, []byte(relpath))
|
if line > 0 {
|
||||||
fmt.Fprintf(w, "#L%d", line)
|
fmt.Fprintf(w, "#L%d", line)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -705,14 +555,14 @@ func infoLineFmt(w io.Writer, format string, x ...interface{}) {
|
|||||||
// Template formatter for "infoSnippet" format.
|
// Template formatter for "infoSnippet" format.
|
||||||
func infoSnippetFmt(w io.Writer, format string, x ...interface{}) {
|
func infoSnippetFmt(w io.Writer, format string, x ...interface{}) {
|
||||||
info := x[0].(SpotInfo)
|
info := x[0].(SpotInfo)
|
||||||
text := `<span class="alert">no snippet text available</span>`
|
text := []byte(`<span class="alert">no snippet text available</span>`)
|
||||||
if info.IsIndex() {
|
if info.IsIndex() {
|
||||||
index, _ := searchIndex.get()
|
index, _ := searchIndex.get()
|
||||||
// no escaping of snippet text needed;
|
// no escaping of snippet text needed;
|
||||||
// snippet text is escaped when generated
|
// snippet text is escaped when generated
|
||||||
text = index.(*Index).Snippet(info.Lori()).Text
|
text = index.(*Index).Snippet(info.Lori()).Text
|
||||||
}
|
}
|
||||||
fmt.Fprint(w, text)
|
w.Write(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -752,27 +602,6 @@ func numlinesFmt(w io.Writer, format string, x ...interface{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Template formatter for "linelist" format.
|
|
||||||
func linelistFmt(w io.Writer, format string, x ...interface{}) {
|
|
||||||
list := x[0].([]int)
|
|
||||||
complete := x[1].(bool)
|
|
||||||
|
|
||||||
const max = 100 // show at most this many lines
|
|
||||||
if len(list) > max {
|
|
||||||
list = list[0:max]
|
|
||||||
complete = false
|
|
||||||
}
|
|
||||||
sort.SortInts(list)
|
|
||||||
|
|
||||||
for _, line := range list {
|
|
||||||
fmt.Fprintf(w, " %d", line)
|
|
||||||
}
|
|
||||||
if !complete {
|
|
||||||
fmt.Fprintf(w, " ...")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
var fmap = template.FormatterMap{
|
var fmap = template.FormatterMap{
|
||||||
"": textFmt,
|
"": textFmt,
|
||||||
"html": htmlFmt,
|
"html": htmlFmt,
|
||||||
@ -790,7 +619,6 @@ var fmap = template.FormatterMap{
|
|||||||
"dir/": dirslashFmt,
|
"dir/": dirslashFmt,
|
||||||
"localname": localnameFmt,
|
"localname": localnameFmt,
|
||||||
"numlines": numlinesFmt,
|
"numlines": numlinesFmt,
|
||||||
"linelist": linelistFmt,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -817,8 +645,7 @@ var (
|
|||||||
packageHTML,
|
packageHTML,
|
||||||
packageText,
|
packageText,
|
||||||
searchHTML,
|
searchHTML,
|
||||||
searchText,
|
searchText *template.Template
|
||||||
sourceHTML *template.Template
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func readTemplates() {
|
func readTemplates() {
|
||||||
@ -832,7 +659,6 @@ func readTemplates() {
|
|||||||
packageText = readTemplate("package.txt")
|
packageText = readTemplate("package.txt")
|
||||||
searchHTML = readTemplate("search.html")
|
searchHTML = readTemplate("search.html")
|
||||||
searchText = readTemplate("search.txt")
|
searchText = readTemplate("search.txt")
|
||||||
sourceHTML = readTemplate("source.html")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -933,34 +759,6 @@ func applyTemplate(t *template.Template, name string, data interface{}) []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func serveGoSource(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
|
|
||||||
fset := token.NewFileSet()
|
|
||||||
file, err := parser.ParseFile(fset, abspath, nil, parser.ParseComments)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("parser.ParseFile: %s", err)
|
|
||||||
serveError(w, r, relpath, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(gri) enable once we are confident it works for all files
|
|
||||||
// augment AST with types; ignore errors (partial type information ok)
|
|
||||||
// typechecker.CheckFile(file, nil)
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
styler := newStyler(r.FormValue("h"))
|
|
||||||
writeNode(&buf, fset, file, true, styler)
|
|
||||||
|
|
||||||
type SourceInfo struct {
|
|
||||||
IdList []byte
|
|
||||||
Source []byte
|
|
||||||
}
|
|
||||||
info := &SourceInfo{styler.idList(fset), buf.Bytes()}
|
|
||||||
|
|
||||||
contents := applyTemplate(sourceHTML, "sourceHTML", info)
|
|
||||||
servePage(w, "Source file "+relpath, "", "", contents)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func redirect(w http.ResponseWriter, r *http.Request) (redirected bool) {
|
func redirect(w http.ResponseWriter, r *http.Request) (redirected bool) {
|
||||||
if canonical := pathutil.Clean(r.URL.Path) + "/"; r.URL.Path != canonical {
|
if canonical := pathutil.Clean(r.URL.Path) + "/"; r.URL.Path != canonical {
|
||||||
http.Redirect(w, r, canonical, http.StatusMovedPermanently)
|
http.Redirect(w, r, canonical, http.StatusMovedPermanently)
|
||||||
@ -1017,36 +815,7 @@ func isTextFile(path string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// HTMLSubst replaces all occurences of f in s with r and HTML-escapes
|
func serveTextFile(w http.ResponseWriter, r *http.Request, abspath, relpath, title string) {
|
||||||
// everything else in s (but not r). The result is written to w.
|
|
||||||
//
|
|
||||||
func HTMLSubst(w io.Writer, s, f, r []byte) {
|
|
||||||
for {
|
|
||||||
i := bytes.Index(s, f)
|
|
||||||
if i < 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
template.HTMLEscape(w, s[0:i])
|
|
||||||
w.Write(r)
|
|
||||||
s = s[i+len(f):]
|
|
||||||
}
|
|
||||||
template.HTMLEscape(w, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// highlight highlights all occurrences of h in s and writes the
|
|
||||||
// HTML-escaped result to w.
|
|
||||||
//
|
|
||||||
func highlight(w io.Writer, s, h []byte) {
|
|
||||||
var r bytes.Buffer
|
|
||||||
r.WriteString(`<span class="highlight">`)
|
|
||||||
template.HTMLEscape(&r, h)
|
|
||||||
r.WriteString(`</span>`)
|
|
||||||
HTMLSubst(w, s, h, r.Bytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func serveTextFile(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
|
|
||||||
src, err := ioutil.ReadFile(abspath)
|
src, err := ioutil.ReadFile(abspath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("ioutil.ReadFile: %s", err)
|
log.Printf("ioutil.ReadFile: %s", err)
|
||||||
@ -1054,17 +823,8 @@ func serveTextFile(w http.ResponseWriter, r *http.Request, abspath, relpath stri
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var buf bytes.Buffer
|
contents := FormatText(src, 1, pathutil.Ext(abspath) == ".go", r.FormValue("h"), rangeSelection(r.FormValue("s")))
|
||||||
buf.WriteString("<pre>\n")
|
servePage(w, title+" "+relpath, "", "", contents)
|
||||||
g := r.FormValue("g")
|
|
||||||
if g != "" {
|
|
||||||
highlight(&buf, src, []byte(g))
|
|
||||||
} else {
|
|
||||||
template.HTMLEscape(&buf, src)
|
|
||||||
}
|
|
||||||
buf.WriteString("</pre>\n")
|
|
||||||
|
|
||||||
servePage(w, "Text file "+relpath, "", "", buf.Bytes())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -1119,11 +879,7 @@ func serveFile(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
|
|
||||||
case ".go":
|
case ".go":
|
||||||
if r.FormValue("g") != "" {
|
serveTextFile(w, r, abspath, relpath, "Source file")
|
||||||
serveTextFile(w, r, abspath, relpath)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
serveGoSource(w, r, abspath, relpath)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1147,7 +903,7 @@ func serveFile(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if isTextFile(abspath) {
|
if isTextFile(abspath) {
|
||||||
serveTextFile(w, r, abspath, relpath)
|
serveTextFile(w, r, abspath, relpath, "Text file")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1195,7 +951,7 @@ type httpHandler struct {
|
|||||||
// computed (PageInfo.PAst), otherwise package documentation (PageInfo.Doc)
|
// computed (PageInfo.PAst), otherwise package documentation (PageInfo.Doc)
|
||||||
// is extracted from the AST. If there is no corresponding package in the
|
// is extracted from the AST. If there is no corresponding package in the
|
||||||
// directory, PageInfo.PAst and PageInfo.PDoc are nil. If there are no sub-
|
// directory, PageInfo.PAst and PageInfo.PDoc are nil. If there are no sub-
|
||||||
// directories, PageInfo.Dirs is nil. If a directory read error occured,
|
// directories, PageInfo.Dirs is nil. If a directory read error occurred,
|
||||||
// PageInfo.Err is set to the respective error but the error is not logged.
|
// PageInfo.Err is set to the respective error but the error is not logged.
|
||||||
//
|
//
|
||||||
func (h *httpHandler) getPageInfo(abspath, relpath, pkgname string, mode PageInfoMode) PageInfo {
|
func (h *httpHandler) getPageInfo(abspath, relpath, pkgname string, mode PageInfoMode) PageInfo {
|
||||||
@ -1386,26 +1142,57 @@ func (h *httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
var searchIndex RWValue
|
var searchIndex RWValue
|
||||||
|
|
||||||
type SearchResult struct {
|
type SearchResult struct {
|
||||||
Query string
|
Query string
|
||||||
Hit *LookupResult // identifier occurences of Query
|
Alert string // error or warning message
|
||||||
Alt *AltWords // alternative identifiers to look for
|
|
||||||
Found int // number of textual occurences found
|
// identifier matches
|
||||||
Textual []Positions // textual occurences of Query
|
Hit *LookupResult // identifier matches of Query
|
||||||
Complete bool // true if all textual occurences of Query are reported
|
Alt *AltWords // alternative identifiers to look for
|
||||||
Accurate bool // true if the index is not older than the indexed files
|
|
||||||
|
// textual matches
|
||||||
|
Found int // number of textual occurrences found
|
||||||
|
Textual []FileLines // textual matches of Query
|
||||||
|
Complete bool // true if all textual occurrences of Query are reported
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func lookup(query string) (result SearchResult) {
|
func lookup(query string) (result SearchResult) {
|
||||||
result.Query = query
|
result.Query = query
|
||||||
|
|
||||||
|
// determine identifier lookup string and full text regexp
|
||||||
|
lookupStr := ""
|
||||||
|
lookupRx, err := regexp.Compile(query)
|
||||||
|
if err != nil {
|
||||||
|
result.Alert = "Error in query regular expression: " + err.String()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if prefix, complete := lookupRx.LiteralPrefix(); complete {
|
||||||
|
// otherwise we lookup "" (with no result) because
|
||||||
|
// identifier lookup doesn't support regexp search
|
||||||
|
lookupStr = prefix
|
||||||
|
}
|
||||||
|
|
||||||
if index, timestamp := searchIndex.get(); index != nil {
|
if index, timestamp := searchIndex.get(); index != nil {
|
||||||
|
// identifier search
|
||||||
index := index.(*Index)
|
index := index.(*Index)
|
||||||
result.Hit, result.Alt, _ = index.Lookup(query)
|
result.Hit, result.Alt, err = index.Lookup(lookupStr)
|
||||||
|
if err != nil && !*fulltextIndex {
|
||||||
|
// ignore the error if there is full text search
|
||||||
|
// since it accepts that query regular expression
|
||||||
|
result.Alert = "Error in query string: " + err.String()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// textual search
|
||||||
// TODO(gri) should max be a flag?
|
// TODO(gri) should max be a flag?
|
||||||
const max = 5000 // show at most this many fulltext results
|
const max = 10000 // show at most this many fulltext results
|
||||||
result.Found, result.Textual, result.Complete = index.LookupString(query, max)
|
result.Found, result.Textual = index.LookupRegexp(lookupRx, max+1)
|
||||||
_, ts := fsModified.get()
|
result.Complete = result.Found <= max
|
||||||
result.Accurate = timestamp >= ts
|
|
||||||
|
// is the result accurate?
|
||||||
|
if _, ts := fsModified.get(); timestamp < ts {
|
||||||
|
result.Alert = "Indexing in progress: result may be inaccurate"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1500,10 +1287,15 @@ func indexer() {
|
|||||||
log.Printf("index updated (%gs, %d bytes of source, %d files, %d lines, %d unique words, %d spots)",
|
log.Printf("index updated (%gs, %d bytes of source, %d files, %d lines, %d unique words, %d spots)",
|
||||||
secs, stats.Bytes, stats.Files, stats.Lines, stats.Words, stats.Spots)
|
secs, stats.Bytes, stats.Files, stats.Lines, stats.Words, stats.Spots)
|
||||||
}
|
}
|
||||||
log.Printf("before GC: bytes = %d footprint = %d\n", runtime.MemStats.HeapAlloc, runtime.MemStats.Sys)
|
log.Printf("before GC: bytes = %d footprint = %d", runtime.MemStats.HeapAlloc, runtime.MemStats.Sys)
|
||||||
runtime.GC()
|
runtime.GC()
|
||||||
log.Printf("after GC: bytes = %d footprint = %d\n", runtime.MemStats.HeapAlloc, runtime.MemStats.Sys)
|
log.Printf("after GC: bytes = %d footprint = %d", runtime.MemStats.HeapAlloc, runtime.MemStats.Sys)
|
||||||
}
|
}
|
||||||
time.Sleep(1 * 60e9) // try once a minute
|
var delay int64 = 60 * 1e9 // by default, try every 60s
|
||||||
|
if *testDir != "" {
|
||||||
|
// in test mode, try once a second for fast startup
|
||||||
|
delay = 1 * 1e9
|
||||||
|
}
|
||||||
|
time.Sleep(delay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
//
|
//
|
||||||
// Algorithm for identifier index:
|
// Algorithm for identifier index:
|
||||||
// - traverse all .go files of the file tree specified by root
|
// - traverse all .go files of the file tree specified by root
|
||||||
// - for each word (identifier) encountered, collect all occurences (spots)
|
// - for each word (identifier) encountered, collect all occurrences (spots)
|
||||||
// into a list; this produces a list of spots for each word
|
// into a list; this produces a list of spots for each word
|
||||||
// - reduce the lists: from a list of spots to a list of FileRuns,
|
// - reduce the lists: from a list of spots to a list of FileRuns,
|
||||||
// and from a list of FileRuns into a list of PakRuns
|
// and from a list of FileRuns into a list of PakRuns
|
||||||
@ -48,6 +48,7 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
pathutil "path"
|
pathutil "path"
|
||||||
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@ -247,7 +248,7 @@ type File struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// A Spot describes a single occurence of a word.
|
// A Spot describes a single occurrence of a word.
|
||||||
type Spot struct {
|
type Spot struct {
|
||||||
File *File
|
File *File
|
||||||
Info SpotInfo
|
Info SpotInfo
|
||||||
@ -435,7 +436,7 @@ const excludeTestFiles = false
|
|||||||
|
|
||||||
type IndexResult struct {
|
type IndexResult struct {
|
||||||
Decls RunList // package-level declarations (with snippets)
|
Decls RunList // package-level declarations (with snippets)
|
||||||
Others RunList // all other occurences
|
Others RunList // all other occurrences
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -445,7 +446,7 @@ type Statistics struct {
|
|||||||
Files int // number of indexed source files
|
Files int // number of indexed source files
|
||||||
Lines int // number of lines (all files)
|
Lines int // number of lines (all files)
|
||||||
Words int // number of different identifiers
|
Words int // number of different identifiers
|
||||||
Spots int // number of identifier occurences
|
Spots int // number of identifier occurrences
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -709,7 +710,7 @@ func (x *Indexer) visitFile(dirname string, f *os.FileInfo) {
|
|||||||
|
|
||||||
type LookupResult struct {
|
type LookupResult struct {
|
||||||
Decls HitList // package-level declarations (with snippets)
|
Decls HitList // package-level declarations (with snippets)
|
||||||
Others HitList // all other occurences
|
Others HitList // all other occurrences
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -833,14 +834,14 @@ func isIdentifier(s string) bool {
|
|||||||
|
|
||||||
// For a given query, which is either a single identifier or a qualified
|
// For a given query, which is either a single identifier or a qualified
|
||||||
// identifier, Lookup returns a LookupResult, and a list of alternative
|
// identifier, Lookup returns a LookupResult, and a list of alternative
|
||||||
// spellings, if any. If the query syntax is wrong, illegal is set.
|
// spellings, if any. If the query syntax is wrong, an error is reported.
|
||||||
func (x *Index) Lookup(query string) (match *LookupResult, alt *AltWords, illegal bool) {
|
func (x *Index) Lookup(query string) (match *LookupResult, alt *AltWords, err os.Error) {
|
||||||
ss := strings.Split(query, ".", -1)
|
ss := strings.Split(query, ".", -1)
|
||||||
|
|
||||||
// check query syntax
|
// check query syntax
|
||||||
for _, s := range ss {
|
for _, s := range ss {
|
||||||
if !isIdentifier(s) {
|
if !isIdentifier(s) {
|
||||||
illegal = true
|
err = os.NewError("all query parts must be identifiers")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -860,7 +861,7 @@ func (x *Index) Lookup(query string) (match *LookupResult, alt *AltWords, illega
|
|||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
illegal = true
|
err = os.NewError("query is not a (qualified) identifier")
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
@ -886,60 +887,91 @@ func (list positionList) Less(i, j int) bool { return list[i].filename < list[j]
|
|||||||
func (list positionList) Swap(i, j int) { list[i], list[j] = list[j], list[i] }
|
func (list positionList) Swap(i, j int) { list[i], list[j] = list[j], list[i] }
|
||||||
|
|
||||||
|
|
||||||
// A Positions value specifies a file and line numbers within that file.
|
// unique returns the list sorted and with duplicate entries removed
|
||||||
type Positions struct {
|
func unique(list []int) []int {
|
||||||
|
sort.SortInts(list)
|
||||||
|
var last int
|
||||||
|
i := 0
|
||||||
|
for _, x := range list {
|
||||||
|
if i == 0 || x != last {
|
||||||
|
last = x
|
||||||
|
list[i] = x
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list[0:i]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// A FileLines value specifies a file and line numbers within that file.
|
||||||
|
type FileLines struct {
|
||||||
Filename string
|
Filename string
|
||||||
Lines []int
|
Lines []int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// LookupString returns the number and list of positions where a string
|
// LookupRegexp returns the number of matches and the matches where a regular
|
||||||
// s is found in the full text index and whether the result is complete
|
// expression r is found in the full text index. At most n matches are
|
||||||
// or not. At most n positions (filename and line) are returned (and thus
|
// returned (thus found <= n).
|
||||||
// found <= n). The result is incomplete if the index is not present or
|
|
||||||
// if there are more than n occurrences of s.
|
|
||||||
//
|
//
|
||||||
func (x *Index) LookupString(s string, n int) (found int, result []Positions, complete bool) {
|
func (x *Index) LookupRegexp(r *regexp.Regexp, n int) (found int, result []FileLines) {
|
||||||
if x.suffixes == nil {
|
if x.suffixes == nil || n <= 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// n > 0
|
||||||
|
|
||||||
offsets := x.suffixes.Lookup([]byte(s), n+1)
|
var list positionList
|
||||||
if len(offsets) <= n {
|
// FindAllIndex may returns matches that span across file boundaries.
|
||||||
complete = true
|
// Such matches are unlikely, buf after eliminating them we may end up
|
||||||
} else {
|
// with fewer than n matches. If we don't have enough at the end, redo
|
||||||
offsets = offsets[0:n]
|
// the search with an increased value n1, but only if FindAllIndex
|
||||||
|
// returned all the requested matches in the first place (if it
|
||||||
|
// returned fewer than that there cannot be more).
|
||||||
|
for n1 := n; found < n; n1 += n - found {
|
||||||
|
found = 0
|
||||||
|
matches := x.suffixes.FindAllIndex(r, n1)
|
||||||
|
// compute files, exclude matches that span file boundaries,
|
||||||
|
// and map offsets to file-local offsets
|
||||||
|
list = make(positionList, len(matches))
|
||||||
|
for _, m := range matches {
|
||||||
|
// by construction, an offset corresponds to the Pos value
|
||||||
|
// for the file set - use it to get the file and line
|
||||||
|
p := token.Pos(m[0])
|
||||||
|
if file := x.fset.File(p); file != nil {
|
||||||
|
if base := file.Base(); base <= m[1] && m[1] <= base+file.Size() {
|
||||||
|
// match [m[0], m[1]) is within the file boundaries
|
||||||
|
list[found].filename = file.Name()
|
||||||
|
list[found].line = file.Line(p)
|
||||||
|
found++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if found == n || len(matches) < n1 {
|
||||||
|
// found all matches or there's no chance to find more
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
found = len(offsets)
|
list = list[0:found]
|
||||||
|
sort.Sort(list) // sort by filename
|
||||||
|
|
||||||
// compute file names and lines and sort the list by filename
|
// collect matches belonging to the same file
|
||||||
list := make(positionList, len(offsets))
|
|
||||||
for i, offs := range offsets {
|
|
||||||
// by construction, an offs corresponds to
|
|
||||||
// the Pos value for the file set - use it
|
|
||||||
// to get full Position information
|
|
||||||
pos := x.fset.Position(token.Pos(offs))
|
|
||||||
list[i].filename = pos.Filename
|
|
||||||
list[i].line = pos.Line
|
|
||||||
}
|
|
||||||
sort.Sort(list)
|
|
||||||
|
|
||||||
// compact positions with equal file names
|
|
||||||
var last string
|
var last string
|
||||||
var lines []int
|
var lines []int
|
||||||
for _, pos := range list {
|
addLines := func() {
|
||||||
if pos.filename != last {
|
if len(lines) > 0 {
|
||||||
if len(lines) > 0 {
|
// remove duplicate lines
|
||||||
result = append(result, Positions{last, lines})
|
result = append(result, FileLines{last, unique(lines)})
|
||||||
lines = nil
|
lines = nil
|
||||||
}
|
|
||||||
last = pos.filename
|
|
||||||
}
|
}
|
||||||
lines = append(lines, pos.line)
|
|
||||||
}
|
}
|
||||||
if len(lines) > 0 {
|
for _, m := range list {
|
||||||
result = append(result, Positions{last, lines})
|
if m.filename != last {
|
||||||
|
addLines()
|
||||||
|
last = m.filename
|
||||||
|
}
|
||||||
|
lines = append(lines, m.line)
|
||||||
}
|
}
|
||||||
|
addLines()
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -74,7 +74,7 @@ func serveError(w http.ResponseWriter, r *http.Request, relpath string, err os.E
|
|||||||
func exec(rw http.ResponseWriter, args []string) (status int) {
|
func exec(rw http.ResponseWriter, args []string) (status int) {
|
||||||
r, w, err := os.Pipe()
|
r, w, err := os.Pipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("os.Pipe(): %v\n", err)
|
log.Printf("os.Pipe(): %v", err)
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,7 +87,7 @@ func exec(rw http.ResponseWriter, args []string) (status int) {
|
|||||||
defer r.Close()
|
defer r.Close()
|
||||||
w.Close()
|
w.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("os.ForkExec(%q): %v\n", bin, err)
|
log.Printf("os.ForkExec(%q): %v", bin, err)
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,7 +96,7 @@ func exec(rw http.ResponseWriter, args []string) (status int) {
|
|||||||
wait, err := os.Wait(pid, 0)
|
wait, err := os.Wait(pid, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
os.Stderr.Write(buf.Bytes())
|
os.Stderr.Write(buf.Bytes())
|
||||||
log.Printf("os.Wait(%d, 0): %v\n", pid, err)
|
log.Printf("os.Wait(%d, 0): %v", pid, err)
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
status = wait.ExitStatus()
|
status = wait.ExitStatus()
|
||||||
@ -127,8 +127,7 @@ func dosync(w http.ResponseWriter, r *http.Request) {
|
|||||||
// TODO(gri): The directory tree may be temporarily out-of-sync.
|
// TODO(gri): The directory tree may be temporarily out-of-sync.
|
||||||
// Consider keeping separate time stamps so the web-
|
// Consider keeping separate time stamps so the web-
|
||||||
// page can indicate this discrepancy.
|
// page can indicate this discrepancy.
|
||||||
fsTree.set(newDirectory(*goroot, nil, -1))
|
initFSTree()
|
||||||
invalidateIndex()
|
|
||||||
fallthrough
|
fallthrough
|
||||||
case 1:
|
case 1:
|
||||||
// sync failed because no files changed;
|
// sync failed because no files changed;
|
||||||
@ -238,11 +237,14 @@ func main() {
|
|||||||
// HTTP server mode.
|
// HTTP server mode.
|
||||||
var handler http.Handler = http.DefaultServeMux
|
var handler http.Handler = http.DefaultServeMux
|
||||||
if *verbose {
|
if *verbose {
|
||||||
log.Printf("Go Documentation Server\n")
|
log.Printf("Go Documentation Server")
|
||||||
log.Printf("version = %s\n", runtime.Version())
|
log.Printf("version = %s", runtime.Version())
|
||||||
log.Printf("address = %s\n", *httpAddr)
|
log.Printf("address = %s", *httpAddr)
|
||||||
log.Printf("goroot = %s\n", *goroot)
|
log.Printf("goroot = %s", *goroot)
|
||||||
log.Printf("tabwidth = %d\n", *tabwidth)
|
log.Printf("tabwidth = %d", *tabwidth)
|
||||||
|
if *fulltextIndex {
|
||||||
|
log.Print("full text index enabled")
|
||||||
|
}
|
||||||
if !fsMap.IsEmpty() {
|
if !fsMap.IsEmpty() {
|
||||||
log.Print("user-defined mapping:")
|
log.Print("user-defined mapping:")
|
||||||
fsMap.Fprint(os.Stderr)
|
fsMap.Fprint(os.Stderr)
|
||||||
@ -257,10 +259,7 @@ func main() {
|
|||||||
|
|
||||||
// Initialize default directory tree with corresponding timestamp.
|
// Initialize default directory tree with corresponding timestamp.
|
||||||
// (Do it in a goroutine so that launch is quick.)
|
// (Do it in a goroutine so that launch is quick.)
|
||||||
go func() {
|
go initFSTree()
|
||||||
fsTree.set(newDirectory(*goroot, nil, -1))
|
|
||||||
invalidateIndex()
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Initialize directory trees for user-defined file systems (-path flag).
|
// Initialize directory trees for user-defined file systems (-path flag).
|
||||||
initDirTrees()
|
initDirTrees()
|
||||||
|
@ -13,41 +13,21 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"go/ast"
|
"go/ast"
|
||||||
"go/token"
|
"go/token"
|
||||||
"go/printer"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
type Snippet struct {
|
type Snippet struct {
|
||||||
Line int
|
Line int
|
||||||
Text string
|
Text []byte
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
type snippetStyler struct {
|
|
||||||
Styler // defined in godoc.go
|
|
||||||
highlight *ast.Ident // identifier to highlight
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func (s *snippetStyler) LineTag(line int) (text []uint8, tag printer.HTMLTag) {
|
|
||||||
return // no LineTag for snippets
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func (s *snippetStyler) Ident(id *ast.Ident) (text []byte, tag printer.HTMLTag) {
|
|
||||||
text = []byte(id.Name)
|
|
||||||
if s.highlight == id {
|
|
||||||
tag = printer.HTMLTag{"<span class=highlight>", "</span>"}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func newSnippet(fset *token.FileSet, decl ast.Decl, id *ast.Ident) *Snippet {
|
func 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 buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
writeNode(&buf, fset, decl, true, &snippetStyler{highlight: id})
|
writeNode(&buf, fset, decl, true)
|
||||||
return &Snippet{fset.Position(id.Pos()).Line, buf.String()}
|
return &Snippet{fset.Position(id.Pos()).Line, FormatText(buf.Bytes(), -1, true, id.Name, nil)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -113,9 +93,11 @@ func NewSnippet(fset *token.FileSet, decl ast.Decl, id *ast.Ident) (s *Snippet)
|
|||||||
|
|
||||||
// handle failure gracefully
|
// handle failure gracefully
|
||||||
if s == nil {
|
if s == nil {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
fmt.Fprintf(&buf, `<span class="alert">could not generate a snippet for <span class="highlight">%s</span></span>`, id.Name)
|
||||||
s = &Snippet{
|
s = &Snippet{
|
||||||
fset.Position(id.Pos()).Line,
|
fset.Position(id.Pos()).Line,
|
||||||
fmt.Sprintf(`could not generate a snippet for <span class="highlight">%s</span>`, id.Name),
|
buf.Bytes(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
Loading…
Reference in New Issue
Block a user