about summary refs log tree commit diff stats
path: root/mu.vim
blob: d62b792f7b638bee266f45a46143813916f44af8 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
" Vim syntax file
" Language:    mu
" Maintainer:  Kartik Agaram <mu@akkartik.com>
" URL:         http://github.com/akkartik/mu
" License:     public domain
"
" Copy this into your ftplugin directory, and add the following to your vimrc
" or to .vim/ftdetect/mu.vim:
"   autocmd BufReadPost,BufNewFile *.mu set filetype=mu

let s:save_cpo = &cpo
set cpo&vim

" todo: why does this periodically lose syntax, like on file reload?
"   $ vim x.mu
"   :e
"? if exists("b:syntax")
"?   finish
"? endif
"? let b:syntax = "mu"

setlocal iskeyword=@,48-57,?,!,_,$,-
setlocal formatoptions-=t  " Mu programs have long lines
setlocal formatoptions+=c  " but comments should still wrap

syntax match muComment /#.*$/  | highlight link muComment Comment
syntax match muSalientComment /##.*$/  | highlight link muSalientComment SalientComment
syntax match muComment /;.*$/  | highlight link muComment Comment
syntax match muSalientComment /;;.*$/  | highlight link muSalientComment SalientComment
set comments+=n:#
syntax match muCommentedCode "#? .*"  | highlight link muCommentedCode CommentedCode
let b:cmt_head = "#? "

syntax match muDelimiter "[{}]"  | highlight link muDelimiter Delimiter

" Mu strings are inside [ ... ] and can span multiple lines
" don't match '[' at end of line, that's usually code
syntax match muLiteral %^[^ a-zA-Z0-9(){}\[\]#$_*@&,=-][^ ,]*\|[ ,]\@<=[^ a-zA-Z0-9(){}\[\]#$_*@&,=-][^ ,]*%
syntax region muString start=+\[[^\]]+ end=+\]+
syntax match muString "\[\]"
highlight link muString String
" Mu syntax for representing the screen in scenarios
syntax region muScreen start=+ \.+ end=+\.$\|$+
highlight link muScreen muString

" Mu literals
syntax match muLiteral %[^ ]\+:literal/[^ ,]*\|[^ ]\+:literal\>%
syntax match muLiteral %\<[0-9-]\?[0-9]\+/[^ ,]*%
syntax match muLiteral % [0-9-]\?[0-9]\+[, ]\@=\| [0-9-]\?[0-9]\+$%
syntax match muLiteral "^\s\+[^ 0-9a-zA-Z{}$#\[\]][^ ]*\s*$"
" labels
syntax match muLiteral %[^ ]\+:label/[^ ,]*\|[^ ]\+:label\>%
" other literal types
syntax match muLiteral %[^ ]\+:type/[^ ,]*\|[^ ]\+:type\>%
syntax match muLiteral %[^ ]\+:offset/[^ ,]*\|[^ ]\+:offset\>%
syntax match muLiteral %[^ ]\+:variant/[^ ,]*\|[^ ]\+:variant\>%
syntax match muLiteral % true\(\/[^ ]*\)\?\| false\(\/[^ ]*\)\?%  " literals will never be the first word in an instruction
syntax match muLiteral % null\(\/[^ ]*\)\?%
highlight link muLiteral Constant

" sources of action at a distance
syntax match muAssign "<-"
syntax match muAssign "\<raw\>"
highlight link muAssign SpecialChar
syntax match muGlobal %[^ ]\+:global/\?[^ ,]*%  | highlight link muGlobal SpecialChar

" common keywords
" use regular expressions for common words that may come after '/'
syntax keyword muKeyword default-space local-scope
syntax keyword muKeyword next-input rewind-inputs load-inputs
syntax keyword muKeyword next-ingredient rewind-ingredients load-ingredients
syntax match muKeyword " input\>\| ingredient\>"
highlight link muKeyword Constant

syntax keyword muControl return return-if return-unless
syntax keyword muControl reply reply-if reply-unless
syntax keyword muControl output-if output-unless
syntax match muControl "^return\>\| return\>\|^reply\>\| reply\>\|^output\|^ output\>"
syntax keyword muControl jump-if jump-unless
syntax keyword muControl break-if break-unless
syntax keyword muControl loop-if loop-unless
syntax match muControl "^jump\>\| jump\>\|^break\>\| break\>\|^loop\>\| loop\>"
syntax keyword muControl start-running
syntax keyword muControl call-with-continuation-mark return-continuation-until-mark
highlight muControl ctermfg=3

