Merge branch 'num-workers' into issue-648-peach-num-worker

This commit is contained in:
Qi Xiao 2023-05-01 22:25:24 +01:00 committed by GitHub
commit 1f0d1c8251
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 675 additions and 522 deletions

View File

@ -5,6 +5,11 @@ Draft release notes for Elvish 0.20.0.
- The `peach` command now has a `&num-workers` option
([#648](https://github.com/elves/elvish/issues/648)).
- A new `str:fields` command ([#1689](https://b.elv.sh/1689)).
- The language server now supports showing the documentation of builtin
functions and variables on hover ([#1684](https://b.elv.sh/1684)).
# Breaking changes
- The `except` keyword in the `try` command was deprecated since 0.18.0 and is

3
go.mod
View File

@ -4,11 +4,12 @@ require (
github.com/creack/pty v1.1.15
github.com/google/go-cmp v0.5.9
github.com/mattn/go-isatty v0.0.17
github.com/sourcegraph/go-lsp v0.0.0-20200429204803-219e11d77f5d
github.com/sourcegraph/jsonrpc2 v0.2.0
go.etcd.io/bbolt v1.3.7
golang.org/x/sync v0.1.0
golang.org/x/sys v0.5.0
)
require pkg.nimblebun.works/go-lsp v1.1.0
go 1.19

4
go.sum
View File

@ -8,8 +8,6 @@ github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/sourcegraph/go-lsp v0.0.0-20200429204803-219e11d77f5d h1:afLbh+ltiygTOB37ymZVwKlJwWZn+86syPTbrrOAydY=
github.com/sourcegraph/go-lsp v0.0.0-20200429204803-219e11d77f5d/go.mod h1:SULmZY7YNBsvNiQbrb/BEDdEJ84TGnfyUQxaHt8t8rY=
github.com/sourcegraph/jsonrpc2 v0.2.0 h1:KjN/dC4fP6aN9030MZCJs9WQbTOjWHhrtKVpzzSrr/U=
github.com/sourcegraph/jsonrpc2 v0.2.0/go.mod h1:ZafdZgk/axhT1cvZAPOhw+95nz2I/Ra5qMlU4gTRwIo=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
@ -21,3 +19,5 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
pkg.nimblebun.works/go-lsp v1.1.0 h1:TH5ro4p2vlDtELK4LoVeKs4TsKm6aW1f5WP8jHm/9m4=
pkg.nimblebun.works/go-lsp v1.1.0/go.mod h1:Suh759Ki+DjU0zwf0xkl1H6Ln1C6/+GtYyNofbtfcug=

View File

@ -9,6 +9,7 @@ import (
"src.elv.sh/pkg/diag"
"src.elv.sh/pkg/eval"
"src.elv.sh/pkg/parse"
"src.elv.sh/pkg/parse/np"
)
// An error returned by Complete as well as the completers if there is no
@ -64,7 +65,7 @@ func Complete(code CodeBuffer, ev *eval.Evaler, cfg Config) (*Result, error) {
// Ignore the error; the function always returns a valid *ChunkNode.
tree, _ := parse.Parse(parse.Source{Name: "[interactive]", Code: code.Content}, parse.Config{})
path := findNodePath(tree.Root, code.Dot)
path := np.FindLeft(tree.Root, code.Dot)
if len(path) == 0 {
// This can happen when there is a parse error.
return nil, errNoCompletion

View File

@ -4,9 +4,10 @@ import (
"src.elv.sh/pkg/diag"
"src.elv.sh/pkg/eval"
"src.elv.sh/pkg/parse"
"src.elv.sh/pkg/parse/np"
)
var completers = []func(nodePath, *eval.Evaler, Config) (*context, []RawItem, error){
var completers = []func(np.Path, *eval.Evaler, Config) (*context, []RawItem, error){
completeCommand,
completeIndex,
completeRedir,
@ -21,63 +22,63 @@ type context struct {
interval diag.Ranging
}
func completeArg(np nodePath, ev *eval.Evaler, cfg Config) (*context, []RawItem, error) {
func completeArg(p np.Path, ev *eval.Evaler, cfg Config) (*context, []RawItem, error) {
var form *parse.Form
if np.match(aSep, store(&form)) && form.Head != nil {
if p.Match(np.Sep, np.Store(&form)) && form.Head != nil {
// Case 1: starting a new argument.
ctx := &context{"argument", "", parse.Bareword, range0(np[0].Range().To)}
args := purelyEvalForm(form, "", np[0].Range().To, ev)
items, err := generateArgs(args, ev, np, cfg)
ctx := &context{"argument", "", parse.Bareword, range0(p[0].Range().To)}
args := purelyEvalForm(form, "", p[0].Range().To, ev)
items, err := generateArgs(args, ev, p, cfg)
return ctx, items, err
}
expr := simpleExpr(ev)
if np.match(expr, store(&form)) && form.Head != nil && form.Head != expr.compound {
var expr np.SimpleExprData
if p.Match(np.SimpleExpr(&expr, ev), np.Store(&form)) && form.Head != nil && form.Head != expr.Compound {
// Case 2: in an incomplete argument.
ctx := &context{"argument", expr.s, expr.quote, expr.compound.Range()}
args := purelyEvalForm(form, expr.s, expr.compound.Range().From, ev)
items, err := generateArgs(args, ev, np, cfg)
ctx := &context{"argument", expr.Value, expr.PrimarType, expr.Compound.Range()}
args := purelyEvalForm(form, expr.Value, expr.Compound.Range().From, ev)
items, err := generateArgs(args, ev, p, cfg)
return ctx, items, err
}
return nil, nil, errNoCompletion
}
func completeCommand(np nodePath, ev *eval.Evaler, cfg Config) (*context, []RawItem, error) {
func completeCommand(p np.Path, ev *eval.Evaler, cfg Config) (*context, []RawItem, error) {
generateForEmpty := func(pos int) (*context, []RawItem, error) {
ctx := &context{"command", "", parse.Bareword, range0(pos)}
items, err := generateCommands("", ev, np)
items, err := generateCommands("", ev, p)
return ctx, items, err
}
if np.match(aChunk) {
if p.Match(np.Chunk) {
// Case 1: The leaf is a Chunk. That means that the chunk is empty
// (nothing entered at all) and it is a correct place for completing a
// command.
return generateForEmpty(np[0].Range().To)
return generateForEmpty(p[0].Range().To)
}
if np.match(aSep, aChunk) || np.match(aSep, aPipeline) {
if p.Match(np.Sep, np.Chunk) || p.Match(np.Sep, np.Pipeline) {
// Case 2: Just after a newline, semicolon, or a pipe.
return generateForEmpty(np[0].Range().To)
return generateForEmpty(p[0].Range().To)
}
var primary *parse.Primary
if np.match(aSep, store(&primary)) {
if p.Match(np.Sep, np.Store(&primary)) {
t := primary.Type
if t == parse.OutputCapture || t == parse.ExceptionCapture || t == parse.Lambda {
// Case 3: At the beginning of output, exception capture or lambda.
//
// TODO: Don't trigger after "{|".
return generateForEmpty(np[0].Range().To)
return generateForEmpty(p[0].Range().To)
}
}
expr := simpleExpr(ev)
var expr np.SimpleExprData
var form *parse.Form
if np.match(expr, store(&form)) && form.Head == expr.compound {
if p.Match(np.SimpleExpr(&expr, ev), np.Store(&form)) && form.Head == expr.Compound {
// Case 4: At an already started command.
ctx := &context{"command", expr.s, expr.quote, expr.compound.Range()}
items, err := generateCommands(expr.s, ev, np)
ctx := &context{"command", expr.Value, expr.PrimarType, expr.Compound.Range()}
items, err := generateCommands(expr.Value, ev, p)
return ctx, items, err
}
@ -86,30 +87,30 @@ func completeCommand(np nodePath, ev *eval.Evaler, cfg Config) (*context, []RawI
// NOTE: This now only supports a single level of indexing; for instance,
// $a[<Tab> is supported, but $a[x][<Tab> is not.
func completeIndex(np nodePath, ev *eval.Evaler, cfg Config) (*context, []RawItem, error) {
func completeIndex(p np.Path, ev *eval.Evaler, cfg Config) (*context, []RawItem, error) {
generateForEmpty := func(v any, pos int) (*context, []RawItem, error) {
ctx := &context{"index", "", parse.Bareword, range0(pos)}
return ctx, generateIndices(v), nil
}
var indexing *parse.Indexing
if np.match(aSep, store(&indexing)) || np.match(aSep, aArray, store(&indexing)) {
if p.Match(np.Sep, np.Store(&indexing)) || p.Match(np.Sep, np.Array, np.Store(&indexing)) {
// We are at a new index, either directly after the opening bracket, or
// after an existing index and some spaces.
if len(indexing.Indices) == 1 {
if indexee := ev.PurelyEvalPrimary(indexing.Head); indexee != nil {
return generateForEmpty(indexee, np[0].Range().To)
return generateForEmpty(indexee, p[0].Range().To)
}
}
}
expr := simpleExpr(ev)
if np.match(expr, aArray, store(&indexing)) {
var expr np.SimpleExprData
if p.Match(np.SimpleExpr(&expr, ev), np.Array, np.Store(&indexing)) {
// We are just after an incomplete index.
if len(indexing.Indices) == 1 {
if indexee := ev.PurelyEvalPrimary(indexing.Head); indexee != nil {
ctx := &context{
"index", expr.s, expr.quote, expr.compound.Range()}
"index", expr.Value, expr.PrimarType, expr.Compound.Range()}
return ctx, generateIndices(indexee), nil
}
}
@ -118,27 +119,27 @@ func completeIndex(np nodePath, ev *eval.Evaler, cfg Config) (*context, []RawIte
return nil, nil, errNoCompletion
}
func completeRedir(np nodePath, ev *eval.Evaler, cfg Config) (*context, []RawItem, error) {
if np.match(aSep, aRedir) {
func completeRedir(p np.Path, ev *eval.Evaler, cfg Config) (*context, []RawItem, error) {
if p.Match(np.Sep, np.Redir) {
// Empty redirection target.
ctx := &context{"redir", "", parse.Bareword, range0(np[0].Range().To)}
ctx := &context{"redir", "", parse.Bareword, range0(p[0].Range().To)}
items, err := generateFileNames("", false)
return ctx, items, err
}
expr := simpleExpr(ev)
if np.match(expr, aRedir) {
var expr np.SimpleExprData
if p.Match(np.SimpleExpr(&expr, ev), np.Redir) {
// Non-empty redirection target.
ctx := &context{"redir", expr.s, expr.quote, expr.compound.Range()}
items, err := generateFileNames(expr.s, false)
ctx := &context{"redir", expr.Value, expr.PrimarType, expr.Compound.Range()}
items, err := generateFileNames(expr.Value, false)
return ctx, items, err
}
return nil, nil, errNoCompletion
}
func completeVariable(np nodePath, ev *eval.Evaler, cfg Config) (*context, []RawItem, error) {
primary, ok := np[0].(*parse.Primary)
func completeVariable(p np.Path, ev *eval.Evaler, cfg Config) (*context, []RawItem, error) {
primary, ok := p[0].(*parse.Primary)
if !ok || primary.Type != parse.Variable {
return nil, nil, errNoCompletion
}
@ -152,7 +153,7 @@ func completeVariable(np nodePath, ev *eval.Evaler, cfg Config) (*context, []Raw
diag.Ranging{From: begin, To: primary.Range().To}}
var items []RawItem
eachVariableInNs(ev, np, ns, func(varname string) {
eachVariableInNs(ev, p, ns, func(varname string) {
items = append(items, noQuoteItem(parse.QuoteVariableName(varname)))
})
if ns == "" {

View File

@ -11,6 +11,7 @@ import (
"src.elv.sh/pkg/eval/vals"
"src.elv.sh/pkg/fsutil"
"src.elv.sh/pkg/parse"
"src.elv.sh/pkg/parse/np"
"src.elv.sh/pkg/ui"
)
@ -39,7 +40,7 @@ func GenerateForSudo(args []string, ev *eval.Evaler, cfg Config) ([]RawItem, err
// Internal generators, used from completers.
func generateArgs(args []string, ev *eval.Evaler, np nodePath, cfg Config) ([]RawItem, error) {
func generateArgs(args []string, ev *eval.Evaler, p np.Path, cfg Config) ([]RawItem, error) {
switch args[0] {
case "set", "tmp":
for _, arg := range args[1:] {
@ -51,7 +52,7 @@ func generateArgs(args []string, ev *eval.Evaler, np nodePath, cfg Config) ([]Ra
sigil, qname := eval.SplitSigil(seed)
ns, _ := eval.SplitIncompleteQNameNs(qname)
var items []RawItem
eachVariableInNs(ev, np, ns, func(varname string) {
eachVariableInNs(ev, p, ns, func(varname string) {
items = append(items, noQuoteItem(sigil+parse.QuoteVariableName(ns+varname)))
})
return items, nil
@ -71,7 +72,7 @@ func generateExternalCommands(seed string, ev *eval.Evaler) ([]RawItem, error) {
return items, nil
}
func generateCommands(seed string, ev *eval.Evaler, np nodePath) ([]RawItem, error) {
func generateCommands(seed string, ev *eval.Evaler, p np.Path) ([]RawItem, error) {
if fsutil.DontSearch(seed) {
// Completing a local external command name.
return generateFileNames(seed, true)
@ -99,7 +100,7 @@ func generateCommands(seed string, ev *eval.Evaler, np nodePath) ([]RawItem, err
ns, _ := eval.SplitIncompleteQNameNs(qname)
if sigil == "" {
// Generate functions, namespaces, and variable assignments.
eachVariableInNs(ev, np, ns, func(varname string) {
eachVariableInNs(ev, p, ns, func(varname string) {
switch {
case strings.HasSuffix(varname, eval.FnSuffix):
addPlainItem(

View File

@ -1,122 +0,0 @@
package complete
import (
"src.elv.sh/pkg/eval"
"src.elv.sh/pkg/parse"
)
type nodePath []parse.Node
// Returns the path of Node's from n to a leaf at position p. Leaf first in the
// returned slice.
func findNodePath(root parse.Node, p int) nodePath {
n := root
descend:
for len(parse.Children(n)) > 0 {
for _, ch := range parse.Children(n) {
if rg := ch.Range(); rg.From <= p && p <= rg.To {
n = ch
continue descend
}
}
return nil
}
var path []parse.Node
for {
path = append(path, n)
if n == root {
break
}
n = parse.Parent(n)
}
return path
}
func (ns nodePath) match(ms ...nodesMatcher) bool {
for _, m := range ms {
ns2, ok := m.matchNodes(ns)
if !ok {
return false
}
ns = ns2
}
return true
}
type nodesMatcher interface {
// Matches from the beginning of nodes. Returns the remaining nodes and
// whether the match succeeded.
matchNodes([]parse.Node) ([]parse.Node, bool)
}
// Matches one node of a given type, without storing it.
type typedMatcher[T parse.Node] struct{}
func (m typedMatcher[T]) matchNodes(ns []parse.Node) ([]parse.Node, bool) {
if len(ns) > 0 {
if _, ok := ns[0].(T); ok {
return ns[1:], true
}
}
return nil, false
}
var (
aChunk = typedMatcher[*parse.Chunk]{}
aPipeline = typedMatcher[*parse.Pipeline]{}
aArray = typedMatcher[*parse.Array]{}
aRedir = typedMatcher[*parse.Redir]{}
aSep = typedMatcher[*parse.Sep]{}
)
// Matches one node of a certain type, and stores it into a pointer.
type storeMatcher[T parse.Node] struct{ p *T }
func store[T parse.Node](p *T) nodesMatcher { return storeMatcher[T]{p} }
func (m storeMatcher[T]) matchNodes(ns []parse.Node) ([]parse.Node, bool) {
if len(ns) > 0 {
if n, ok := ns[0].(T); ok {
*m.p = n
return ns[1:], true
}
}
return nil, false
}
// Matches an expression that can be evaluated statically. Consumes 3 nodes
// (Primary, Indexing and Compound).
type simpleExprMatcher struct {
ev *eval.Evaler
s string
compound *parse.Compound
quote parse.PrimaryType
}
func simpleExpr(ev *eval.Evaler) *simpleExprMatcher {
return &simpleExprMatcher{ev: ev}
}
func (m *simpleExprMatcher) matchNodes(ns []parse.Node) ([]parse.Node, bool) {
if len(ns) < 3 {
return nil, false
}
primary, ok := ns[0].(*parse.Primary)
if !ok {
return nil, false
}
indexing, ok := ns[1].(*parse.Indexing)
if !ok {
return nil, false
}
compound, ok := ns[2].(*parse.Compound)
if !ok {
return nil, false
}
s, ok := m.ev.PurelyEvalPartialCompound(compound, indexing.To)
if !ok {
return nil, false
}
m.compound, m.quote, m.s = compound, primary.Type, s
return ns[3:], true
}

View File

@ -7,18 +7,19 @@ import (
"src.elv.sh/pkg/eval"
"src.elv.sh/pkg/parse"
"src.elv.sh/pkg/parse/cmpd"
"src.elv.sh/pkg/parse/np"
)
var environ = os.Environ
// Calls f for each variable name in namespace ns that can be found at the point
// of np.
func eachVariableInNs(ev *eval.Evaler, np nodePath, ns string, f func(s string)) {
func eachVariableInNs(ev *eval.Evaler, p np.Path, ns string, f func(s string)) {
switch ns {
case "", ":":
ev.Global().IterateKeysString(f)
ev.Builtin().IterateKeysString(f)
eachDefinedVariable(np[len(np)-1], np[0].Range().From, f)
eachDefinedVariable(p[len(p)-1], p[0].Range().From, f)
case "e:":
eachExternal(func(cmd string) {
f(cmd + eval.FnSuffix)

View File

@ -9,8 +9,10 @@ import (
"testing"
"time"
lsp "github.com/sourcegraph/go-lsp"
"github.com/google/go-cmp/cmp"
"github.com/sourcegraph/jsonrpc2"
lsp "pkg.nimblebun.works/go-lsp"
"src.elv.sh/pkg/mods/doc"
"src.elv.sh/pkg/must"
"src.elv.sh/pkg/prog"
"src.elv.sh/pkg/prog/progtest"
@ -31,7 +33,7 @@ var diagTests = []struct {
Range: lsp.Range{
Start: lsp.Position{Line: 0, Character: 1},
End: lsp.Position{Line: 0, Character: 2}},
Severity: lsp.Error, Source: "parse", Message: "should be variable name",
Severity: lsp.DSError, Source: "parse", Message: "should be variable name",
},
}},
{"multi line with NL", "\n$!", []lsp.Diagnostic{
@ -39,7 +41,7 @@ var diagTests = []struct {
Range: lsp.Range{
Start: lsp.Position{Line: 1, Character: 1},
End: lsp.Position{Line: 1, Character: 2}},
Severity: lsp.Error, Source: "parse", Message: "should be variable name",
Severity: lsp.DSError, Source: "parse", Message: "should be variable name",
},
}},
{"multi line with CR", "\r$!", []lsp.Diagnostic{
@ -47,7 +49,7 @@ var diagTests = []struct {
Range: lsp.Range{
Start: lsp.Position{Line: 1, Character: 1},
End: lsp.Position{Line: 1, Character: 2}},
Severity: lsp.Error, Source: "parse", Message: "should be variable name",
Severity: lsp.DSError, Source: "parse", Message: "should be variable name",
},
}},
{"multi line with CRNL", "\r\n$!", []lsp.Diagnostic{
@ -55,7 +57,7 @@ var diagTests = []struct {
Range: lsp.Range{
Start: lsp.Position{Line: 1, Character: 1},
End: lsp.Position{Line: 1, Character: 2}},
Severity: lsp.Error, Source: "parse", Message: "should be variable name",
Severity: lsp.DSError, Source: "parse", Message: "should be variable name",
},
}},
{"text with code point beyond FFFF", "\U00010000 $!", []lsp.Diagnostic{
@ -63,7 +65,7 @@ var diagTests = []struct {
Range: lsp.Range{
Start: lsp.Position{Line: 0, Character: 4},
End: lsp.Position{Line: 0, Character: 5}},
Severity: lsp.Error, Source: "parse", Message: "should be variable name",
Severity: lsp.DSError, Source: "parse", Message: "should be variable name",
},
}},
}
@ -91,15 +93,68 @@ func TestDidChangeDiagnostics(t *testing.T) {
}
}
var hoverTests = []struct {
name string
text string
pos lsp.Position
wantHover lsp.Hover
}{
{
name: "command doc",
text: "echo foo",
pos: lsp.Position{Line: 0, Character: 0},
wantHover: hoverWith(must.OK1(doc.Source("echo"))),
},
{
name: "variable doc",
// 012345
text: "echo $paths",
pos: lsp.Position{Line: 0, Character: 5},
wantHover: hoverWith(must.OK1(doc.Source("$paths"))),
},
{
name: "unknown command",
text: "some-external",
pos: lsp.Position{Line: 0, Character: 0},
wantHover: lsp.Hover{},
},
{
name: "command at non-command position",
// 012345678
text: "echo echo",
pos: lsp.Position{Line: 0, Character: 5},
wantHover: lsp.Hover{},
},
}
func hoverWith(markdown string) lsp.Hover {
return lsp.Hover{Contents: lsp.MarkupContent{Kind: lsp.MKMarkdown, Value: markdown}}
}
func TestHover(t *testing.T) {
f := setup(t)
f.conn.Notify(bgCtx, "textDocument/didOpen", didOpenParams(""))
// Hover is a no-op now; just check that it doesn't error.
var hover lsp.Hover
err := f.conn.Call(bgCtx, "textDocument/hover", struct{}{}, &hover)
if err != nil {
t.Errorf("got error %v", err)
for _, test := range hoverTests {
t.Run(test.name, func(t *testing.T) {
f.conn.Notify(bgCtx, "textDocument/didOpen", didOpenParams(test.text))
request := lsp.TextDocumentPositionParams{
TextDocument: lsp.TextDocumentIdentifier{URI: testURI},
Position: test.pos,
}
var response lsp.Hover
err := f.conn.Call(bgCtx, "textDocument/hover", request, &response)
if err != nil {
t.Errorf("got error %v", err)
}
if diff := cmp.Diff(test.wantHover, response); diff != "" {
t.Errorf("response (-want +got):\n%s", diff)
}
})
}
}
@ -143,23 +198,31 @@ func TestCompletion(t *testing.T) {
}
var jsonrpcErrorTests = []struct {
name string
method string
params any
wantErr error
}{
{"unknown/method", struct{}{}, errMethodNotFound},
{"textDocument/didOpen", []int{}, errInvalidParams},
{"textDocument/didChange", []int{}, errInvalidParams},
{"textDocument/completion", []int{}, errInvalidParams},
{"unknown method", "unknown/method", struct{}{}, errMethodNotFound},
{"invalid request type", "textDocument/didOpen", []int{}, errInvalidParams},
{"unknown document to hover", "textDocument/hover",
lsp.TextDocumentPositionParams{
TextDocument: lsp.TextDocumentIdentifier{URI: "file://unknown"}},
unknownDocument("file://unknown")},
{"unknown document to completion", "textDocument/completion",
lsp.CompletionParams{
TextDocumentPositionParams: lsp.TextDocumentPositionParams{
TextDocument: lsp.TextDocumentIdentifier{URI: "file://unknown"}}},
unknownDocument("file://unknown")},
}
func TestJSONRPCErrors(t *testing.T) {
f := setup(t)
for _, test := range jsonrpcErrorTests {
t.Run(test.method, func(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
err := f.conn.Call(context.Background(), test.method, test.params, &struct{}{})
if err.Error() != test.wantErr.Error() {
t.Errorf("got error %v, want %v", err, errMethodNotFound)
t.Errorf("got error %v, want %v", err, test.wantErr)
}
})
}

View File

@ -3,13 +3,16 @@ package lsp
import (
"context"
"encoding/json"
"fmt"
lsp "github.com/sourcegraph/go-lsp"
"github.com/sourcegraph/jsonrpc2"
lsp "pkg.nimblebun.works/go-lsp"
"src.elv.sh/pkg/diag"
"src.elv.sh/pkg/edit/complete"
"src.elv.sh/pkg/eval"
"src.elv.sh/pkg/mods/doc"
"src.elv.sh/pkg/parse"
"src.elv.sh/pkg/parse/np"
)
var (
@ -20,12 +23,18 @@ var (
)
type server struct {
evaler *eval.Evaler
content map[lsp.DocumentURI]string
evaler *eval.Evaler
documents map[lsp.DocumentURI]document
}
type document struct {
code string
parseTree parse.Tree
parseErr error
}
func newServer() *server {
return &server{eval.NewEvaler(), make(map[lsp.DocumentURI]string)}
return &server{eval.NewEvaler(), make(map[lsp.DocumentURI]document)}
}
func handler(s *server) jsonrpc2.Handler {
@ -33,7 +42,7 @@ func handler(s *server) jsonrpc2.Handler {
"initialize": s.initialize,
"textDocument/didOpen": convertMethod(s.didOpen),
"textDocument/didChange": convertMethod(s.didChange),
"textDocument/hover": s.hover,
"textDocument/hover": convertMethod(s.hover),
"textDocument/completion": convertMethod(s.completion),
"textDocument/didClose": noop,
@ -71,6 +80,8 @@ func routingHandler(methods map[string]method) jsonrpc2.Handler {
})
}
// Can be used within handler implementations to recover the connection stored
// in the Context.
func conn(ctx context.Context) *jsonrpc2.Conn { return ctx.Value(connKey{}).(*jsonrpc2.Conn) }
// Handler implementations. These are all called synchronously.
@ -78,21 +89,19 @@ func conn(ctx context.Context) *jsonrpc2.Conn { return ctx.Value(connKey{}).(*js
func (s *server) initialize(_ context.Context, _ json.RawMessage) (any, error) {
return &lsp.InitializeResult{
Capabilities: lsp.ServerCapabilities{
TextDocumentSync: &lsp.TextDocumentSyncOptionsOrKind{
Options: &lsp.TextDocumentSyncOptions{
OpenClose: true,
Change: lsp.TDSKFull,
},
TextDocumentSync: &lsp.TextDocumentSyncOptions{
OpenClose: true,
Change: lsp.TDSyncKindFull,
},
CompletionProvider: &lsp.CompletionOptions{},
HoverProvider: &lsp.HoverOptions{},
},
}, nil
}
func (s *server) didOpen(ctx context.Context, params lsp.DidOpenTextDocumentParams) (any, error) {
uri, content := params.TextDocument.URI, params.TextDocument.Text
s.content[uri] = content
go publishDiagnostics(ctx, uri, content)
s.updateDocument(conn(ctx), uri, content)
return nil, nil
}
@ -100,21 +109,50 @@ func (s *server) didChange(ctx context.Context, params lsp.DidChangeTextDocument
// ContentChanges includes full text since the server is only advertised to
// support that; see the initialize method.
uri, content := params.TextDocument.URI, params.ContentChanges[0].Text
s.content[uri] = content
go publishDiagnostics(ctx, uri, content)
s.updateDocument(conn(ctx), uri, content)
return nil, nil
}
func (s *server) hover(_ context.Context, rawParams json.RawMessage) (any, error) {
return lsp.Hover{}, nil
func (s *server) hover(_ context.Context, params lsp.TextDocumentPositionParams) (any, error) {
document, ok := s.documents[params.TextDocument.URI]
if !ok {
return nil, unknownDocument(params.TextDocument.URI)
}
pos := lspPositionToIdx(document.code, params.Position)
p := np.Find(document.parseTree.Root, pos)
// Try variable doc
var primary *parse.Primary
if p.Match(np.Store(&primary)) && primary.Type == parse.Variable {
// TODO: Take shadowing into consideration.
markdown, err := doc.Source("$" + primary.Value)
if err == nil {
return lsp.Hover{Contents: lsp.MarkupContent{Kind: lsp.MKMarkdown, Value: markdown}}, nil
}
}
// Try command doc
var expr np.SimpleExprData
var form *parse.Form
if p.Match(np.SimpleExpr(&expr, nil), np.Store(&form)) && form.Head == expr.Compound {
// TODO: Take shadowing into consideration.
markdown, err := doc.Source(expr.Value)
if err == nil {
return lsp.Hover{Contents: lsp.MarkupContent{Kind: lsp.MKMarkdown, Value: markdown}}, nil
}
}
return nil, nil
}
func (s *server) completion(_ context.Context, params lsp.CompletionParams) (any, error) {
content := s.content[params.TextDocument.URI]
document, ok := s.documents[params.TextDocument.URI]
if !ok {
return nil, unknownDocument(params.TextDocument.URI)
}
code := document.code
result, err := complete.Complete(
complete.CodeBuffer{
Content: content,
Dot: lspPositionToIdx(content, params.Position)},
Content: code,
Dot: lspPositionToIdx(code, params.Position)},
s.evaler,
complete.Config{},
)
@ -124,7 +162,7 @@ func (s *server) completion(_ context.Context, params lsp.CompletionParams) (any
}
lspItems := make([]lsp.CompletionItem, len(result.Items))
lspRange := lspRangeFromRange(content, result.Replace)
lspRange := lspRangeFromRange(code, result.Replace)
var kind lsp.CompletionItemKind
switch result.Name {
case "command":
@ -147,28 +185,31 @@ func (s *server) completion(_ context.Context, params lsp.CompletionParams) (any
return lspItems, nil
}
func publishDiagnostics(ctx context.Context, uri lsp.DocumentURI, content string) {
conn(ctx).Notify(ctx, "textDocument/publishDiagnostics",
lsp.PublishDiagnosticsParams{URI: uri, Diagnostics: diagnostics(uri, content)})
func (s *server) updateDocument(conn *jsonrpc2.Conn, uri lsp.DocumentURI, code string) {
tree, err := parse.Parse(parse.Source{Name: string(uri), Code: code}, parse.Config{})
s.documents[uri] = document{code, tree, err}
go func() {
// Convert the parse error to lsp.Diagnostic objects and publish them.
entries := parse.UnpackErrors(err)
diags := make([]lsp.Diagnostic, len(entries))
for i, err := range entries {
diags[i] = lsp.Diagnostic{
Range: lspRangeFromRange(code, err),
Severity: lsp.DSError,
Source: "parse",
Message: err.Message,
}
}
conn.Notify(context.Background(), "textDocument/publishDiagnostics",
lsp.PublishDiagnosticsParams{URI: uri, Diagnostics: diags})
}()
}
func diagnostics(uri lsp.DocumentURI, content string) []lsp.Diagnostic {
_, err := parse.Parse(parse.Source{Name: string(uri), Code: content}, parse.Config{})
if err == nil {
return []lsp.Diagnostic{}
func unknownDocument(uri lsp.DocumentURI) error {
return &jsonrpc2.Error{
Code: jsonrpc2.CodeInvalidParams,
Message: fmt.Sprintf("unknown document: %v", uri),
}
entries := parse.UnpackErrors(err)
diags := make([]lsp.Diagnostic, len(entries))
for i, err := range entries {
diags[i] = lsp.Diagnostic{
Range: lspRangeFromRange(content, err),
Severity: lsp.Error,
Source: "parse",
Message: err.Message,
}
}
return diags
}
func lspRangeFromRange(s string, r diag.Ranger) lsp.Range {

View File

@ -33,7 +33,7 @@ var Ns = eval.BuildNsNamed("doc").
AddGoFns(map[string]any{
"show": show,
"find": find,
"source": source,
"source": Source,
"-symbols": symbols,
}).
Ns()
@ -48,7 +48,7 @@ type showOptions struct{ Width int }
func (opts *showOptions) SetDefaultOptions() {}
func show(fm *eval.Frame, opts showOptions, fqname string) error {
doc, err := source(fqname)
doc, err := Source(fqname)
if err != nil {
return err
}
@ -103,7 +103,8 @@ func find(fm *eval.Frame, qs ...string) {
}
}
func source(fqname string) (string, error) {
// Source returns the doc source for a symbol.
func Source(fqname string) (string, error) {
isVar := strings.HasPrefix(fqname, "$")
if isVar {
fqname = fqname[1:]

View File

@ -54,6 +54,21 @@ fn count {|str substr| }
# ```
fn equal-fold {|str1 str2| }
# Splits `$str` around each instance of one or more consecutive white space
# characters.
#
# ```elvish-transcript
# ~> str:split "lorem ipsum dolor"
# ▶ lorem
# ▶ ipsum
# ▶ dolor
# ~> str:split " "
# ```
#
# See also [`str:split`]().
fn fields {|str| }
# Outputs a string consisting of the given Unicode codepoints. Example:
#
# ```elvish-transcript
@ -184,7 +199,7 @@ fn replace {|&max=-1 old repl source| }
# Etymology: Various languages, in particular
# [Python](https://docs.python.org/3.6/library/stdtypes.html#str.split).
#
# See also [`str:join`]().
# See also [`str:join`]() and [`str:fields`]().
fn split {|&max=-1 sep string| }
# Outputs `$str` with all Unicode letters that begin words mapped to their

View File

@ -23,7 +23,8 @@ var Ns = eval.BuildNsNamed("str").
"contains-any": strings.ContainsAny,
"count": strings.Count,
"equal-fold": strings.EqualFold,
// TODO: Fields, FieldsFunc
// TODO: FieldsFunc
"fields": strings.Fields,
"from-codepoints": fromCodepoints,
"from-utf8-bytes": fromUtf8Bytes,
"has-prefix": strings.HasPrefix,

View File

@ -35,6 +35,10 @@ func TestStr(t *testing.T) {
That(`str:equal-fold abc ABC`).Puts(true),
That(`str:equal-fold abc A`).Puts(false),
That(`str:fields "abc ABC"`).Puts("abc", "ABC"),
That(`str:fields "abc ABC"`).Puts("abc", "ABC"),
That(`str:fields " "`).Puts(),
That(`str:from-codepoints 0x61`).Puts("a"),
That(`str:from-codepoints 0x4f60 0x597d`).Puts("你好"),
That(`str:from-codepoints -0x1`).Throws(errs.OutOfRange{

View File

@ -0,0 +1,7 @@
package cmpd
import "testing"
func Test(t *testing.T) {
// Test coverage of this package is provided by tests of its users.
}

145
pkg/parse/np/np.go Normal file
View File

@ -0,0 +1,145 @@
// Package np provides utilities for working with node paths from a leaf of a
// parse tree to the root.
package np
import (
"src.elv.sh/pkg/eval"
"src.elv.sh/pkg/parse"
)
// Path is a path from a leaf in a parse tree to the root.
type Path []parse.Node
// Find finds the path of nodes from the leaf at position p to the root.
func Find(root parse.Node, p int) Path { return find(root, p, false) }
// FindLeft finds the path of nodes from the leaf at position p to the root. If
// p points to the start of one node (p == x.From), FindLeft finds the node to
// the left instead (y s.t. p == y.To).
func FindLeft(root parse.Node, p int) Path { return find(root, p, true) }
func find(root parse.Node, p int, preferLeft bool) Path {
n := root
descend:
for len(parse.Children(n)) > 0 {
for _, ch := range parse.Children(n) {
r := ch.Range()
if r.From <= p && p < r.To || preferLeft && p == r.To {
n = ch
continue descend
}
}
return nil
}
var path []parse.Node
for {
path = append(path, n)
if n == root {
break
}
n = parse.Parent(n)
}
return path
}
// Match matches against matchers, and returns whether all matches have
// succeeded.
func (p Path) Match(ms ...Matcher) bool {
for _, m := range ms {
p2, ok := m.Match(p)
if !ok {
return false
}
p = p2
}
return true
}
// Matcher wraps the Match method.
type Matcher interface {
// Match takes a slice of nodes and returns the remaining nodes and whether
// the match succeeded.
Match([]parse.Node) ([]parse.Node, bool)
}
// Typed returns a [Matcher] matching one node of a given type.
func Typed[T parse.Node]() Matcher { return typedMatcher[T]{} }
// Commonly used [Typed] matchers.
var (
Chunk = Typed[*parse.Chunk]()
Pipeline = Typed[*parse.Pipeline]()
Array = Typed[*parse.Array]()
Redir = Typed[*parse.Redir]()
Sep = Typed[*parse.Sep]()
)
type typedMatcher[T parse.Node] struct{}
func (m typedMatcher[T]) Match(ns []parse.Node) ([]parse.Node, bool) {
if len(ns) > 0 {
if _, ok := ns[0].(T); ok {
return ns[1:], true
}
}
return nil, false
}
// Store returns a [Matcher] matching one node of a given type, and stores it
// if a match succeeds.
func Store[T parse.Node](p *T) Matcher { return storeMatcher[T]{p} }
type storeMatcher[T parse.Node] struct{ p *T }
func (m storeMatcher[T]) Match(ns []parse.Node) ([]parse.Node, bool) {
if len(ns) > 0 {
if n, ok := ns[0].(T); ok {
*m.p = n
return ns[1:], true
}
}
return nil, false
}
// SimpleExpr returns a [Matcher] matching a "simple expression", which consists
// of 3 nodes from the leaf upwards (Primary, Indexing and Compound) and where
// the Compound expression can be evaluated statically using ev.
func SimpleExpr(data *SimpleExprData, ev *eval.Evaler) Matcher {
return simpleExprMatcher{data, ev}
}
// SimpleExprData contains useful data written by the [SimpleExpr] matcher.
type SimpleExprData struct {
Value string
Compound *parse.Compound
PrimarType parse.PrimaryType
}
type simpleExprMatcher struct {
data *SimpleExprData
ev *eval.Evaler
}
func (m simpleExprMatcher) Match(ns []parse.Node) ([]parse.Node, bool) {
if len(ns) < 3 {
return nil, false
}
primary, ok := ns[0].(*parse.Primary)
if !ok {
return nil, false
}
indexing, ok := ns[1].(*parse.Indexing)
if !ok {
return nil, false
}
compound, ok := ns[2].(*parse.Compound)
if !ok {
return nil, false
}
value, ok := m.ev.PurelyEvalPartialCompound(compound, indexing.To)
if !ok {
return nil, false
}
*m.data = SimpleExprData{value, compound, primary.Type}
return ns[3:], true
}

7
pkg/parse/np/np_test.go Normal file
View File

@ -0,0 +1,7 @@
package np
import "testing"
func Test(t *testing.T) {
// Test coverage of this package is provided by tests of its users.
}

View File

@ -149,32 +149,28 @@ func processHTMLID(s string) string {
}
const tocBefore = `
<div id="pandoc-toc-wrapper">
<p>Table of Content: <span id="pandoc-toc-toggle-wrapper"></span></p>
<div id="pandoc-toc">
<div id="toc-wrapper">
<div id="toc-header"><span id="toc-status"></span> Table of content</div>
<div id="toc">
`
const tocAfter = `
</div>
<script>
(function() {
var shown = true,
tocToggleWrapper = document.getElementById('pandoc-toc-toggle-wrapper'),
tocList = document.getElementById('pandoc-toc');
var tocToggle = document.createElement('a');
tocToggle.innerText = "[Hide]";
tocToggle.href = "";
tocToggleWrapper.appendChild(tocToggle);
tocToggle.onclick = function(ev) {
shown = !shown;
if (shown) {
tocToggle.innerText = "[Hide]";
tocList.className = "";
var open = true,
tocHeader = document.getElementById('toc-header'),
tocStatus = document.getElementById('toc-status'),
tocList = document.getElementById('toc');
tocHeader.onclick = function() {
open = !open;
if (open) {
tocStatus.className = '';
tocList.className = '';
} else {
tocToggle.innerText = "[Show]";
tocList.className = "no-display";
tocStatus.className = 'closed';
tocList.className = 'no-display';
}
ev.preventDefault();
};
})();
</script>

View File

@ -22,7 +22,7 @@ import (
const (
terminalRows = 100
terminalCols = 58
terminalCols = 52
)
const (

View File

@ -23,7 +23,7 @@ are supported:
Now find your platform in the table, and download the corresponding binary
archive:
<table>
<table class="extra-wide">
<tr>
<th>Version</th>
<th>amd64</th>
@ -348,7 +348,7 @@ nix-env -i elvish
The following old versions are no longer supported. They are only listed here
for historical interest.
<table>
<table class="extra-wide">
<tr>
<th>Version</th>
<th>amd64</th>

View File

@ -17,13 +17,8 @@ Windows.
<div class="demo-col demo-description">
<h2>Powerful Pipelines</h2>
<p>
Text pipelines are intuitive and powerful. However, if your data have
inherently complex structures, processing them with the pipeline
often requires a lot of ad-hoc, hard-to-maintain text processing code.
</p>
<p>
Pipelines in Elvish can carry structured data, not just text. You can
stream lists, maps and even functions through the pipeline.
Pipelines in Elvish can carry structured data, not just text. Stream
lists, maps and even functions through the pipeline.
</p>
</div>
<div class="demo-col demo-ttyshot">
@ -35,14 +30,8 @@ Windows.
<div class="demo-col demo-description">
<h2>Intuitive Control Structures</h2>
<p>
If you know programming, you probably already know how
<code>if</code> looks in C. So why learn another syntax?
</p>
<p>
Elvish comes with a standard set of control structures: conditional
control with <code>if</code>, loops with <code>for</code> and
<code>while</code>, and exception handling with <code>try</code>. All
of them have a familiar C-like syntax.
Control structures in Elvish have a familiar C-like syntax. Never spell
<code>if</code> backwards again.
</p>
</div>
<div class="demo-col demo-ttyshot">
@ -54,14 +43,8 @@ Windows.
<div class="demo-col demo-description">
<h2>Directory History</h2>
<p>
Do you type far too many <code>cd</code> commands? Do you struggle to
remember which <code>deeply/nested/directory</code> your source codes,
logs and configuration files are in?
</p>
<p>
Backed by a real database, Elvish remembers all the directories you
have been to, all the time. Just press <kbd>Ctrl-L</kbd>
and search, as you do in a browser.
Press <kbd>Ctrl-L</kbd> and jump to any directory you've been to.
Type <code>cd java/com/lorem/ipsum</code> once and only once.
</p>
</div>
<div class="demo-col demo-ttyshot">
@ -73,14 +56,8 @@ Windows.
<div class="demo-col demo-description">
<h2>Command History</h2>
<p>
Want to find the magical <code>ffmpeg</code> command that you used to
transcode a video file two months ago, but it is buried under a
million other commands?
</p>
<p>
No more cycling through history one command at a time.
Press <kbd>Ctrl-R</kbd> and start searching your entire
command history.
Press <kbd>Ctrl-R</kbd> and find that beautiful <code>ffmpeg</code>
command you used to transcode a video file two months ago.
</p>
</div>
<div class="demo-col demo-ttyshot">
@ -92,14 +69,8 @@ Windows.
<div class="demo-col demo-description">
<h2>Built-in File Manager</h2>
<p>
Want the convenience of a file manager, but can't give up the power of
a shell?
</p>
<p>
You no longer have to choose. Press
<kbd>Ctrl-N</kbd> to start exploring directories and
preview files, with the full power of a shell still under your
fingertips.
Press <kbd>Ctrl-N</kbd> to explore directories and preview files, with
the full power of a shell still under your fingertips.
</p>
</div>
<div class="demo-col demo-ttyshot">
@ -128,7 +99,7 @@ Start your Elvish journey in this very website!
- Peruse the definitive [reference](ref/) documents
- Read the [blog](blog/) for news, tips, and developers' musings
- Read the [blog](blog/) for the latest news
- Subscribe to the [feed](feed.atom) to keep updated

View File

@ -3,9 +3,9 @@
echo $x.pdf
}
~> try {
fail 'bad error'
fail 'something bad happened'
} catch e {
echo error $e
echo (styled 'error:' red) $e[reason][content]
} else {
echo ok
}

View File

@ -6,10 +6,10 @@ good
lorem.pdf
ipsum.pdf
~&gt; <span class="sgr-32">try</span> <span class="sgr-1">{</span>
<span class="sgr-32">fail</span> <span class="sgr-33">'bad error'</span>
<span class="sgr-1">}</span> catch e <span class="sgr-1">{</span>
<span class="sgr-32">echo</span> error <span class="sgr-35">$e</span>
<span class="sgr-1">}</span> else <span class="sgr-1">{</span>
<span class="sgr-32">fail</span> <span class="sgr-33">'something bad happened'</span>
<span class="sgr-1">}</span> <span class="sgr-33">catch</span> <span class="sgr-35">e</span> <span class="sgr-1">{</span>
<span class="sgr-32">echo</span> <span class="sgr-1">(</span><span class="sgr-32">styled</span> <span class="sgr-33">'error:'</span> red<span class="sgr-1">)</span> <span class="sgr-35">$e</span><span class="sgr-1">[</span>reason<span class="sgr-1">][</span>content<span class="sgr-1">]</span>
<span class="sgr-1">}</span> <span class="sgr-33">else</span> <span class="sgr-1">{</span>
<span class="sgr-32">echo</span> ok
<span class="sgr-1">}</span>
error [&amp;reason=[&amp;content='bad error' &amp;type=fail]]
<span class="sgr-31">error:</span> something bad happened

View File

@ -1,15 +1,15 @@
~&gt; <span class="sgr-7fg sgr-7bg">elf@host</span>
<span class="sgr-1 sgr-37 sgr-45"> HISTORY (dedup on) </span>
3 echo "hello\nbye" &gt; /tmp/x <span class="sgr-35"></span>
4 from-lines &lt; /tmp/x <span class="sgr-35"></span>
5 cd /tmp <span class="sgr-7fg sgr-45"> </span>
6 cd ~/elvish <span class="sgr-7fg sgr-45"> </span>
7 git branch <span class="sgr-7fg sgr-45"> </span>
8 git checkout . <span class="sgr-7fg sgr-45"> </span>
9 git commit <span class="sgr-7fg sgr-45"> </span>
19 git status <span class="sgr-7fg sgr-45"> </span>
20 cd /usr/local/bin <span class="sgr-7fg sgr-45"> </span>
21 echo $pwd <span class="sgr-7fg sgr-45"> </span>
<span class="sgr-7fg sgr-7bg"> 22 * (+ 3 4) (- 100 94) </span><span class="sgr-7fg sgr-45"> </span>
31 make <span class="sgr-7fg sgr-45"> </span>
32 math:min 3 1 30 <span class="sgr-7fg sgr-45"> </span>
~&gt; <span class="sgr-7fg sgr-7bg">elf@host</span>
<span class="sgr-1 sgr-37 sgr-45"> HISTORY (dedup on) </span> <span class="sgr-7fg sgr-7bg">Ctrl-D</span> dedup
3 echo "hello\nbye" &gt; /tmp/x <span class="sgr-35"></span>
4 from-lines &lt; /tmp/x <span class="sgr-35"></span>
5 cd /tmp <span class="sgr-7fg sgr-45"> </span>
6 cd ~/elvish <span class="sgr-7fg sgr-45"> </span>
7 git branch <span class="sgr-7fg sgr-45"> </span>
8 git checkout . <span class="sgr-7fg sgr-45"> </span>
9 git commit <span class="sgr-7fg sgr-45"> </span>
19 git status <span class="sgr-7fg sgr-45"> </span>
20 cd /usr/local/bin <span class="sgr-7fg sgr-45"> </span>
21 echo $pwd <span class="sgr-7fg sgr-45"> </span>
<span class="sgr-7fg sgr-7bg"> 22 * (+ 3 4) (- 100 94) </span><span class="sgr-7fg sgr-45"> </span>
31 make <span class="sgr-7fg sgr-45"> </span>
32 math:min 3 1 30 <span class="sgr-7fg sgr-45"> </span>

View File

@ -128,14 +128,8 @@ ul#demo-switcher > li > a:hover {
/* Overriding default styles */
.article h1 {
border-bottom: none;
padding-bottom: 0;
}
.article li > p {
margin-top: 0.5em;
margin-bottom: 0.5em;
.content h1 {
border: none;
}
/**

View File

@ -1,15 +1,15 @@
~&gt; <span class="sgr-7fg sgr-7bg">elf@host</span>
~&gt; <span class="sgr-7fg sgr-7bg">elf@host</span>
<span class="sgr-1 sgr-37 sgr-45"> LOCATION </span>
<span class="sgr-7fg sgr-7bg"> 10 ~/elvish </span><span class="sgr-7fg sgr-45"> </span>
10 ~/.local/share/elvish <span class="sgr-7fg sgr-45"> </span>
10 ~/elvish/website <span class="sgr-7fg sgr-45"> </span>
10 ~/.config/elvish <span class="sgr-7fg sgr-45"> </span>
9 ~/elvish/pkg/edit <span class="sgr-7fg sgr-45"> </span>
9 ~/elvish/pkg/eval <span class="sgr-7fg sgr-45"> </span>
9 /opt <span class="sgr-7fg sgr-45"> </span>
9 /usr/local <span class="sgr-7fg sgr-45"> </span>
9 /usr/local/share <span class="sgr-7fg sgr-45"> </span>
9 /usr/local/bin <span class="sgr-7fg sgr-45"> </span>
9 /usr <span class="sgr-7fg sgr-45"> </span>
9 /tmp <span class="sgr-35"></span>
8 ~/zsh <span class="sgr-35"></span>
<span class="sgr-7fg sgr-7bg"> 10 ~/elvish </span><span class="sgr-7fg sgr-45"> </span>
10 ~/.local/share/elvish <span class="sgr-7fg sgr-45"> </span>
10 ~/elvish/website <span class="sgr-7fg sgr-45"> </span>
10 ~/.config/elvish <span class="sgr-7fg sgr-45"> </span>
9 ~/elvish/pkg/edit <span class="sgr-7fg sgr-45"> </span>
9 ~/elvish/pkg/eval <span class="sgr-7fg sgr-45"> </span>
9 /opt <span class="sgr-7fg sgr-45"> </span>
9 /usr/local <span class="sgr-7fg sgr-45"> </span>
9 /usr/local/share <span class="sgr-7fg sgr-45"> </span>
9 /usr/local/bin <span class="sgr-7fg sgr-45"> </span>
9 /usr <span class="sgr-7fg sgr-45"> </span>
9 /tmp <span class="sgr-35"></span>
8 ~/zsh <span class="sgr-35"></span>

View File

@ -1,15 +1,15 @@
~/elvish&gt; <span class="sgr-7fg sgr-7bg">elf@host</span>
<span class="sgr-1 sgr-37 sgr-45"> NAVIGATING </span>
<span class="sgr-1 sgr-34"> bash </span> <span class="sgr-7fg sgr-7bg"> 1.0-release.md </span><span class="sgr-7fg sgr-45"> </span> 1.0 has not been released yet.
<span class="sgr-7fg sgr-1 sgr-44"> elvish </span> CONTRIBUTING.md <span class="sgr-7fg sgr-45"> </span>
<span class="sgr-1 sgr-34"> zsh </span> Dockerfile <span class="sgr-7fg sgr-45"> </span>
LICENSE <span class="sgr-7fg sgr-45"> </span>
Makefile <span class="sgr-7fg sgr-45"> </span>
PACKAGING.md <span class="sgr-7fg sgr-45"> </span>
README.md <span class="sgr-7fg sgr-45"> </span>
SECURITY.md <span class="sgr-7fg sgr-45"> </span>
<span class="sgr-1 sgr-34"> cmd </span><span class="sgr-7fg sgr-45"> </span>
go.mod <span class="sgr-7fg sgr-45"> </span>
go.sum <span class="sgr-7fg sgr-45"> </span>
<span class="sgr-1 sgr-34"> pkg </span><span class="sgr-35"></span>
<span class="sgr-1 sgr-34"> syntaxes </span><span class="sgr-35"></span>
~/elvish&gt; <span class="sgr-7fg sgr-7bg">elf@host</span>
<span class="sgr-1 sgr-37 sgr-45"> NAVIGATING </span> <span class="sgr-7fg sgr-7bg">Ctrl-H</span> hidden <span class="sgr-7fg sgr-7bg">Ctrl-F</span> filter
<span class="sgr-1 sgr-34"> bash </span> <span class="sgr-7fg sgr-7bg"> 1.0-release.m </span><span class="sgr-7fg sgr-45"> </span> 1.0 has not been released y
<span class="sgr-7fg sgr-1 sgr-44"> elvis </span> CONTRIBUTING. <span class="sgr-7fg sgr-45"> </span>
<span class="sgr-1 sgr-34"> zsh </span> Dockerfile <span class="sgr-7fg sgr-45"> </span>
LICENSE <span class="sgr-7fg sgr-45"> </span>
Makefile <span class="sgr-7fg sgr-45"> </span>
PACKAGING.md <span class="sgr-7fg sgr-45"> </span>
README.md <span class="sgr-7fg sgr-45"> </span>
SECURITY.md <span class="sgr-7fg sgr-45"> </span>
<span class="sgr-1 sgr-34"> cmd </span><span class="sgr-7fg sgr-45"> </span>
go.mod <span class="sgr-7fg sgr-45"> </span>
go.sum <span class="sgr-7fg sgr-45"> </span>
<span class="sgr-1 sgr-34"> pkg </span><span class="sgr-35"></span>
<span class="sgr-1 sgr-34"> syntaxes </span><span class="sgr-35"></span>

View File

@ -1,4 +1,4 @@
~> curl -sL api.github.com/repos/elves/elvish/issues |
all (from-json) |
each {|x| echo (exact-num $x[number]): $x[title] } |
head -n 10
~> range 1100 1111 |
each {|x| curl -sL xkcd.com/$x/info.0.json } |
from-json |
each {|x| printf "%g: %s\n" $x[num] $x[title] }

View File

@ -1,15 +1,15 @@
~&gt; <span class="sgr-32">curl</span> -sL api.github.com/repos/elves/elvish/issues <span class="sgr-32">|</span>
<span class="sgr-32">all</span> <span class="sgr-1">(</span><span class="sgr-32">from-json</span><span class="sgr-1">)</span> <span class="sgr-32">|</span>
<span class="sgr-32">each</span> <span class="sgr-1">{</span><span class="sgr-32">|</span>x<span class="sgr-32">|</span> <span class="sgr-32">echo</span> <span class="sgr-1">(</span><span class="sgr-32">exact-num</span> <span class="sgr-35">$x</span><span class="sgr-1">[</span>number<span class="sgr-1">])</span>: <span class="sgr-35">$x</span><span class="sgr-1">[</span>title<span class="sgr-1">]</span> <span class="sgr-1">}</span> <span class="sgr-32">|</span>
<span class="sgr-32">head</span> -n 10
1593: A mechanism to trap "interrupts" would be useful
1592: Should `make style` implicitly run the `codespell` t
arget?
1591: Add a &amp;benchmark option to the time command
1590: Correct the documentation for the `try` command
1588: Support comparing booleans
1587: vi append command binding
1586: Add a `&amp;benchmark` option to the `time` command
1585: boolean values are not comparable
1584: Documentation fixups
1583: Implement a `help` command
~&gt; <span class="sgr-32">range</span> 1100 1111 <span class="sgr-32">|</span>
<span class="sgr-32">each</span> <span class="sgr-1">{</span><span class="sgr-32">|</span>x<span class="sgr-32">|</span> <span class="sgr-32">curl</span> -sL xkcd.com/<span class="sgr-35">$x</span>/info.0.json <span class="sgr-1">}</span> <span class="sgr-32">|</span>
<span class="sgr-32">from-json</span> <span class="sgr-32">|</span>
<span class="sgr-32">each</span> <span class="sgr-1">{</span><span class="sgr-32">|</span>x<span class="sgr-32">|</span> <span class="sgr-32">printf</span> <span class="sgr-33">"%g: %s\n"</span> <span class="sgr-35">$x</span><span class="sgr-1">[</span>num<span class="sgr-1">]</span> <span class="sgr-35">$x</span><span class="sgr-1">[</span>title<span class="sgr-1">]</span> <span class="sgr-1">}</span>
1100: Vows
1101: Sketchiness
1102: Fastest-Growing
1103: Nine
1104: Feathers
1105: License Plate
1106: ADD
1107: Sports Cheat Sheet
1108: Cautionary Ghost
1109: Refrigerator
1110: Click and Drag

View File

@ -1,4 +1,4 @@
~&gt; <span class="sgr-32">randint</span> 1 7
▶ (num 6)
~&gt; <span class="sgr-4 sgr-32">randint</span><span class="sgr-4"> 1 7</span> <span class="sgr-7fg sgr-7bg">elf@host</span>
~&gt; <span class="sgr-4 sgr-32">randint</span><span class="sgr-4"> 1 7</span> <span class="sgr-7fg sgr-7bg">elf@host</span>
<span class="sgr-1 sgr-37 sgr-45"> HISTORY #33 </span>

View File

@ -1,5 +1,5 @@
~&gt; <span class="sgr-32">randint</span> 1 7
▶ (num 5)
~&gt; <span class="sgr-36"># more commands ...</span>
~&gt; <span class="sgr-32">ra</span><span class="sgr-4 sgr-32">ndint</span><span class="sgr-4"> 1 7</span> <span class="sgr-7fg sgr-7bg">elf@host</span>
~&gt; <span class="sgr-32">ra</span><span class="sgr-4 sgr-32">ndint</span><span class="sgr-4"> 1 7</span> <span class="sgr-7fg sgr-7bg">elf@host</span>
<span class="sgr-1 sgr-37 sgr-45"> HISTORY #33 </span>

View File

@ -1,5 +1,5 @@
~&gt; <span class="sgr-32">cd</span> elvish
~/elvish&gt; <span class="sgr-32">echo</span> <span class="sgr-4">1.0-release.md </span> <span class="sgr-7fg sgr-7bg">elf@host</span>
~/elvish&gt; <span class="sgr-32">echo</span> <span class="sgr-4">1.0-release.md </span> <span class="sgr-7fg sgr-7bg">elf@host</span>
<span class="sgr-1 sgr-37 sgr-45"> COMPLETING argument </span> .md
<span class="sgr-7fg sgr-7bg">1.0-release.md </span> PACKAGING.md SECURITY.md
CONTRIBUTING.md README.md

View File

@ -1,5 +1,5 @@
~&gt; <span class="sgr-32">cd</span> elvish
~/elvish&gt; <span class="sgr-32">echo</span> <span class="sgr-4">1.0-release.md </span> <span class="sgr-7fg sgr-7bg">elf@host</span>
~/elvish&gt; <span class="sgr-32">echo</span> <span class="sgr-4">1.0-release.md </span> <span class="sgr-7fg sgr-7bg">elf@host</span>
<span class="sgr-1 sgr-37 sgr-45"> COMPLETING argument </span>
<span class="sgr-7fg sgr-7bg">1.0-release.md </span> README.md <span class="sgr-1 sgr-34">syntaxes/</span>
CONTRIBUTING.md SECURITY.md <span class="sgr-1 sgr-34">tools/ </span>

View File

@ -1,15 +1,15 @@
~&gt; <span class="sgr-7fg sgr-7bg">elf@host</span>
<span class="sgr-1 sgr-37 sgr-45"> HISTORY (dedup on) </span>
3 echo "hello\nbye" &gt; /tmp/x <span class="sgr-35"></span>
4 from-lines &lt; /tmp/x <span class="sgr-35"></span>
5 cd /tmp <span class="sgr-7fg sgr-45"> </span>
6 cd ~/elvish <span class="sgr-7fg sgr-45"> </span>
7 git branch <span class="sgr-7fg sgr-45"> </span>
8 git checkout . <span class="sgr-7fg sgr-45"> </span>
9 git commit <span class="sgr-7fg sgr-45"> </span>
19 git status <span class="sgr-7fg sgr-45"> </span>
20 cd /usr/local/bin <span class="sgr-7fg sgr-45"> </span>
21 echo $pwd <span class="sgr-7fg sgr-45"> </span>
22 * (+ 3 4) (- 100 94) <span class="sgr-7fg sgr-45"> </span>
31 make <span class="sgr-7fg sgr-45"> </span>
<span class="sgr-7fg sgr-7bg"> 32 math:min 3 1 30 </span><span class="sgr-7fg sgr-45"> </span>
~&gt; <span class="sgr-7fg sgr-7bg">elf@host</span>
<span class="sgr-1 sgr-37 sgr-45"> HISTORY (dedup on) </span> <span class="sgr-7fg sgr-7bg">Ctrl-D</span> dedup
3 echo "hello\nbye" &gt; /tmp/x <span class="sgr-35"></span>
4 from-lines &lt; /tmp/x <span class="sgr-35"></span>
5 cd /tmp <span class="sgr-7fg sgr-45"> </span>
6 cd ~/elvish <span class="sgr-7fg sgr-45"> </span>
7 git branch <span class="sgr-7fg sgr-45"> </span>
8 git checkout . <span class="sgr-7fg sgr-45"> </span>
9 git commit <span class="sgr-7fg sgr-45"> </span>
19 git status <span class="sgr-7fg sgr-45"> </span>
20 cd /usr/local/bin <span class="sgr-7fg sgr-45"> </span>
21 echo $pwd <span class="sgr-7fg sgr-45"> </span>
22 * (+ 3 4) (- 100 94) <span class="sgr-7fg sgr-45"> </span>
31 make <span class="sgr-7fg sgr-45"> </span>
<span class="sgr-7fg sgr-7bg"> 32 math:min 3 1 30 </span><span class="sgr-7fg sgr-45"> </span>

View File

@ -1,2 +1,2 @@
~&gt; <span class="sgr-32">echo</span><span class="sgr-4"> </span><span class="sgr-1 sgr-4">(</span><span class="sgr-4 sgr-32">styled</span><span class="sgr-4"> warning: red</span><span class="sgr-1 sgr-4">)</span><span class="sgr-4"> bumpy road</span> <span class="sgr-7fg sgr-7bg">elf@host</span>
~&gt; <span class="sgr-32">echo</span><span class="sgr-4"> </span><span class="sgr-1 sgr-4">(</span><span class="sgr-4 sgr-32">styled</span><span class="sgr-4"> warning: red</span><span class="sgr-1 sgr-4">)</span><span class="sgr-4"> bumpy road</span> <span class="sgr-7fg sgr-7bg">elf@host</span>
<span class="sgr-1 sgr-37 sgr-45"> HISTORY #2 </span>

View File

@ -1,2 +1,3 @@
~&gt; <span class="sgr-4 sgr-31">math:min</span><span class="sgr-4"> 3 1 30</span> <span class="sgr-7fg sgr-7bg">elf@host</span>
~&gt; <span class="sgr-4 sgr-31">math:min</span><span class="sgr-4"> 3 1 30</span> <span class="sgr-7fg sgr-7bg">elf@host</span>
<span class="sgr-7fg sgr-7bg">Ctrl-A</span> autofix: use math <span class="sgr-7fg sgr-7bg">Tab</span> <span class="sgr-7fg sgr-7bg">Enter</span> autofix first
<span class="sgr-1 sgr-37 sgr-45"> HISTORY #32 </span>

View File

@ -1,8 +1,8 @@
~&gt; <span class="sgr-32">echo</span> abc def
abc def
~&gt; <span class="sgr-32">vim</span> <span class="sgr-7fg sgr-7bg">elf@host</span>
~&gt; <span class="sgr-32">vim</span> <span class="sgr-7fg sgr-7bg">elf@host</span>
<span class="sgr-1 sgr-37 sgr-45"> LASTCMD </span>
<span class="sgr-7fg sgr-7bg"> echo abc def </span>
<span class="sgr-7fg sgr-7bg"> echo abc def </span>
0 echo
1 abc
2 def

View File

@ -1,6 +1,6 @@
~&gt; <span class="sgr-7fg sgr-7bg">elf@host</span>
~&gt; <span class="sgr-7fg sgr-7bg">elf@host</span>
<span class="sgr-1 sgr-37 sgr-45"> LOCATION </span> local
<span class="sgr-7fg sgr-7bg"> 10 ~/.local/share/elvish </span>
<span class="sgr-7fg sgr-7bg"> 10 ~/.local/share/elvish </span>
9 /usr/local
9 /usr/local/share
9 /usr/local/bin

View File

@ -1,15 +1,15 @@
~&gt; <span class="sgr-7fg sgr-7bg">elf@host</span>
~&gt; <span class="sgr-7fg sgr-7bg">elf@host</span>
<span class="sgr-1 sgr-37 sgr-45"> LOCATION </span>
<span class="sgr-7fg sgr-7bg"> 10 ~/elvish </span><span class="sgr-7fg sgr-45"> </span>
10 ~/.local/share/elvish <span class="sgr-7fg sgr-45"> </span>
10 ~/elvish/website <span class="sgr-7fg sgr-45"> </span>
10 ~/.config/elvish <span class="sgr-7fg sgr-45"> </span>
9 ~/elvish/pkg/edit <span class="sgr-7fg sgr-45"> </span>
9 ~/elvish/pkg/eval <span class="sgr-7fg sgr-45"> </span>
9 /opt <span class="sgr-7fg sgr-45"> </span>
9 /usr/local <span class="sgr-7fg sgr-45"> </span>
9 /usr/local/share <span class="sgr-7fg sgr-45"> </span>
9 /usr/local/bin <span class="sgr-7fg sgr-45"> </span>
9 /usr <span class="sgr-7fg sgr-45"> </span>
9 /tmp <span class="sgr-35"></span>
8 ~/zsh <span class="sgr-35"></span>
<span class="sgr-7fg sgr-7bg"> 10 ~/elvish </span><span class="sgr-7fg sgr-45"> </span>
10 ~/.local/share/elvish <span class="sgr-7fg sgr-45"> </span>
10 ~/elvish/website <span class="sgr-7fg sgr-45"> </span>
10 ~/.config/elvish <span class="sgr-7fg sgr-45"> </span>
9 ~/elvish/pkg/edit <span class="sgr-7fg sgr-45"> </span>
9 ~/elvish/pkg/eval <span class="sgr-7fg sgr-45"> </span>
9 /opt <span class="sgr-7fg sgr-45"> </span>
9 /usr/local <span class="sgr-7fg sgr-45"> </span>
9 /usr/local/share <span class="sgr-7fg sgr-45"> </span>
9 /usr/local/bin <span class="sgr-7fg sgr-45"> </span>
9 /usr <span class="sgr-7fg sgr-45"> </span>
9 /tmp <span class="sgr-35"></span>
8 ~/zsh <span class="sgr-35"></span>

View File

@ -1,15 +1,15 @@
~/elvish&gt; <span class="sgr-7fg sgr-7bg">elf@host</span>
<span class="sgr-1 sgr-37 sgr-45"> NAVIGATING </span>
<span class="sgr-1 sgr-34"> bash </span> <span class="sgr-7fg sgr-7bg"> 1.0-release.md </span><span class="sgr-7fg sgr-45"> </span> 1.0 has not been released yet.
<span class="sgr-7fg sgr-1 sgr-44"> elvish </span> CONTRIBUTING.md <span class="sgr-7fg sgr-45"> </span>
<span class="sgr-1 sgr-34"> zsh </span> Dockerfile <span class="sgr-7fg sgr-45"> </span>
LICENSE <span class="sgr-7fg sgr-45"> </span>
Makefile <span class="sgr-7fg sgr-45"> </span>
PACKAGING.md <span class="sgr-7fg sgr-45"> </span>
README.md <span class="sgr-7fg sgr-45"> </span>
SECURITY.md <span class="sgr-7fg sgr-45"> </span>
<span class="sgr-1 sgr-34"> cmd </span><span class="sgr-7fg sgr-45"> </span>
go.mod <span class="sgr-7fg sgr-45"> </span>
go.sum <span class="sgr-7fg sgr-45"> </span>
<span class="sgr-1 sgr-34"> pkg </span><span class="sgr-35"></span>
<span class="sgr-1 sgr-34"> syntaxes </span><span class="sgr-35"></span>
~/elvish&gt; <span class="sgr-7fg sgr-7bg">elf@host</span>
<span class="sgr-1 sgr-37 sgr-45"> NAVIGATING </span> <span class="sgr-7fg sgr-7bg">Ctrl-H</span> hidden <span class="sgr-7fg sgr-7bg">Ctrl-F</span> filter
<span class="sgr-1 sgr-34"> bash </span> <span class="sgr-7fg sgr-7bg"> 1.0-release.m </span><span class="sgr-7fg sgr-45"> </span> 1.0 has not been released y
<span class="sgr-7fg sgr-1 sgr-44"> elvis </span> CONTRIBUTING. <span class="sgr-7fg sgr-45"> </span>
<span class="sgr-1 sgr-34"> zsh </span> Dockerfile <span class="sgr-7fg sgr-45"> </span>
LICENSE <span class="sgr-7fg sgr-45"> </span>
Makefile <span class="sgr-7fg sgr-45"> </span>
PACKAGING.md <span class="sgr-7fg sgr-45"> </span>
README.md <span class="sgr-7fg sgr-45"> </span>
SECURITY.md <span class="sgr-7fg sgr-45"> </span>
<span class="sgr-1 sgr-34"> cmd </span><span class="sgr-7fg sgr-45"> </span>
go.mod <span class="sgr-7fg sgr-45"> </span>
go.sum <span class="sgr-7fg sgr-45"> </span>
<span class="sgr-1 sgr-34"> pkg </span><span class="sgr-35"></span>
<span class="sgr-1 sgr-34"> syntaxes </span><span class="sgr-35"></span>

View File

@ -4,4 +4,4 @@
<span class="sgr-32">tilde-abbr</span> <span class="sgr-35">$pwd</span>
<span class="sgr-32">styled</span> <span class="sgr-33">'❱ '</span> bright-red
<span class="sgr-1">}</span>
~<span class="sgr-91"></span><span class="sgr-36"># Fancy unicode prompts!</span> <span class="sgr-7fg sgr-7bg">elf✸host.example.com</span>
~<span class="sgr-91"></span><span class="sgr-36"># Fancy unicode prompts!</span> <span class="sgr-7fg sgr-7bg">elf✸host.example.com</span>

View File

@ -1,2 +1,2 @@
~&gt; <span class="sgr-31">str:</span> <span class="sgr-7fg sgr-7bg">elf@host</span>
~&gt; <span class="sgr-31">str:</span> <span class="sgr-7fg sgr-7bg">elf@host</span>
<span class="sgr-7fg sgr-7bg">Ctrl-A</span> autofix: use str <span class="sgr-7fg sgr-7bg">Tab</span> <span class="sgr-7fg sgr-7bg">Enter</span> autofix first

View File

@ -1,4 +1,4 @@
~/elvish&gt; <span class="sgr-32">vim</span> <span class="sgr-4">1.0-release.md </span> <span class="sgr-7fg sgr-7bg">elf@host</span>
~/elvish&gt; <span class="sgr-32">vim</span> <span class="sgr-4">1.0-release.md </span> <span class="sgr-7fg sgr-7bg">elf@host</span>
<span class="sgr-1 sgr-37 sgr-45"> COMPLETING argument </span>
<span class="sgr-7fg sgr-7bg">1.0-release.md </span> README.md <span class="sgr-1 sgr-34">syntaxes/</span>
CONTRIBUTING.md SECURITY.md <span class="sgr-1 sgr-34">tools/ </span>

View File

@ -1,4 +1,3 @@
The latest version of documents in this section are also available as a
[docset](https://elv.sh/ref/docset/Elvish.tgz). You can also
[subscribe](dash-feed://https%3A%2F%2Felv.sh%2Fref%2Fdocset%2FElvish.xml) to the
feed.
Reference documents also available as a docset. Download the
[latest build](https://elv.sh/ref/docset/Elvish.tgz) or subscribe to the
[feed](dash-feed://https%3A%2F%2Felv.sh%2Fref%2Fdocset%2FElvish.xml).

View File

@ -10,7 +10,7 @@ html {
body {
font-family: "Source Serif", Georgia, serif;
font-size: 17px;
line-height: 1.4em;
line-height: 1.4;
}
body.has-js .no-js, body.no-js .has-js {
@ -29,7 +29,7 @@ a {
/**
* Top-level elements.
*
* There are two main elements: #navbar and #content. Both have a maximum
* There are two main elements: #navbar and .content. Both have a maximum
* width, and is centered when the viewport is wider than that.
*
* #navbar is wrapped by #navbar-container, a black stripe that always span
@ -40,13 +40,29 @@ a {
width: 100%;
color: white;
background-color: #1a1a1a;
padding: 12px 0;
padding: 7px 0;
}
#content, #navbar {
max-width: 1024px;
.content, #navbar {
max-width: 800px;
margin: 0 auto;
padding: 0 4%;
padding: 0 16px;
}
/*
832px = max-width + left and right padding of .content.
After this screen width, .content will no longer get wider, but we allow
.extra-wide elements to continue to get wider up to 900px, using negative
left and right margins.
*/
@media screen and (min-width: 832px) {
.extra-wide {
/* 32px is left and right padding of .content. */
width: calc(min(100vw - 32px, 900px));
/* upper bound is calculated by substituting 100vw = 900px + 32px */
margin-inline: calc(max((832px - 100vw) / 2, -50px));
}
}
/**
@ -59,13 +75,12 @@ a {
#site-title, #nav-list {
display: inline-block;
/* Add spacing between lines when the navbar cannot fit in one line. */
line-height: 1.4em;
line-height: 1.4;
}
#site-title {
font-size: 1.2em;
margin-right: 0.6em;
/* Move the title upward 1px so that it looks more aligned with the
/* Move the title downward 1px so that it looks more aligned with the
* category list. */
position: relative;
top: 1px;
@ -75,17 +90,23 @@ a {
color: #5b5;
}
#nav-list {
margin-left: 0.8em;
}
.nav-item {
list-style: none;
display: inline-block;
margin-left: 0.2em;
}
.nav-link {
color: white;
border-radius: 3px;
}
.nav-link > code {
padding: 0px 0.5em;
padding: 0px 0.4em;
}
.nav-link:hover {
@ -97,10 +118,6 @@ a {
background-color: white;
}
.nav-item + .nav-item::before {
content: "|";
}
/**
* Article header.
**/
@ -111,12 +128,12 @@ a {
.article-title {
padding: 16px 0;
border-bottom: solid 1px #667;
border-bottom: darkred solid 2px;
}
/* Extra level needed to be more specific than .article h1 */
.article .article-title h1 {
font-size: 1.5em;
/* Override .content h1 */
.content .article-title h1 {
font-size: 1.4em;
margin: 0;
padding: 0;
border: none;
@ -130,30 +147,26 @@ a {
padding-top: 32px;
}
.article p, .article ul, .article pre {
margin-bottom: 16px;
.content p, .content ul, .content pre {
margin-bottom: 0.7em;
}
.article li {
.content li {
margin: 0.5em 0;
}
.article li > p {
margin: 1em 0;
}
/* Block code. */
.article pre {
.content pre {
padding: 1em;
overflow: auto;
}
/* Inline code. */
.article p code {
.content p code {
padding: 0.1em 0;
}
.article p code::before, .article p code::after {
.content p code::before, .content p code::after {
letter-spacing: -0.2em;
content: "\00a0";
}
@ -162,116 +175,129 @@ code, pre {
font-family: "Fira Mono", Menlo, "Roboto Mono", Consolas, monospace;
}
.article code, .article pre {
.content code, .content pre {
background-color: #f0f0f0;
border-radius: 3px;
}
/* This doesn't have p, so that it also applies to ttyshots. */
.article code {
.content code {
font-size: 85%;
}
/* We only use h1 to h3. */
.article h1, .article h2, .article h3 {
.content h1, .content h2, .content h3 {
line-height: 1.25;
}
.article h1, .article h2, .article h3 {
.content h1, .content h2, .content h3 {
margin-top: 24px;
margin-bottom: 20px;
font-weight: bold;
}
.article h1 {
.content h1 {
font-size: 1.3em;
padding-bottom: 0.4em;
border-bottom: 1px solid #aaa;
}
.article h2 {
.content h1 {
border-left: darkred solid 0.3em;
padding-left: 0.3em;
margin-left: -0.6em;
}
.content h2 {
font-size: 1.2em;
}
.article h3 {
font-style: italic;
}
.article ul, .article ol {
margin-left: 1em;
.content ul, .content ol {
margin-left: 1.5em;
}
/**
* Table of content.
*/
#pandoc-toc-wrapper {
#toc-wrapper {
background-color: #f0f0f0;
padding: 1em;
margin: 0 16px 16px 0;
margin: 0 0 16px 0;
border-radius: 6px;
line-height: 1;
}
/* The first <h1> clears the TOC */
.article-content h1 {
clear: both;
#toc-header {
padding: 1em 1em 0.6em 1em;
border-bottom: solid white 2px;
cursor: pointer;
}
#pandoc-toc {
#toc-status {
display: inline-block;
width: 0;
height: 0;
margin-right: 2px;
border: 6px solid transparent;
position: relative;
}
#toc-status:not(.closed) {
border-top: 6px solid black;
top: 3px;
}
#toc-status.closed {
border-left: 6px solid black;
left: 3px;
}
#toc {
margin-left: -0.6em;
padding: 1em;
}
@media (min-width: 600px) and (max-width: 899px) {
#pandoc-toc {
@media screen and (min-width: 600px) {
#toc {
column-count: 2;
}
}
@media (min-width: 900px) {
#pandoc-toc {
column-count: 3;
}
/* Override value from .content ul, which is too big for the ToC */
#toc ul {
margin-left: 1em;
}
#pandoc-toc li {
#toc li {
list-style: none;
/* Keep first-level ToC within one column */
break-inside: avoid;
}
/*
When the ToC can have two columns, the first item of the second column will not
the intended top margin (this is how columns work in CSS). Work around this by
adding some extra top padding in #toc, and removing the top margin of the very
first <li> element to match.
*/
#toc > ul:first-child > li:first-child {
margin-top: 0;
}
/**
* Category content.
**/
#content.category {
padding-top: 16px;
}
.category-prelude, .group-intro, .article-list {
margin-top: 16px;
}
.article-list > li {
list-style: square inside;
padding: 3px;
.content.category {
padding-top: 32px;
}
.article-list > li:hover {
background-color: #c0c0c0;
}
.article-link, .article-link:visited {
color: black;
display: inline;
line-height: 1.4em;
border-bottom: 1px solid black;
background-color: #d0d0d0;
}
.article-timestamp {
float: right;
display: inline-block;
display: block;
margin-left: 1em;
}
@ -319,11 +345,6 @@ kbd {
font-family: "Lucida Grande", Arial, sans-serif;
}
/** Section numbers generated by pandoc */
.header-section-number:after, .toc-section-number:after {
content: ".";
}
/**
* TTY shots.
*/
@ -456,11 +477,11 @@ pre.ttyshot, pre.ttyshot code {
background-color: #333;
}
.dark .article code, .dark .article pre {
.dark .content code, .dark .content pre {
background-color: #181818;
}
.dark #pandoc-toc-wrapper {
.dark #toc-wrapper {
background-color: #181818;
}

View File

@ -162,8 +162,8 @@
</html>
{{ define "article-content" }}
<div id="content">
<article class="article">
<div class="content">
<article>
{{ if not .IsHomepage }}
<div class="article-title">
<div class="timestamp"> {{ .Timestamp }} </div>
@ -181,15 +181,15 @@
{{ define "category-content" }}
{{ $category := .Category }}
<div id="content" class="category">
<div class="content category">
{{ if .Prelude }}
<article class="category-prelude article">
<article>
{{ .Prelude }}
</article>
{{ end }}
{{ range $group := .Groups }}
{{ if $group.Intro }}
<div class="group-intro">{{ $group.Intro }}</div>
<p>{{ $group.Intro }}</p>
{{ end }}
<ul class="article-list">
{{ range $article := $group.Articles }}
@ -202,7 +202,6 @@
<span class="article-timestamp">
{{ $article.Timestamp }}
</span>
<div class="clear"></div>
</li>
{{ end }}
</ul>