Merge branch 'type_compiler'

This commit is contained in:
Oliver Blanthorn 2018-11-05 13:24:35 +00:00
commit 7f034a161c
No known key found for this signature in database
GPG key ID: 2BB8C36BB504BFF3
31 changed files with 730 additions and 329 deletions

3
.gitignore vendored
View file

@ -13,5 +13,6 @@ native/native_main
native_main.spec
.wine-pyinstaller
tags
compiler/gen_metadata.js
compiler/*.js
compiler/**/*.js
src/.metadata.generated.ts

View file

@ -1,95 +1,83 @@
import * as ts from "typescript"
import * as fs from "fs"
import * as commandLineArgs from "command-line-args"
import * as AllTypes from "./types/AllTypes"
import * as AllMetadata from "./metadata/AllMetadata"
class SimpleType {
name: string
kind: string
// Support for generics is mostly done but it might not be needed for now
// generics: Array<SimpleType>
arguments: Array<SimpleType>
type: SimpleType
constructor(typeNode) {
switch (typeNode.kind) {
case ts.SyntaxKind.VoidKeyword:
this.kind = "void"
break
case ts.SyntaxKind.AnyKeyword:
this.kind = "any"
break
case ts.SyntaxKind.BooleanKeyword:
this.kind = "boolean"
break
case ts.SyntaxKind.NumberKeyword:
this.kind = "number"
break
case ts.SyntaxKind.ObjectKeyword:
this.kind = "object"
break
case ts.SyntaxKind.StringKeyword:
this.kind = "string"
break
case ts.SyntaxKind.Parameter:
// 149 is "Parameter". We don't care about that so let's
// convert its type into a SimpleType and grab what we need from it
let ttype = new SimpleType(typeNode.type)
this.kind = ttype.kind
// this.generics = ttype.generics
this.arguments = ttype.arguments
this.type = ttype.type
this.name = typeNode.name.original.escapedText
break
case ts.SyntaxKind.TypeReference:
// 162 is "TypeReference". Not sure what the rules are here but it seems to be used for generics
this.kind = typeNode.typeName.escapedText
if (typeNode.typeArguments) {
this.arguments = typeNode.typeArguments.map(
t => new SimpleType(typeNode.typeArguments[0]),
)
export function toSimpleType(typeNode) {
switch (typeNode.kind) {
case ts.SyntaxKind.VoidKeyword:
return new AllTypes.VoidType()
case ts.SyntaxKind.AnyKeyword:
return new AllTypes.AnyType()
case ts.SyntaxKind.BooleanKeyword:
return new AllTypes.BooleanType()
case ts.SyntaxKind.NumberKeyword:
return new AllTypes.NumberType()
case ts.SyntaxKind.ObjectKeyword:
return new AllTypes.ObjectType()
case ts.SyntaxKind.StringKeyword:
return new AllTypes.StringType()
case ts.SyntaxKind.Parameter:
let n = toSimpleType(typeNode.type)
n.name = typeNode.name.original.escapedText
return n
case ts.SyntaxKind.TypeReference:
if (!typeNode.typeArguments) {
// If there are no typeArguments, this is not a parametric type and we can return the type directly
try {
return toSimpleType(typeNode.typeName.symbol.declarations[0].type)
} catch (e) {
// Fall back to what you'd do with typeArguments
}
break
case ts.SyntaxKind.FunctionType:
this.kind = "function"
// Probably don't need generics for now
// this.generics = (typeNode.typeParameters || []).map(p => new SimpleType(p))
this.arguments = typeNode.parameters.map(p => new SimpleType(p))
this.type = new SimpleType(typeNode.type)
break
case ts.SyntaxKind.TypeLiteral:
// This is a type literal. i.e., something like this: { [str: string]: string[] }
// Very complicated and perhaps not useful to know about. Let's just say "object" for now
this.kind = "object"
break
case ts.SyntaxKind.ArrayType:
this.kind = "array"
this.type = new SimpleType(typeNode.elementType)
break
case ts.SyntaxKind.TupleType:
this.kind = "tuple"
this.arguments = typeNode.elementTypes.map(
t => new SimpleType(t),
)
break
case ts.SyntaxKind.UnionType:
this.kind = "union"
this.arguments = typeNode.types.map(t => new SimpleType(t))
break
case ts.SyntaxKind.LiteralType:
// "LiteralType". I'm not sure what this is. Probably things like type a = "b" | "c"
this.kind = "LiteralType"
this.name = typeNode.literal.text
break
default:
console.log(typeNode)
throw new Error(
`Unhandled kind (${typeNode.kind}) for ${typeNode}`,
)
}
}
let args = typeNode.typeArguments
? typeNode.typeArguments.map(t =>
toSimpleType(typeNode.typeArguments[0]),
)
: []
return new AllTypes.TypeReferenceType(
typeNode.typeName.escapedText,
args,
)
case ts.SyntaxKind.FunctionType:
// generics = (typeNode.typeParameters || []).map(p => new AllTypes.SimpleType(p))
return new AllTypes.FunctionType(
typeNode.parameters.map(p => toSimpleType(p)),
toSimpleType(typeNode.type),
)
case ts.SyntaxKind.TypeLiteral:
let members = typeNode.members.map(member => {
if (member.kind == ts.SyntaxKind.IndexSignature) {
// Something like this: { [str: string]: string[] }
return ["", toSimpleType(member.type)]
}
// Very fun feature: when you have an object literal with >20 members, typescript will decide to replace some of them with a "... X more ..." node that obviously doesn't have a corresponding symbol, hence this check and the filter after the map
if (member.name.symbol)
return [member.name.symbol.escapedName, toSimpleType(member.type)];
}).filter(m => m)
return new AllTypes.ObjectType(new Map(members));
case ts.SyntaxKind.ArrayType:
return new AllTypes.ArrayType(toSimpleType(typeNode.elementType))
case ts.SyntaxKind.TupleType:
return new AllTypes.TupleType(
typeNode.elementTypes.map(t => toSimpleType(t)),
)
case ts.SyntaxKind.UnionType:
return new AllTypes.UnionType(
typeNode.types.map(t => toSimpleType(t)),
)
break
case ts.SyntaxKind.LiteralType:
return new AllTypes.LiteralTypeType(typeNode.literal.text)
break
default:
console.log(typeNode)
throw new Error(`Unhandled kind (${typeNode.kind}) for ${typeNode}`)
}
}
/** True if this is visible outside this file, false otherwise */
/** True if node is visible outside its file, false otherwise */
function isNodeExported(node: ts.Node): boolean {
return (
(ts.getCombinedModifierFlags(<ts.Declaration>node) &
@ -99,57 +87,64 @@ function isNodeExported(node: ts.Node): boolean {
)
}
function visit(checker: any, filename: string, node: any, everything: any) {
function visit(checker: any, file: AllMetadata.FileMetadata, node: any) {
let symbol = checker.getSymbolAtLocation(node.name)
if (symbol && isNodeExported(node)) {
// ensure() is very simple, it just creates a key named `name` the value of which is `def` if `name` doesn't exist in `obj`
let ensure = (obj, name, def) => {
obj[name] = obj[name] || def
return obj[name]
}
// addDoc creates a "doc" key set to an empty array in `obj` if it doesn't exist and then adds documentation from the symbol to it if it isn't already in the array
let addDoc = (obj, symbol) => {
let doc = ensure(obj, "doc", [])
let docstr = ts.displayPartsToString(
symbol.getDocumentationComment(),
)
if (docstr && !doc.includes(docstr)) doc.push(docstr)
}
// addType sets the `type` attribute of `obj` to the SimpleType of `symbol` if it has one
let addType = (obj, symbol) => {
let ttype = checker.getTypeOfSymbolAtLocation(
symbol,
symbol.valueDeclaration!,
)
if (ttype) {
obj["type"] = new SimpleType(checker.typeToTypeNode(ttype))
}
}
let nodeName = symbol.escapedName
let file = ensure(everything, filename, {})
switch (node.kind) {
case ts.SyntaxKind.FunctionDeclaration:
let functions = ensure(file, "functions", {})
let func = ensure(functions, nodeName, {})
addDoc(func, symbol)
addType(func, symbol)
break
// Grab the doc, default to empty string
let doc =
ts.displayPartsToString(symbol.getDocumentationComment()) ||
""
// Grab the type
let ttype = checker.getTypeOfSymbolAtLocation(
symbol,
symbol.valueDeclaration!,
)
// If the function has a type, try to convert it, if it doesn't, default to any
let t = ttype
? toSimpleType(checker.typeToTypeNode(ttype))
: new AllTypes.AnyType()
file.setFunction(
nodeName,
new AllMetadata.SymbolMetadata(doc, t),
)
return
case ts.SyntaxKind.ClassDeclaration:
let classes = ensure(file, "classes", {})
let clazz = ensure(classes, nodeName, {})
let clazz = file.getClass(nodeName)
if (!clazz) {
clazz = new AllMetadata.ClassMetadata()
file.setClass(nodeName, clazz)
}
symbol.members.forEach((sym, name, map) => {
// Can't get doc/type from these special functions
// Or at least, it requires work that might not be needed for now
if (["__constructor", "get", "set"].includes(name)) return
let member = ensure(clazz, name, {})
addDoc(member, sym)
addType(member, sym)
// Grab the doc, default to empty string
let doc =
ts.displayPartsToString(
sym.getDocumentationComment(),
) || ""
// Grab the type
let ttype = checker.getTypeOfSymbolAtLocation(
sym,
sym.valueDeclaration!,
)
// If the function has a type, try to convert it, if it doesn't, default to any
let t = ttype
? toSimpleType(checker.typeToTypeNode(ttype))
: new AllTypes.AnyType()
clazz.setMember(
name,
new AllMetadata.SymbolMetadata(doc, t),
)
})
break
return
// Other declaration syntaxkinds:
// case ts.SyntaxKind.VariableDeclaration:
// case ts.SyntaxKind.VariableDeclarationList:
@ -171,7 +166,7 @@ function visit(checker: any, filename: string, node: any, everything: any) {
}
}
ts.forEachChild(node, node => visit(checker, filename, node, everything))
ts.forEachChild(node, node => visit(checker, file, node))
}
function generateMetadata(
@ -185,18 +180,33 @@ function generateMetadata(
module: ts.ModuleKind.CommonJS,
})
let everything = {}
let metadata = new AllMetadata.ProgramMetadata()
for (const sourceFile of program.getSourceFiles()) {
let n = (fileNames as any).find(name => sourceFile.fileName.match(name))
if (n) visit(program.getTypeChecker(), n, sourceFile, everything)
let name = (fileNames as any).find(name =>
sourceFile.fileName.match(name),
)
if (name) {
let file = metadata.getFile(name)
if (!file) {
file = new AllMetadata.FileMetadata()
metadata.setFile(name, file)
}
visit(program.getTypeChecker(), file, sourceFile)
}
}
let metadataString = `\nexport let everything = ${JSON.stringify(
everything,
undefined,
4,
)}\n`
// We need to specify Type itself because it won't exist in AllTypes.js since it's an interface
let imports = `import { Type } from "../compiler/types/AllTypes"\n` +
`import {${Object.keys(AllTypes).join(
", ",
)}} from "../compiler/types/AllTypes"\n` +
`import {${Object.keys(AllMetadata).join(
", ",
)}} from "../compiler/metadata/AllMetadata"\n`
let metadataString =
imports + `\nexport let everything = ${metadata.toConstructor()}\n`
if (themedir) {
metadataString += `\nexport let staticThemes = ${JSON.stringify(

View file

@ -0,0 +1,4 @@
export { SymbolMetadata } from "./SymbolMetadata"
export { ClassMetadata } from "./ClassMetadata"
export { FileMetadata } from "./FileMetadata"
export { ProgramMetadata } from "./ProgramMetadata"

View file

@ -0,0 +1,33 @@
import { Type } from "../types/AllTypes"
import { SymbolMetadata } from "./SymbolMetadata"
export class ClassMetadata {
constructor(
public members: Map<string, SymbolMetadata> = new Map<
string,
SymbolMetadata
>(),
) {}
setMember(name: string, s: SymbolMetadata) {
this.members.set(name, s)
}
getMember(name: string) {
return this.members.get(name)
}
getMembers() {
return this.members.keys()
}
toConstructor() {
return (
`new ClassMetadata(new Map<string, SymbolMetadata>([` +
Array.from(this.members.entries())
.map(([n, m]) => `[${JSON.stringify(n)}, ${m.toConstructor()}]`)
.join(",\n") +
`]))`
)
}
}

View file

@ -0,0 +1,53 @@
import { SymbolMetadata } from "./SymbolMetadata"
import { ClassMetadata } from "./ClassMetadata"
export class FileMetadata {
constructor(
public classes: Map<string, ClassMetadata> = new Map<
string,
ClassMetadata
>(),
public functions: Map<string, SymbolMetadata> = new Map<
string,
SymbolMetadata
>(),
) {}
setClass(name: string, c: ClassMetadata) {
this.classes.set(name, c)
}
getClass(name: string) {
return this.classes.get(name)
}
getClasses() {
return Array.from(this.classes.keys())
}
setFunction(name: string, f: SymbolMetadata) {
this.functions.set(name, f)
}
getFunction(name: string) {
return this.functions.get(name)
}
getFunctions() {
return Array.from(this.functions.keys())
}
toConstructor() {
return (
`new FileMetadata(new Map<string, ClassMetadata>([` +
Array.from(this.classes.entries())
.map(([n, c]) => `[${JSON.stringify(n)}, ${c.toConstructor()}]`)
.join(",\n") +
`]), new Map<string, SymbolMetadata>([` +
Array.from(this.functions.entries())
.map(([n, f]) => `[${JSON.stringify(n)}, ${f.toConstructor()}]`)
.join(",\n") +
`]))`
)
}
}

View file

@ -0,0 +1,28 @@
import { FileMetadata } from "./FileMetadata"
export class ProgramMetadata {
constructor(
public files: Map<string, FileMetadata> = new Map<
string,
FileMetadata
>(),
) {}
setFile(name: string, file: FileMetadata) {
this.files.set(name, file)
}
getFile(name: string) {
return this.files.get(name)
}
toConstructor() {
return (
`new ProgramMetadata(new Map<string, FileMetadata>([` +
Array.from(this.files.entries())
.map(([n, f]) => `[${JSON.stringify(n)}, ${f.toConstructor()}]`)
.join(",\n") +
`]))`
)
}
}

View file

@ -0,0 +1,11 @@
import { Type } from "../types/AllTypes"
export class SymbolMetadata {
constructor(public doc: string, public type: Type) {}
toConstructor() {
return `new SymbolMetadata(${JSON.stringify(
this.doc,
)}, ${this.type.toConstructor()})`
}
}

View file

@ -0,0 +1,13 @@
export { Type } from "./Type"
export { AnyType } from "./AnyType"
export { BooleanType } from "./BooleanType"
export { FunctionType } from "./FunctionType"
export { NumberType } from "./NumberType"
export { ObjectType } from "./ObjectType"
export { StringType } from "./StringType"
export { TypeReferenceType } from "./TypeReferenceType"
export { VoidType } from "./VoidType"
export { ArrayType } from "./ArrayType"
export { LiteralTypeType } from "./LiteralTypeType"
export { TupleType } from "./TupleType"
export { UnionType } from "./UnionType"

19
compiler/types/AnyType.ts Normal file
View file

@ -0,0 +1,19 @@
import { Type } from "./Type"
export class AnyType implements Type {
kind = "any"
constructor() {}
toConstructor() {
return "new AnyType()"
}
toString() {
return this.kind
}
convert(argument) {
return argument
}
}

View file

@ -0,0 +1,29 @@
import { Type } from "./Type"
export class ArrayType implements Type {
kind = "array"
constructor(public elemType: Type) {}
toConstructor() {
return `new ArrayType(${this.elemType.toConstructor()})`
}
toString() {
return `${this.elemType.toString()}[]`
}
convert(argument) {
if (!Array.isArray(argument)) {
try {
argument = JSON.parse(argument)
} catch (e) {
throw new Error(`Can't convert ${argument} to array:`)
}
if (!Array.isArray(argument)) {
throw new Error(`Can't convert ${argument} to array:`)
}
}
return argument.map(v => this.elemType.convert(v))
}
}

View file

@ -0,0 +1,21 @@
import { Type } from "./Type"
export class BooleanType implements Type {
kind = "boolean"
constructor() {}
toConstructor() {
return "new BooleanType()"
}
toString() {
return this.kind
}
convert(argument) {
if (argument === "true") return true
else if (argument === "false") return false
throw new Error("Can't convert ${argument} to boolean")
}
}

View file

@ -0,0 +1,28 @@
import { Type } from "./Type"
export class FunctionType implements Type {
kind = "function"
constructor(public args: Type[], public ret: Type) {}
toConstructor() {
return (
`new FunctionType([` +
// Convert every argument type to its string constructor representation
this.args.map(cur => cur.toConstructor()) +
`], ${this.ret.toConstructor()})`
)
}
toString() {
return `(${this.args.map(a => a.toString()).join(", ")}) => ${this.ret.toString()}`
}
convert(argument) {
// Possible strategies:
// - eval()
// - window[argument]
// - tri.excmds[argument]
throw new Error(`Conversion to function not implemented: ${argument}`)
}
}

View file

@ -0,0 +1,24 @@
import { Type } from "./Type"
export class LiteralTypeType implements Type {
kind = "LiteralType"
constructor(public value: string) {}
toConstructor() {
return `new LiteralTypeType(${JSON.stringify(this.value)})`
}
toString() {
return JSON.stringify(this.value)
}
convert(argument) {
if (argument === this.value) return argument
throw new Error(
`Argument does not match expected value (${
this.value
}): ${argument}`,
)
}
}

View file

@ -0,0 +1,23 @@
import { Type } from "./Type"
export class NumberType implements Type {
kind = "number"
constructor() {}
toConstructor() {
return "new NumberType()"
}
toString() {
return this.kind
}
convert(argument) {
let n = parseInt(argument)
if (!Number.isNaN(n)) return n
n = parseFloat(argument)
if (!Number.isNaN(n)) return n
throw new Error(`Can't convert to number: ${argument}`)
}
}

View file

@ -0,0 +1,43 @@
import { Type } from "./Type"
export class ObjectType implements Type {
kind = "object"
// Note: a map that has an empty key ("") uses the corresponding type as default type
constructor(public members: Map<string, Type> = new Map<string, Type>()) {}
toConstructor() {
return `new ObjectType(new Map<string, Type>([` +
Array.from(this.members.entries()).map(([n, m]) => `[${JSON.stringify(n)}, ${m.toConstructor()}]`)
.join(", ") +
`]))`
}
toString() {
return this.kind
}
convertMember(memberName: string[], memberValue: string) {
let type = this.members.get(memberName[0])
if (!type) {
// No type, try to get the default type
type = this.members.get("")
if (!type) {
// No info for this member and no default type, anything goes
return memberValue
}
}
if (type.kind == "object") {
return (type as ObjectType).convertMember(memberName.slice(1), memberValue)
}
return type.convert(memberValue)
}
convert(argument) {
try {
return JSON.parse(argument)
} catch (e) {
throw new Error(`Can't convert to object: ${argument}`)
}
}
}

View file

@ -0,0 +1,20 @@
import { Type } from "./Type"
export class StringType implements Type {
kind = "string"
constructor() {}
toConstructor() {
return "new StringType()"
}
toString() {
return this.kind
}
convert(argument) {
if (typeof argument === "string") return argument
throw new Error(`Can't convert to string: ${argument}`)
}
}

View file

@ -0,0 +1,39 @@
import { Type } from "./Type"
export class TupleType implements Type {
kind = "tuple"
constructor(public elemTypes: Type[]) {}
toConstructor() {
return (
`new TupleType([` +
// Convert every element type to its constructor representation
this.elemTypes.map(cur => cur.toConstructor()).join(",\n") +
`])`
)
}
toString() {
return `[${this.elemTypes.map(e => e.toString()).join(", ")}]`
}
convert(argument) {
if (!Array.isArray(argument)) {
try {
argument = JSON.parse(argument)
} catch (e) {
throw new Error(`Can't convert to tuple: ${argument}`)
}
if (!Array.isArray(argument)) {
throw new Error(`Can't convert to tuple: ${argument}`)
}
}
if (argument.length != this.elemTypes.length) {
throw new Error(
`Error converting tuple: number of elements and type mismatch ${argument}`,
)
}
return argument.map((v, i) => this.elemTypes[i].convert(v))
}
}

9
compiler/types/Type.ts Normal file
View file

@ -0,0 +1,9 @@
import * as ts from "typescript"
export interface Type {
kind: string
name?: string
toConstructor(): string
toString(): string
convert: (argument: string) => any
}

View file

@ -0,0 +1,22 @@
import { Type } from "./Type"
export class TypeReferenceType implements Type {
constructor(public kind: string, public args: Type[]) {}
toConstructor() {
return (
`new TypeReferenceType(${JSON.stringify(this.kind)}, [` +
// Turn every type argument into its constructor representation
this.args.map(cur => cur.toConstructor()).join(",\n") +
`])`
)
}
toString() {
return `${this.kind}<${this.args.map(a => a.toString()).join(", ")}>`
}
convert(argument) {
throw new Error("Conversion of simple type references not implemented.")
}
}

View file

@ -0,0 +1,33 @@
import { Type } from "./Type"
export class UnionType implements Type {
kind = "union"
constructor(public types: Type[]) {}
toConstructor() {
return (
`new UnionType([` +
// Convert every type to its string constructor representation
this.types.map(cur => cur.toConstructor()).join(",\n") +
`])`
)
}
toString() {
return this.types.map(t => t.toString()).join(" | ")
}
convert(argument) {
for (let t of this.types) {
try {
return t.convert(argument)
} catch (e) {}
}
throw new Error(
`Can't convert argument to any of types: ${argument}, ${
this.types
}`,
)
}
}

View file

@ -0,0 +1,19 @@
import { Type } from "./Type"
export class VoidType implements Type {
kind = "void"
constructor() {}
toConstructor() {
return "new VoidType()"
}
toString() {
return this.kind
}
convert(argument) {
return null
}
}

View file

@ -38,7 +38,7 @@ fi
# It's important to generate the metadata before the documentation because
# missing imports might break documentation generation on clean builds
"$(npm bin)/tsc" compiler/gen_metadata.ts -m commonjs --target es2016 \
"$(npm bin)/tsc" compiler/gen_metadata.ts -m commonjs --target es2017 \
&& node compiler/gen_metadata.js \
--out src/.metadata.generated.ts \
--themeDir src/static/themes \

View file

@ -1,5 +1,4 @@
import * as Completions from "@src/completions"
import { typeToSimpleString } from "@src/lib/metadata"
import * as Metadata from "@src/.metadata.generated"
import state from "@src/state"
import * as config from "@src/lib/config"
@ -10,7 +9,6 @@ class ExcmdCompletionOption extends Completions.CompletionOptionHTML
public fuseKeys = []
constructor(
public value: string,
public ttype: string = "",
public documentation: string = "",
) {
super()
@ -22,7 +20,6 @@ class ExcmdCompletionOption extends Completions.CompletionOptionHTML
<td class="documentation">${documentation}</td>
</tr>`
}
// <td class="type">${ttype}</td>
}
export class ExcmdCompletionSource extends Completions.CompletionSourceFuse {
@ -49,37 +46,26 @@ export class ExcmdCompletionSource extends Completions.CompletionSourceFuse {
private async updateOptions(exstr?: string) {
if (!exstr) exstr = ""
this.lastExstr = exstr
let fns = Metadata.everything["src/excmds.ts"].functions
this.options = (await this.scoreOptions(
Object.keys(fns).filter(f => f.startsWith(exstr)),
)).map(f => {
let t = ""
if (fns[f].type) t = typeToSimpleString(fns[f].type)
return new ExcmdCompletionOption(f, t, fns[f].doc)
})
let exaliases = config.get("exaliases")
for (let alias of Object.keys(exaliases).filter(a =>
a.startsWith(exstr),
)) {
let excmds = Metadata.everything.getFile("src/excmds.ts")
if (!excmds) return
let fns = excmds.getFunctions()
// Add all excmds that start with exstr and that tridactyl has metadata about to completions
this.options = (await this.scoreOptions(
fns.filter(f => f.startsWith(exstr)),
)).map(f => new ExcmdCompletionOption(f, excmds.getFunction(f).doc))
// Also add aliases to possible completions
let exaliases = Object.keys(config.get("exaliases")).filter(a => a.startsWith(exstr))
for (let alias of exaliases) {
let cmd = aliases.expandExstr(alias)
if (fns[cmd]) {
this.options = this.options.concat(
new ExcmdCompletionOption(
alias,
fns[cmd].type ? typeToSimpleString(fns[cmd].type) : "",
`Alias for \`${cmd}\`. ${fns[cmd].doc}`,
),
)
let fn = excmds.getFunction(cmd)
if (fn) {
this.options.push(new ExcmdCompletionOption(alias, `Alias for \`${cmd}\`. ${fn.doc}`))
} else {
// This can happen when the alias is a composite command or a command with arguments. We can't display type or doc because we don't know what parameter the alias takes or what it does.
this.options = this.options.concat(
new ExcmdCompletionOption(
alias,
"",
`Alias for \`${cmd}\`.`,
),
)
// This can happen when the alias is a composite command or a command with arguments. We can't display doc because we don't know what parameter the alias takes or what it does.
this.options.push(new ExcmdCompletionOption(alias, `Alias for \`${cmd}\`.`))
}
}

View file

@ -4,7 +4,6 @@ import * as aliases from "@src/lib/aliases"
import * as config from "@src/lib/config"
import state from "@src/state"
import { browserBg } from "@src/lib/webext"
import { typeToString } from "@src/lib/metadata"
class HelpCompletionOption extends Completions.CompletionOptionHTML implements Completions.CompletionOptionFuse {
public fuseKeys = []
@ -51,33 +50,37 @@ export class HelpCompletionSource extends Completions.CompletionSourceFuse {
}
let configmd = Metadata.everything["src/lib/config.ts"].classes.default_config
let fns = Metadata.everything["src/excmds.ts"].functions
let settings = config.get()
let exaliases = settings.exaliases
let bindings = settings.nmaps
let file, default_config, excmds, fns, settings, exaliases, bindings
if (!(file = Metadata.everything.getFile("src/lib/config.ts"))
|| !(default_config = file.getClass("default_config"))
|| !(excmds = Metadata.everything.getFile("src/excmds.ts"))
|| !(fns = excmds.getFunctions())
|| !(settings = config.get())
|| !(exaliases = settings.exaliases)
|| !(bindings = settings.nmaps))
return;
// Settings completion
this.options = Object.keys(settings)
.filter(x => x.startsWith(query))
.map(setting => {
let doc = ""
if (configmd[setting]) {
doc = configmd[setting].doc.join(" ")
let member, doc = ""
if (member = default_config.getMember(setting)) {
doc = member.doc
}
return new HelpCompletionOption(setting, `Setting. ${doc}`)
})
// Excmd completion
.concat(Object.keys(fns)
.concat(fns
.filter(fn => fn.startsWith(query))
.map(f => new HelpCompletionOption(f, `Excmd. ${fns[f].doc}`))
.map(f => new HelpCompletionOption(f, `Excmd. ${excmds.getFunction(f).doc}`))
)
// Alias completion
.concat(Object.keys(exaliases)
.filter(alias => alias.startsWith(query))
.map(alias => {
let cmd = aliases.expandExstr(alias)
let doc = (fns[cmd] || {}).doc || ""
let doc = (excmds.getFunction(cmd) || {}).doc || ""
return new HelpCompletionOption(alias, `Alias for \`${cmd}\`. ${doc}`)
})
)

View file

@ -115,7 +115,7 @@ export class HistoryCompletionSource extends Completions.CompletionSourceFuse {
// Search history, dedupe and sort by frecency
let history = await browserBg.history.search({
text: query,
maxResults: Number(config.get("historyresults")),
maxResults: config.get("historyresults"),
startTime: 0,
})

View file

@ -2,7 +2,6 @@ import * as Completions from "@src/completions"
import * as config from "@src/lib/config"
import { browserBg } from "@src/lib/webext"
import * as metadata from "@src/.metadata.generated"
import { typeToString } from "@src/lib/metadata"
class SettingsCompletionOption extends Completions.CompletionOptionHTML
implements Completions.CompletionOptionFuse {
@ -62,18 +61,22 @@ export class SettingsCompletionSource extends Completions.CompletionSourceFuse {
options += options ? " " : ""
let configmd =
metadata.everything["src/lib/config.ts"].classes.default_config
let settings = config.get()
let file, default_config, settings
if (!(file = metadata.everything.getFile("src/lib/config.ts"))
|| !(default_config = file.getClass("default_config"))
|| !(settings = config.get()))
return
this.options = Object.keys(settings)
.filter(x => x.startsWith(query))
.sort()
.map(setting => {
let md = undefined
let doc = ""
let type = ""
if (configmd[setting]) {
doc = configmd[setting].doc.join(" ")
type = typeToString(configmd[setting].type)
if (md = default_config.getMember(setting)) {
doc = md.doc
type = md.type.toString()
}
return new SettingsCompletionOption(options + setting, {
name: setting,
@ -82,7 +85,6 @@ export class SettingsCompletionSource extends Completions.CompletionSourceFuse {
type: type,
})
})
// this.options = [new SettingsCompletionOption("ok", {name: "ok", docs:""})]
this.updateChain()
}

View file

@ -12,14 +12,14 @@ async function getSmooth() {
async function getDuration() {
if (opts.duration === null)
opts.duration = await config.getAsync("scrollduration")
return Number.parseInt(opts.duration)
return opts.duration
}
browser.storage.onChanged.addListener(changes => {
if ("userconfig" in changes) {
if ("smoothscroll" in changes.userconfig.newValue)
opts.smooth = changes.userconfig.newValue["smoothscroll"]
if ("scrollduration" in changes.userconfig.newValue)
opts.duration = Number.parseInt(changes.userconfig.newValue["scrollduration"])
opts.duration = changes.userconfig.newValue["scrollduration"]
}
})
@ -60,7 +60,7 @@ class ScrollingData {
if (elapsed >= this.duration || this.elem[this.pos] == this.endPos)
return this.endPos
let result = (this.endPos - this.startPos) * elapsed / this.duration
let result = ((this.endPos - this.startPos) * elapsed) / this.duration
if (result >= 1 || result <= -1) return this.startPos + result
return this.elem[this.pos] + (this.startPos < this.endPos ? 1 : -1)
}

View file

@ -84,7 +84,6 @@ import Mark from "mark.js"
import * as CSS from "css"
import * as Perf from "@src/perf"
import * as Metadata from "@src/.metadata.generated"
import { fitsType, typeToString } from "@src/lib/metadata"
//#content_helper
// {
@ -759,7 +758,7 @@ export function addJump(scrollEvent: UIEvent) {
list.push({ x: pageX, y: pageY })
jumps.cur = jumps.list.length - 1
saveJumps(alljumps)
}, Number.parseInt(config.get("jumpdelay")))
}, config.get("jumpdelay"))
})
}
@ -1080,7 +1079,7 @@ export function viewsource(url = "") {
}
/**
* Go to the homepages you have set with `set homepages [url1] [url2]`.
* Go to the homepages you have set with `set homepages ["url1", "url2"]`.
*
* @param all
* - if "true", opens all homepages in new tabs
@ -2836,21 +2835,27 @@ export function searchsetkeyword(keyword: string, url: string) {
*/
function validateSetArgs(key: string, values: string[]) {
const target: any[] = key.split(".")
const currentValue = config.get(...target)
const last = target[target.length - 1]
let value: string | string[] = values
if (Array.isArray(currentValue)) {
// Do nothing
} else if (currentValue === undefined || typeof currentValue === "string") {
value = values.join(" ")
let value, file, default_config, md
if ((file = Metadata.everything.getFile("src/lib/config.ts")) && (default_config = file.getClass("default_config")) && (md = default_config.getMember(target[0]))) {
const strval = values.join(" ")
// Note: the conversion will throw if strval can't be converted to the right type
if (md.type.kind == "object")
value = md.type.convertMember(target.slice(1), strval)
else
value = md.type.convert(strval)
} else {
throw "Unsupported setting type!"
}
let md = Metadata.everything["src/lib/config.ts"].classes.default_config[last]
if (md) {
if (md.type && !fitsType(value, md.type)) throw `Given type does not match expected type (given: ${value}, expected: ${typeToString(md.type)})`
// If we don't have metadata, fall back to the old way
logger.warning("Could not fetch setting metadata. Falling back to type of current value.")
const currentValue = config.get(...target)
if (Array.isArray(currentValue)) {
// Do nothing
} else if (currentValue === undefined || typeof currentValue === "string") {
value = values.join(" ")
} else {
throw "Unsupported setting type!"
}
}
target.push(value)

View file

@ -63,7 +63,7 @@ class default_config {
/**
* Internal field to handle site-specific config priorities. Use :seturl/:unseturl to change this value.
*/
priority = "0"
priority = 0
// Note to developers: When creating new <modifier-letter> maps, make sure to make the modifier uppercase (e.g. <C-a> instead of <c-a>) otherwise some commands might not be able to find them (e.g. `bind <c-a>`)
@ -379,7 +379,7 @@ class default_config {
/**
* Whether to use the keytranslatemap in various maps.
*/
keytranslatemodes = {
keytranslatemodes: {[key:string]: "true" | "false"} = {
nmaps: "true",
imaps: "false",
inputmaps: "false",
@ -517,9 +517,9 @@ class default_config {
storageloc: "sync" | "local" = "sync"
/**
* Pages opened with `gH`.
* Pages opened with `gH`. In order to set this value, use `:set homepages ["example.org", "example.net", "example.com"]` and so on.
*/
homepages = []
homepages: string[] = []
/**
* Characters to use in hint mode.
@ -548,7 +548,7 @@ class default_config {
*
* The point of this is to prevent accidental execution of normal mode binds due to people typing more than is necessary to choose a hint.
*/
hintdelay = "300"
hintdelay = 300
/**
* Controls whether the page can focus elements for you via js
@ -572,7 +572,7 @@ class default_config {
/**
* How viscous you want smooth scrolling to feel.
*/
scrollduration = "100"
scrollduration = 100
/**
* Where to open tabs opened with `tabopen` - to the right of the current tab, or at the end of the tabs.
@ -590,15 +590,15 @@ class default_config {
/**
* Controls text-to-speech volume. Has to be a number between 0 and 1.
*/
ttsvolume = "1"
ttsvolume = 1
/**
* Controls text-to-speech speed. Has to be a number between 0.1 and 10.
*/
ttsrate = "1"
ttsrate = 1
/**
* Controls text-to-speech pitch. Has to be between 0 and 2.
*/
ttspitch = "1"
ttspitch = 1
/**
* If nextinput, <Tab> after gi brings selects the next input
@ -627,7 +627,7 @@ class default_config {
/**
* Milliseconds before registering a scroll in the jumplist
*/
jumpdelay = "3000"
jumpdelay = 3000
/**
* Logging levels. Unless you're debugging Tridactyl, it's unlikely you'll ever need to change these.
@ -644,6 +644,10 @@ class default_config {
state: "warning",
styling: "warning",
}
/**
* Pages on which the command line should not be inserted. Set these values with `:set noiframeon ["url1", "url2"]`.
*/
noiframeon: string[] = []
/**
@ -713,7 +717,7 @@ class default_config {
/**
* Number of most recent results to ask Firefox for. We display the top 20 or so most frequently visited ones.
*/
historyresults = "50"
historyresults = 50
/**
* Change this to "clobber" to ruin the "Content Security Policy" of all sites a bit and make Tridactyl run a bit better on some of them, e.g. raw.github*
@ -807,8 +811,8 @@ export function getURL(url, target) {
// Sort them from highest to lowest priority, default to a priority of 10
.sort(
(k1, k2) =>
(Number(USERCONFIG.subconfigs[k2].priority) || 10) -
(Number(USERCONFIG.subconfigs[k1].priority) || 10),
(USERCONFIG.subconfigs[k2].priority || 10) -
(USERCONFIG.subconfigs[k1].priority || 10),
)
// Get the first config name that has `target`
.find(k => getDeepProperty(USERCONFIG.subconfigs[k], target))
@ -1004,6 +1008,35 @@ export async function update() {
})
set("configversion", "1.3")
},
"1.3": () => {
// Updates a value both in the main config and in sub (=site specific) configs
let updateAll = (setting: any[], fn: (any) => any) => {
let val = getDeepProperty(USERCONFIG, setting)
if (val) {
set(...setting, fn(val))
}
let subconfigs = getDeepProperty(USERCONFIG, ["subconfigs"])
if (subconfigs) {
Object.keys(subconfigs)
.map(pattern => [pattern, getURL(pattern, setting)])
.filter(([pattern, value]) => value)
.forEach(([pattern, value]) =>
setURL(pattern, ...setting, fn(value)),
)
}
}
;[
"priority",
"hintdelay",
"scrollduration",
"ttsvolume",
"ttsrate",
"ttspitch",
"jumpdelay",
"historyresults",
].forEach(setting => updateAll([setting], parseInt))
set("configversion", "1.4")
},
}
if (!get("configversion")) set("configversion", "0.0")
const updatetest = v => {

View file

@ -1,110 +0,0 @@
export function fitsType(variable, type) {
switch (type.kind) {
case "function":
return variable instanceof Function
case "array":
return (
variable instanceof Array &&
!variable.find(e => !fitsType(e, type.type))
)
case "Promise":
return variable instanceof Promise
case "union":
for (let t of type.arguments) {
if (fitsType(variable, t)) return true
}
return false
case "LiteralType":
return variable == type.name
case "tuple":
if (variable.length != type.arguments.length) return false
for (let i = 0; i < variable.length; ++i) {
let v = variable[i]
let t = type.arguments[i]
if (!fitsType(v, t)) return false
}
return true
case "any":
return true
case "object":
return true
case "boolean":
return typeof variable == "number"
case "number":
return typeof variable == "number"
case "string":
return typeof variable == "string"
case "void":
return !variable
}
throw new Error("Unhandled type!")
}
// Gets the type sitting inside a promise
function unwrapPromise(type) {
while (type.kind == "Promise") type = type.arguments[0]
return type
}
// This turns TYPE into a string, the format of which is quite short in order to be useful for completions
export function typeToSimpleString(type): string {
let result = ""
switch (type.kind) {
case "function":
result = type.arguments.map(typeToSimpleString).join(", ")
let t = unwrapPromise(type.type)
if (!["any", "object", "void"].includes(t.kind)) {
if (result == "") result = "()"
result += "->" + typeToString(t)
}
break
default:
result = type.name || ""
}
return result
}
// This turns TYPE into a string, the format of which is quite close to the one tsc uses to display type strings
export function typeToString(type): string {
let allTypes = arr => arr.map(typeToString).join(", ")
let result = ""
switch (type.kind) {
case "function":
result =
"(" +
allTypes(type.arguments) +
")" +
" => " +
typeToString(type.type)
break
case "array":
result = typeToString(type.type) + "[]"
break
case "Promise":
result = "Promise<" + allTypes(type.arguments) + ">"
break
case "union":
result = type.arguments.map(typeToString).join(" | ")
break
case "LiteralType":
result = type.name
break
case "tuple":
result = "(" + type.arguments.map(typeToString).join(", ") + ")"
case "any":
case "object":
case "boolean":
case "number":
case "string":
case "void":
default:
result = type.kind
}
let name = ""
// If the type has a name, it could be interesting to add it to the string
// representation of the type. However, when the type is a LiteralType, the
// name is already in result, so there's no need to add it.
if (type.kind != "LiteralType" && type.name) name = type.name + ": "
return name + result
}

View file

@ -27,10 +27,10 @@ export function readText(text: string): void {
let utterance = new SpeechSynthesisUtterance(text)
let pitch = Number.parseFloat(Config.get("ttspitch"))
let pitch = Config.get("ttspitch")
let voice = Config.get("ttsvoice")
let volume = Number.parseFloat(Config.get("ttsvolume"))
let rate = Number.parseFloat(Config.get("ttsrate"))
let volume = Config.get("ttsvolume")
let rate = Config.get("ttsrate")
if (pitch >= 0 && pitch < 2) utterance.pitch = pitch