Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tooltips for top-level variables #2776

Merged
merged 18 commits into from
Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/components/src/components/CodeEditor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export type CodeEditorProps = {
showGutter?: boolean;
lineWrapping?: boolean;
errors?: SqError[];
sourceId?: string;
sourceId: string;
fontSize?: number;
project: SqProject;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,39 +11,53 @@ import { SqProject } from "@quri/squiggle-lang";

import { FnDocumentationFromName } from "../../ui/FnDocumentation.js";

export function getNameNodes(tree: Tree, from: number) {
type NameNode = {
node: SyntaxNode;
type: "function" | "variable";
};

export function getNameNodes(tree: Tree, from: number): NameNode[] {
const cursor = tree.cursorAt(from, -1);
const nameNodes: SyntaxNode[] = [];
const nameNodes: NameNode[] = [];

// We walk up and backwards through the tree, looking for nodes that have names.

let direction: "start" | "sibling" | "parent" | undefined = "start";
while (1) {
// Only for sibling nodes; `foo = { <cursor> }` shouldn't autocomplete `foo`.
if (cursor.type.is("Binding") && direction === "sibling") {
const nameNode = cursor.node.getChild("VariableName");
if (nameNode) {
nameNodes.push(nameNode);
if (cursor.type.is("Statement") && direction === "sibling") {
// Only for sibling nodes; `foo = { <cursor> }` shouldn't autocomplete `foo`.

// Unwrap decorated statements.
let node: SyntaxNode | null = cursor.node;
while (node && node.type.is("DecoratedStatement")) {
node = node.getChild("Statement");
}
// Only for sibling nodes; Squiggle doesn't support recursive calls.
} else if (cursor.type.is("FunDeclaration") && direction === "sibling") {
const nameNode = cursor.node.getChild("FunctionName");
if (nameNode) {
nameNodes.push(nameNode);

const nameNode = node?.getChild("VariableName");
if (node && nameNode) {
nameNodes.push({
node: nameNode,
type: node?.type.is("DefunStatement") ? "function" : "variable",
});
}
} else if (cursor.type.is("FunDeclaration") && direction !== "sibling") {
} else if (cursor.type.is("DefunStatement") && direction !== "sibling") {
// Function declaration that's a parent, let's autocomplete its parameter names.
// Note that we also allow `direction === "start"`, to handle `f(foo) = foo` correctly.
const parameterNodes =
cursor.node.getChild("LambdaArgs")?.getChildren("LambdaParameter") ??
[];

for (const parameter of parameterNodes) {
const nameNode = parameter.getChild("LambdaParameterName");
if (nameNode) {
nameNodes.push(nameNode);
nameNodes.push({
node: nameNode,
// Is there a more specific type? There's no "parameter" type in CodeMirror.
// https://codemirror.net/docs/ref/#autocomplete.Completion.type
type: "variable",
});
}
}
} else if (cursor.type.is("Decorator") && direction !== "sibling") {
// TODO
}

// Move to the next node and store the direction that we used.
Expand Down Expand Up @@ -106,24 +120,26 @@ export function makeCompletionSource(project: SqProject) {
snippetCompletion("|${args}| ${body}", {
label: "|",
detail: "lambda function",
type: "syntax",
type: "text",
}),
],
};
}
}

{
const identifier = cmpl.tokenBefore(["AccessExpr", "IdentifierExpr"]);
const identifier = cmpl.tokenBefore(["AccessExpr", "Identifier"]);
if (identifier) {
const { from } = identifier;
const nameNodes = getNameNodes(tree, from);
const localCompletions = nameNodes.map((node): Completion => {
const name = cmpl.state.doc.sliceString(node.from, node.to);
const type = node.type.is("FunctionName") ? "function" : "variable";
const localCompletions = nameNodes.map((nameNode): Completion => {
const name = cmpl.state.doc.sliceString(
nameNode.node.from,
nameNode.node.to
);
return {
label: name,
type,
type: nameNode.type,
};
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,7 @@ export const lightThemeHighlightingStyle = HighlightStyle.define([
],
color: numbers,
},
{
tag: [tags.escape],
color: escapes,
},
{ tag: tags.escape, color: escapes },
{
tag: [
tags.operator,
Expand All @@ -67,7 +64,7 @@ export const lightThemeHighlightingStyle = HighlightStyle.define([
fontWeight: "bold",
color: operators,
},
{ tag: [tags.meta, tags.comment], color: comments },
{ tag: tags.comment, color: comments },
{ tag: tags.strong, fontWeight: "bold" },
{ tag: tags.emphasis, fontStyle: "italic" },
{ tag: tags.strikethrough, textDecoration: "line-through" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ commaSep<content> {
// when trailing comma is allowed
commaSep1<content> { "" | content ("," content?)* }

Binding { export? VariableName { identifier } "=" expression }
LetStatement { export? VariableName { identifier } "=" expression }

LambdaParameter {
LambdaParameterName { identifier } (":" expression)?
Expand All @@ -50,7 +50,7 @@ LambdaArgs {
() | LambdaParameter ("," LambdaParameter)*
}

FunDeclaration { export? FunctionName { identifier } ~callOrDeclaration "(" LambdaArgs ")" "=" expression }
DefunStatement { export? VariableName { identifier } ~callOrDeclaration "(" LambdaArgs ")" "=" expression }

Decorator {
"@" DecoratorName { identifier }
Expand All @@ -60,64 +60,62 @@ Decorator {
)
}

statement {
Decorator*
(
Binding
| FunDeclaration
)
statement[@isGroup="Statement"] {
LetStatement
| DefunStatement
| DecoratedStatement { Decorator statement }
}

expression {
expressionWithoutParens
| ( "(" expression ")" )
}

expression[@isGroup="Expression"] {
expressionWithoutParens[@isGroup="Expression"] {
String
| Boolean
| Boolean { @specialize[@name="Boolean"]<identifier, "true" | "false"> }
| Number
| BlockExpr { "{" blockContent "}" }
| DictExpr {
| Block { "{" blockContent "}" }
| Dict {
"{"
commaSep1<
Entry { Field[@dynamicPrecedence=1] { expression } ~inheritAmbig ":" expression }
KeyValue { Field[@dynamicPrecedence=1] { expression } ~inheritAmbig ":" expression }
| InheritEntry { Field[@dynamicPrecedence=0] { identifier } ~inheritAmbig }
>
"}"
}
| LambdaExpr { "{" ArgsOpen { "|" } LambdaArgs "|" blockContent "}" }
| IfExpr { if expression then expression !else else expression }
| ParenExpr { "(" expression ")" }
| IdentifierExpr { identifier }
| Lambda { "{" ArgsOpen { "|" } LambdaArgs "|" blockContent "}" }
| Identifier { identifier }
| AccessExpr { expression !deref "." Field { identifier } }
| CallExpr { expression ~callOrDeclaration !call "(" commaSep<Argument { expression }> ")" }
| TernaryExpr { expression !logop LogicOp<"?"> expression LogicOp<":"> expression }
| KVAccessExpr { expression !call ("[" Key { expression } "]") }
| ArrayExpr { "[" commaSep1<expression> "]" }
| UnaryExpr { !unary (ArithOp<"-"> | ArithOp<"!"> | DotArithOp<".-">) expression }
| LogicExpr {
| Call { expression ~callOrDeclaration !call "(" commaSep<Argument { expression }> ")" }
| TernaryC { expression !logop LogicOp<"?"> expression LogicOp<":"> expression }
| TernaryIfThenElse { if expression then expression !else else expression }
| BracketLookup { expression !call ("[" Key { expression } "]") }
| Array { "[" commaSep1<expression> "]" }
| UnaryCall { !unary (ArithOp<"-"> | ArithOp<"!"> | DotArithOp<".-">) expression }
| InfixCall {
expression !or LogicOp<"||"> expression
| expression !and LogicOp<"&&"> expression
| expression !rel LogicOp<">"> expression
| expression !rel LogicOp<"<"> expression
| expression !rel LogicOp<"<="> expression
| expression !rel LogicOp<">="> expression
| expression !rel LogicOp<"=="> expression
}
| ControlExpr {
expression !control ControlOp<"->"> expression
}
| ArithExpr {
expression !times ( ArithOp<"*"> | DotArithOp<".*"> ) expression
| expression !times ( ArithOp<"*"> | DotArithOp<".*"> ) expression
| expression !times ( ArithOp<"/"> | DotArithOp<"./"> ) expression
| expression !exp ( ArithOp<"^"> | DotArithOp<".^"> ) expression
| expression !plus ( ArithOp<"+"> | DotArithOp<".+"> ) expression
| expression !plus ( ArithOp<"-"> | DotArithOp<".-"> ) expression
| expression !plus @extend[@name="ArithOp"]<identifier, "to"> expression
}
| Pipe {
expression !control ControlOp<"->"> expression
}
}

// use `@extend` instead of `@specialize`, because keywords are valid variable names in Squiggle, for now.
kw<term> { @extend[@name={term}]<identifier, term> }


Boolean { @specialize[@name="Boolean"]<identifier, "true" | "false"> }

kw<term> { @specialize[@name={term}]<identifier, term> }
if { kw<"if"> }
then { kw<"then"> }
else { kw<"else"> }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,9 @@ import { parser } from "./generated/squiggle.js";
const parserWithMetadata = parser.configure({
props: [
styleTags({
if: t.keyword,
then: t.keyword,
else: t.keyword,
import: t.keyword,
export: t.keyword,
as: t.keyword,
"if then else import export as": t.keyword,

Equals: t.definitionOperator,

ArithOp: t.arithmeticOperator,
LogicOp: t.logicOperator,
ControlOp: t.controlOperator,
Expand All @@ -38,40 +32,36 @@ const parserWithMetadata = parser.configure({
Boolean: t.bool,
Number: t.integer,
String: t.string,
Comment: t.comment,
Void: t.escape,
LineComment: t.lineComment,
BlockComment: t.blockComment,
Escape: t.escape,

FunctionName: t.function(t.variableName),

DecoratorName: t.variableName,
"Decorator/*/String": t.comment,
At: t.keyword,

LambdaSyntax: t.blockComment,

VariableName: t.constant(t.variableName),
IdentifierExpr: t.variableName,
Identifier: t.variableName,
Field: t.variableName,
LambdaParameterName: t.variableName,
}),
foldNodeProp.add({
LambdaExpr: (context) => ({
Lambda: (context) => ({
from: context.getChild("NonEmptyProgram")?.from || 0,
to: context.getChild("NonEmptyProgram")?.to || 0,
}),
BlockExpr: foldInside,
DictExpr: foldInside,
ArrayExpr: foldInside,
Block: foldInside,
Dict: foldInside,
Array: foldInside,
}),
indentNodeProp.add({
DictExpr: (context) =>
Dict: (context) =>
context.baseIndent + (context.textAfter === "}" ? 0 : context.unit),
BlockExpr: (context) =>
Block: (context) =>
context.baseIndent + (context.textAfter === "}" ? 0 : context.unit),
LambdaExpr: (context) =>
Lambda: (context) =>
context.baseIndent + (context.textAfter === "}" ? 0 : context.unit),
ArrayExpr: (context) =>
Array: (context) =>
context.baseIndent + (context.textAfter === "]" ? 0 : context.unit),
}),
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,10 @@ export function useSquiggleEditorExtensions(
const formatExtension = useFormatSquiggleExtension();
const errorsExtension = useErrorsExtension(view, params.errors);

const tooltipsExtension = useTooltipsExtension();
const tooltipsExtension = useTooltipsExtension(view, {
project: params.project,
sourceId: params.sourceId,
});

const highPrioritySquiggleExtensions = [
submitExtension, // works only if listed before `builtinExtensions`
Expand Down