syntax match muRecipe "->"
syntax match muRecipe "^recipe\>\|^def\>\|^before\>\|^after\>\| -> "
syntax keyword muRecipe recipe! def! function fn
highlight muRecipe ctermfg=208

syntax match muScenario "^scenario\>"  | highlight muScenario ctermfg=34
syntax keyword muPendingScenario pending-scenario  | highlight link muPendingScenario SpecialChar
syntax match muData "^type\>\|^container\>"
syntax keyword muData exclusive-container
highlight muData ctermfg=226

let &cpo = s:save_cpo
b">string test*: string Mailcap* = seq[MailcapEntry] proc has(state: MailcapParser): bool {.inline.} = return not state.stream.atEnd proc consume(state: var MailcapParser): char = if state.hasbuf: state.hasbuf = false return state.buf var c = state.stream.readChar() if c == '\\' and not state.stream.atEnd: let c2 = state.stream.readChar() if c2 == '\n' and not state.stream.atEnd: inc state.line c = state.stream.readChar() if c == '\n': inc state.line return c proc reconsume(state: var MailcapParser; c: char) = state.buf = c state.hasbuf = true proc skipBlanks(state: var MailcapParser; c: var char): bool = while state.has(): c = state.consume() if c notin AsciiWhitespace - {'\n'}: return true proc skipBlanks(state: var MailcapParser) = var c: char if state.skipBlanks(c): state.reconsume(c) proc skipLine(state: var MailcapParser) = while state.has(): let c = state.consume() if c == '\n': break proc consumeTypeField(state: var MailcapParser): Result[string, string] = var s = "" # type while state.has(): let c = state.consume() if c == '/': s &= c break if c notin AsciiAlphaNumeric + {'-', '*'}: return err("line " & $state.line & ": invalid character in type field: " & c) s &= c.toLowerAscii() if not state.has(): return err("Missing subtype") # subtype while state.has(): let c = state.consume() if c in AsciiWhitespace + {';'}: state.reconsume(c) break if c notin AsciiAlphaNumeric + {'-', '.', '*', '_', '+'}: return err("line " & $state.line & ": invalid character in subtype field: " & c) s &= c.toLowerAscii() var c: char if not state.skipBlanks(c) or c != ';': return err("Semicolon not found") return ok(s) proc consumeCommand(state: var MailcapParser): Result[string, string] = state.skipBlanks() var quoted = false var s = "" while state.has(): let c = state.consume() if not quoted: if c == '\r': continue if c == ';' or c == '\n': state.reconsume(c) return ok(s) if c == '\\': quoted = true continue if c notin Ascii - Controls: return err("line " & $state.line & ": invalid character in command: " & c) else: quoted = false s &= c return ok(s) type NamedField = enum NO_NAMED_FIELD, NAMED_FIELD_TEST, NAMED_FIELD_NAMETEMPLATE, NAMED_FIELD_EDIT proc parseFieldKey(entry: var MailcapEntry; k: string): NamedField = case k of "needsterminal": entry.flags.incl(NEEDSTERMINAL) of "copiousoutput": entry.flags.incl(COPIOUSOUTPUT) of "x-htmloutput": entry.flags.incl(HTMLOUTPUT) of "x-ansioutput": entry.flags.incl(ANSIOUTPUT) of "test": return NAMED_FIELD_TEST of "nametemplate": return NAMED_FIELD_NAMETEMPLATE of "edit": return NAMED_FIELD_EDIT return NO_NAMED_FIELD proc consumeField(state: var MailcapParser; entry: var MailcapEntry): Result[bool, string] = state.skipBlanks() if not state.has(): return ok(false) var buf = "" while state.has(): let c = state.consume() case c of ';', '\n': if parseFieldKey(entry, buf) != NO_NAMED_FIELD: return err("Expected command") return ok(c == ';') of '\r': continue of '=': let f = parseFieldKey(entry, buf) let cmd = ?state.consumeCommand() case f of NO_NAMED_FIELD: discard of NAMED_FIELD_TEST: entry.test = cmd of NAMED_FIELD_NAMETEMPLATE: entry.nametemplate = cmd of NAMED_FIELD_EDIT: entry.edit = cmd return ok(state.consume() == ';') else: if c in Controls: return err("line " & $state.line & ": invalid character in field: " & c) buf &= c proc parseMailcap*(stream: Stream): Result[Mailcap, string] = var state = MailcapParser(stream: stream, line: 1) var mailcap: Mailcap while not stream.atEnd(): let c = state.consume() if c == '#': state.skipLine() continue state.reconsume(c) state.skipBlanks() let c2 = state.consume() if c2 == '\n' or c2 == '\r': continue state.reconsume(c2) let t = ?state.consumeTypeField() let mt = t.until('/') #TODO this could be more efficient let subt = t[mt.len + 1 .. ^1] var entry = MailcapEntry( mt: mt, subt: subt, cmd: ?state.consumeCommand() ) if state.consume() == ';': while ?state.consumeField(entry): discard mailcap.add(entry) return ok(mailcap) # Mostly based on w3m's mailcap quote/unquote type UnquoteState = enum usNormal, usQuoted, usPerc, usAttr, usAttrQuoted, usDollar type UnquoteResult* = object canpipe*: bool cmd*: string type QuoteState* = enum qsNormal, qsDoubleQuoted, qsSingleQuoted proc quoteFile*(file: string; qs: QuoteState): string = var s = "" for c in file: case c of '$', '`', '"', '\\': if qs != qsSingleQuoted: s &= '\\' of '\'': if qs == qsSingleQuoted: s &= "'\\'" # then re-open the quote by appending c elif qs == qsNormal: s &= '\\' # double-quoted: append normally of AsciiAlphaNumeric, '_', '.', ':', '/': discard # no need to quote elif qs == qsNormal: s &= '\\' s &= c return s proc unquoteCommand*(ecmd, contentType, outpath: string; url: URL; canpipe: var bool; line = -1): string = var cmd = "" var attrname = "" var state: UnquoteState var qss = @[qsNormal] # quote state stack. len >1 template qs: var QuoteState = qss[^1] for c in ecmd: case state of usQuoted: cmd &= c state = usNormal of usAttrQuoted: attrname &= c.toLowerAscii() state = usAttr of usNormal, usDollar: let prev_dollar = state == usDollar state = usNormal case c of '%': state = usPerc of '\\': state = usQuoted of '\'': if qs == qsSingleQuoted: qs = qsNormal else: qs = qsSingleQuoted cmd &= c of '"': if qs == qsDoubleQuoted: qs = qsNormal else: qs = qsDoubleQuoted cmd &= c of '$': if qs != qsSingleQuoted: state = usDollar cmd &= c of '(': if prev_dollar: qss.add(qsNormal) cmd &= c of ')': if qs != qsSingleQuoted: if qss.len > 1: qss.setLen(qss.len - 1) else: # mismatched parens; probably an invalid shell command... qss[0] = qsNormal cmd &= c else: cmd &= c of usPerc: case c of '%': cmd &= c of 's': cmd &= quoteFile(outpath, qs) canpipe = false of 't': cmd &= quoteFile(contentType.until(';'), qs) of 'u': # Netscape extension if url != nil: # nil in getEditorCommand cmd &= quoteFile($url, qs) of 'd': # line; not used in mailcap, only in getEditorCommand if line != -1: # -1 in mailcap cmd &= $line of '{': state = usAttr continue else: discard state = usNormal of usAttr: if c == '}': let s = contentType.getContentTypeAttr(attrname) cmd &= quoteFile(s, qs) attrname = "" state = usNormal elif c == '\\': state = usAttrQuoted else: attrname &= c return cmd proc unquoteCommand*(ecmd, contentType, outpath: string; url: URL): string = var canpipe: bool return unquoteCommand(ecmd, contentType, outpath, url, canpipe) proc getMailcapEntry*(mailcap: Mailcap; contentType, outpath: string; url: URL): ptr MailcapEntry = let mt = contentType.until('/') if mt.len + 1 >= contentType.len: return nil let st = contentType.until(AsciiWhitespace + {';'}, mt.len + 1) for entry in mailcap: if not (entry.mt.len == 1 and entry.mt[0] == '*') and entry.mt != mt: continue if not (entry.subt.len == 1 and entry.subt[0] == '*') and entry.subt != st: continue if entry.test != "": var canpipe = true let cmd = unquoteCommand(entry.test, contentType, outpath, url, canpipe) if not canpipe: continue if execCmd(cmd) != 0: continue return unsafeAddr entry