From be3ccb8ef4e8ae7f0362cbad3d2919d56c3c4433 Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Mon, 5 Aug 2024 13:14:19 +0200 Subject: [PATCH 01/14] remove console log --- frontend/components/CellInput.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/components/CellInput.js b/frontend/components/CellInput.js index 35773c436d..91065a7b9b 100644 --- a/frontend/components/CellInput.js +++ b/frontend/components/CellInput.js @@ -461,8 +461,6 @@ export const CellInput = ({ if (show_static_fake) return if (dom_node_ref.current == null) return - console.log("Rendering cell input", cell_id) - const keyMapSubmit = (/** @type {EditorView} */ cm) => { autocomplete.closeCompletion(cm) on_submit() From a71ce27915c3d3819186a679667934014835fa8a Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Tue, 6 Aug 2024 10:08:53 +0200 Subject: [PATCH 02/14] Update import Co-Authored-By: Sergio A. Vargas --- frontend/components/CellInput.js | 6 +- .../components/CellInput/lezer_template.js | 4 +- frontend/components/CellInput/mixedParsers.js | 8 +-- frontend/components/CellOutput.js | 4 +- frontend/components/ErrorMessage.js | 2 +- frontend/imports/CodemirrorPlutoSetup.js | 72 +------------------ 6 files changed, 15 insertions(+), 81 deletions(-) diff --git a/frontend/components/CellInput.js b/frontend/components/CellInput.js index 91065a7b9b..b4cfb464c0 100644 --- a/frontend/components/CellInput.js +++ b/frontend/components/CellInput.js @@ -43,7 +43,7 @@ import { } from "../imports/CodemirrorPlutoSetup.js" import { markdown, html as htmlLang, javascript, sqlLang, python, julia_mixed } from "./CellInput/mixedParsers.js" -import { julia_andrey } from "../imports/CodemirrorPlutoSetup.js" +import { julia } from "../imports/CodemirrorPlutoSetup.js" import { pluto_autocomplete } from "./CellInput/pluto_autocomplete.js" import { NotebookpackagesFacet, pkgBubblePlugin } from "./CellInput/pkg_bubble_plugin.js" import { awesome_line_wrapping, get_start_tabs } from "./CellInput/awesome_line_wrapping.js" @@ -142,7 +142,7 @@ export const pluto_syntax_colors = HighlightStyle.define( ], { all: { color: `var(--cm-editor-text-color)` }, - scope: julia_andrey().language, + scope: julia().language, } ) @@ -776,7 +776,7 @@ export const CellInput = ({ ] : [ // - julia_andrey(), + julia(), ]), go_to_definition_plugin, pluto_autocomplete({ diff --git a/frontend/components/CellInput/lezer_template.js b/frontend/components/CellInput/lezer_template.js index c034642452..edfdcaeab5 100644 --- a/frontend/components/CellInput/lezer_template.js +++ b/frontend/components/CellInput/lezer_template.js @@ -1,4 +1,4 @@ -import { julia_andrey, NodeProp, syntaxTree, Text } from "../../imports/CodemirrorPlutoSetup.js" +import { julia, NodeProp, syntaxTree, Text } from "../../imports/CodemirrorPlutoSetup.js" import lodash from "../../imports/lodash.js" // @ts-ignore @@ -9,7 +9,7 @@ import ManyKeysWeakMap from "https://esm.sh/many-keys-weakmap@1.0.0?pin=v113&tar * @returns {SyntaxNode} */ export let julia_to_ast = (julia_code) => { - return /** @type {any} */ (julia_andrey().language.parser.parse(julia_code).topNode.firstChild) + return /** @type {any} */ (julia().language.parser.parse(julia_code).topNode.firstChild) } // When you get errors while creating the templates (stuff related to substitutions), diff --git a/frontend/components/CellInput/mixedParsers.js b/frontend/components/CellInput/mixedParsers.js index da7e00ae7d..11898e0539 100644 --- a/frontend/components/CellInput/mixedParsers.js +++ b/frontend/components/CellInput/mixedParsers.js @@ -12,7 +12,7 @@ import { sql, javascript, python, - julia_andrey, + julia, parseCode, } from "../../imports/CodemirrorPlutoSetup.js" @@ -166,10 +166,10 @@ const juliaWrapper = parseMixed((node, input) => { }) const julia_mixed = (config) => { - const julia = julia_andrey(config) + const julia_simple = julia(config) // @ts-ignore - julia.language.parser = julia.language.parser.configure({ wrap: juliaWrapper }) - return julia + julia_simple.language.parser = julia_simple.language.parser.configure({ wrap: juliaWrapper }) + return julia_simple } export { julia_mixed, sqlLang, pythonLanguage, javascript, htmlLanguage, javascriptLanguage, python, markdown, html } diff --git a/frontend/components/CellOutput.js b/frontend/components/CellOutput.js index 5ee73add66..5484429c69 100644 --- a/frontend/components/CellOutput.js +++ b/frontend/components/CellOutput.js @@ -25,7 +25,7 @@ import { pluto_syntax_colors, ENABLE_CM_MIXED_PARSER } from "./CellInput.js" import hljs from "../imports/highlightjs.js" import { julia_mixed } from "./CellInput/mixedParsers.js" -import { julia_andrey } from "../imports/CodemirrorPlutoSetup.js" +import { julia } from "../imports/CodemirrorPlutoSetup.js" import { SafePreviewSanitizeMessage } from "./SafePreviewUI.js" const prettyAssignee = (assignee) => @@ -680,7 +680,7 @@ export let highlight = (code_element, language) => { syntaxHighlighting(defaultHighlightStyle, { fallback: true }), EditorState.tabSize.of(4), // TODO Other languages possibly? - ...(language === "julia" ? [ENABLE_CM_MIXED_PARSER ? julia_mixed() : julia_andrey()] : []), + ...(language === "julia" ? [ENABLE_CM_MIXED_PARSER ? julia_mixed() : julia()] : []), EditorView.lineWrapping, EditorView.editable.of(false), ].filter((x) => x != null), diff --git a/frontend/components/ErrorMessage.js b/frontend/components/ErrorMessage.js index 91ae58bb13..463ad010ab 100644 --- a/frontend/components/ErrorMessage.js +++ b/frontend/components/ErrorMessage.js @@ -1,6 +1,6 @@ import { cl } from "../common/ClassTable.js" import { PlutoActionsContext } from "../common/PlutoContext.js" -import { EditorState, EditorView, julia_andrey, lineNumbers, syntaxHighlighting } from "../imports/CodemirrorPlutoSetup.js" +import { EditorState, EditorView, julia, lineNumbers, syntaxHighlighting } from "../imports/CodemirrorPlutoSetup.js" import { html, useContext, useEffect, useLayoutEffect, useRef, useState } from "../imports/Preact.js" import { pluto_syntax_colors } from "./CellInput.js" import { highlight } from "./CellOutput.js" diff --git a/frontend/imports/CodemirrorPlutoSetup.js b/frontend/imports/CodemirrorPlutoSetup.js index a207ef132c..bdc95aeea8 100644 --- a/frontend/imports/CodemirrorPlutoSetup.js +++ b/frontend/imports/CodemirrorPlutoSetup.js @@ -1,10 +1,10 @@ -import { +export { EditorState, EditorSelection, Compartment, EditorView, placeholder, - julia_andrey, + julia, keymap, syntaxHighlighting, history, @@ -64,70 +64,4 @@ import { linter, setDiagnostics, //@ts-ignore -} from "https://cdn.jsdelivr.net/gh/JuliaPluto/codemirror-pluto-setup@1234.3.0/dist/index.es.min.js" - -export { - linter, - setDiagnostics, - EditorState, - EditorSelection, - Compartment, - EditorView, - placeholder, - julia_andrey, - keymap, - history, - historyKeymap, - defaultKeymap, - indentMore, - indentLess, - moveLineUp, - moveLineDown, - tags, - HighlightStyle, - lineNumbers, - highlightSpecialChars, - foldGutter, - drawSelection, - indentOnInput, - defaultHighlightStyle, - bracketMatching, - closeBrackets, - rectangularSelection, - highlightSelectionMatches, - closeBracketsKeymap, - searchKeymap, - foldKeymap, - syntaxTree, - syntaxTreeAvailable, - Decoration, - ViewUpdate, - ViewPlugin, - WidgetType, - Facet, - StateField, - StateEffect, - Transaction, - SelectionRange, - indentUnit, - combineConfig, - NodeProp, - autocomplete, - html, - htmlLanguage, - javascriptLanguage, - markdown, - markdownLanguage, - parseCode, - parseMixed, - PostgreSQL, - pythonLanguage, - sql, - syntaxHighlighting, - javascript, - python, - Text, - css, - cssLanguage, - selectNextOccurrence, -} +} from "https://cdn.jsdelivr.net/gh/JuliaPluto/codemirror-pluto-setup@07a6ae6/dist/index.es.min.js" From fe1428a128a8923b7fcd3050276928ca5603a08a Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Tue, 6 Aug 2024 10:12:02 +0200 Subject: [PATCH 03/14] Update CodemirrorPlutoSetup.d.ts Co-Authored-By: Sergio A. Vargas --- frontend/imports/CodemirrorPlutoSetup.d.ts | 5257 ++++++++++---------- 1 file changed, 2700 insertions(+), 2557 deletions(-) diff --git a/frontend/imports/CodemirrorPlutoSetup.d.ts b/frontend/imports/CodemirrorPlutoSetup.d.ts index 04f50f5260..bc707cc8d6 100644 --- a/frontend/imports/CodemirrorPlutoSetup.d.ts +++ b/frontend/imports/CodemirrorPlutoSetup.d.ts @@ -1580,3096 +1580,3260 @@ declare class RangeSet { static empty: RangeSet; } +declare class StyleModule { + constructor(spec: {[selector: string]: StyleSpec}, options?: { + finish?(sel: string): string + }) + getRules(): string + static mount( + root: Document | ShadowRoot | DocumentOrShadowRoot, + module: StyleModule | ReadonlyArray, + options?: {nonce?: string} + ): void + static newName(): string +} + +type StyleSpec = { + [propOrSelector: string]: string | number | StyleSpec | null +} + /** -The [`TreeFragment.applyChanges`](#common.TreeFragment^applyChanges) -method expects changed ranges in this format. +Used to indicate [text direction](https://codemirror.net/6/docs/ref/#view.EditorView.textDirection). */ -interface ChangedRange { - /** - The start of the change in the start document - */ - fromA: number; - /** - The end of the change in the start document - */ - toA: number; +declare enum Direction { /** - The start of the replacement in the new document + Left-to-right. */ - fromB: number; + LTR = 0, /** - The end of the replacement in the new document + Right-to-left. */ - toB: number; + RTL = 1 } /** -Tree fragments are used during [incremental -parsing](#common.Parser.startParse) to track parts of old trees -that can be reused in a new parse. An array of fragments is used -to track regions of an old tree whose nodes might be reused in new -parses. Use the static -[`applyChanges`](#common.TreeFragment^applyChanges) method to -update fragments for document changes. +Represents a contiguous range of text that has a single direction +(as in left-to-right or right-to-left). */ -declare class TreeFragment { +declare class BidiSpan { /** - The start of the unchanged range pointed to by this fragment. - This refers to an offset in the _updated_ document (as opposed - to the original tree). + The start of the span (relative to the start of the line). */ readonly from: number; /** - The end of the unchanged range. + The end of the span. */ readonly to: number; /** - The tree that this fragment is based on. + The ["bidi + level"](https://unicode.org/reports/tr9/#Basic_Display_Algorithm) + of the span (in this context, 0 means + left-to-right, 1 means right-to-left, 2 means left-to-right + number inside right-to-left text). */ - readonly tree: Tree; + readonly level: number; /** - The offset between the fragment's tree and the document that - this fragment can be used against. Add this when going from - document to tree positions, subtract it to go from tree to - document positions. + The direction of this span. */ - readonly offset: number; + get dir(): Direction; +} + +type Attrs = { + [name: string]: string; +}; + +/** +Basic rectangle type. +*/ +interface Rect { + readonly left: number; + readonly right: number; + readonly top: number; + readonly bottom: number; +} +type ScrollStrategy = "nearest" | "start" | "end" | "center"; + +interface MarkDecorationSpec { /** - Construct a tree fragment. You'll usually want to use - [`addTree`](#common.TreeFragment^addTree) and - [`applyChanges`](#common.TreeFragment^applyChanges) instead of - calling this directly. + Whether the mark covers its start and end position or not. This + influences whether content inserted at those positions becomes + part of the mark. Defaults to false. */ - constructor( + inclusive?: boolean; /** - The start of the unchanged range pointed to by this fragment. - This refers to an offset in the _updated_ document (as opposed - to the original tree). + Specify whether the start position of the marked range should be + inclusive. Overrides `inclusive`, when both are present. */ - from: number, + inclusiveStart?: boolean; /** - The end of the unchanged range. + Whether the end should be inclusive. */ - to: number, + inclusiveEnd?: boolean; /** - The tree that this fragment is based on. + Add attributes to the DOM elements that hold the text in the + marked range. */ - tree: Tree, + attributes?: { + [key: string]: string; + }; /** - The offset between the fragment's tree and the document that - this fragment can be used against. Add this when going from - document to tree positions, subtract it to go from tree to - document positions. + Shorthand for `{attributes: {class: value}}`. */ - offset: number, openStart?: boolean, openEnd?: boolean); + class?: string; /** - Whether the start of the fragment represents the start of a - parse, or the end of a change. (In the second case, it may not - be safe to reuse some nodes at the start, depending on the - parsing algorithm.) + Add a wrapping element around the text in the marked range. Note + that there will not necessarily be a single element covering the + entire range—other decorations with lower precedence might split + this one if they partially overlap it, and line breaks always + end decoration elements. */ - get openStart(): boolean; + tagName?: string; /** - Whether the end of the fragment represents the end of a - full-document parse, or the start of a change. + When using sets of decorations in + [`bidiIsolatedRanges`](https://codemirror.net/6/docs/ref/##view.EditorView^bidiIsolatedRanges), + this property provides the direction of the isolates. When null + or not given, it indicates the range has `dir=auto`, and its + direction should be derived from the first strong directional + character in it. */ - get openEnd(): boolean; + bidiIsolate?: Direction | null; /** - Create a set of fragments from a freshly parsed tree, or update - an existing set of fragments by replacing the ones that overlap - with a tree with content from the new tree. When `partial` is - true, the parse is treated as incomplete, and the resulting - fragment has [`openEnd`](#common.TreeFragment.openEnd) set to - true. + Decoration specs allow extra properties, which can be retrieved + through the decoration's [`spec`](https://codemirror.net/6/docs/ref/#view.Decoration.spec) + property. */ - static addTree(tree: Tree, fragments?: readonly TreeFragment[], partial?: boolean): readonly TreeFragment[]; + [other: string]: any; +} +interface WidgetDecorationSpec { /** - Apply a set of edits to an array of fragments, removing or - splitting fragments as necessary to remove edited ranges, and - adjusting offsets for fragments that moved. + The type of widget to draw here. */ - static applyChanges(fragments: readonly TreeFragment[], changes: readonly ChangedRange[], minGap?: number): readonly TreeFragment[]; -} -/** -Interface used to represent an in-progress parse, which can be -moved forward piece-by-piece. -*/ -interface PartialParse { + widget: WidgetType; /** - Advance the parse state by some amount. Will return the finished - syntax tree when the parse completes. + Which side of the given position the widget is on. When this is + positive, the widget will be drawn after the cursor if the + cursor is on the same position. Otherwise, it'll be drawn before + it. When multiple widgets sit at the same position, their `side` + values will determine their ordering—those with a lower value + come first. Defaults to 0. May not be more than 10000 or less + than -10000. */ - advance(): Tree | null; + side?: number; /** - The position up to which the document has been parsed. Note - that, in multi-pass parsers, this will stay back until the last - pass has moved past a given position. + By default, to avoid unintended mixing of block and inline + widgets, block widgets with a positive `side` are always drawn + after all inline widgets at that position, and those with a + non-positive side before inline widgets. Setting this option to + `true` for a block widget will turn this off and cause it to be + rendered between the inline widgets, ordered by `side`. */ - readonly parsedPos: number; + inlineOrder?: boolean; /** - Tell the parse to not advance beyond the given position. - `advance` will return a tree when the parse has reached the - position. Note that, depending on the parser algorithm and the - state of the parse when `stopAt` was called, that tree may - contain nodes beyond the position. It is an error to call - `stopAt` with a higher position than it's [current - value](#common.PartialParse.stoppedAt). + Determines whether this is a block widgets, which will be drawn + between lines, or an inline widget (the default) which is drawn + between the surrounding text. + + Note that block-level decorations should not have vertical + margins, and if you dynamically change their height, you should + make sure to call + [`requestMeasure`](https://codemirror.net/6/docs/ref/#view.EditorView.requestMeasure), so that the + editor can update its information about its vertical layout. */ - stopAt(pos: number): void; + block?: boolean; /** - Reports whether `stopAt` has been called on this parse. + Other properties are allowed. */ - readonly stoppedAt: number | null; + [other: string]: any; } -/** -A superclass that parsers should extend. -*/ -declare abstract class Parser { +interface ReplaceDecorationSpec { /** - Start a parse for a single tree. This is the method concrete - parser implementations must implement. Called by `startParse`, - with the optional arguments resolved. + An optional widget to drawn in the place of the replaced + content. */ - abstract createParse(input: Input, fragments: readonly TreeFragment[], ranges: readonly { - from: number; - to: number; - }[]): PartialParse; + widget?: WidgetType; /** - Start a parse, returning a [partial parse](#common.PartialParse) - object. [`fragments`](#common.TreeFragment) can be passed in to - make the parse incremental. - - By default, the entire input is parsed. You can pass `ranges`, - which should be a sorted array of non-empty, non-overlapping - ranges, to parse only those ranges. The tree returned in that - case will start at `ranges[0].from`. + Whether this range covers the positions on its sides. This + influences whether new content becomes part of the range and + whether the cursor can be drawn on its sides. Defaults to false + for inline replacements, and true for block replacements. */ - startParse(input: Input | string, fragments?: readonly TreeFragment[], ranges?: readonly { - from: number; - to: number; - }[]): PartialParse; + inclusive?: boolean; /** - Run a full parse, returning the resulting tree. + Set inclusivity at the start. */ - parse(input: Input | string, fragments?: readonly TreeFragment[], ranges?: readonly { - from: number; - to: number; - }[]): Tree; -} -/** -This is the interface parsers use to access the document. To run -Lezer directly on your own document data structure, you have to -write an implementation of it. -*/ -interface Input { - /** - The length of the document. - */ - readonly length: number; + inclusiveStart?: boolean; /** - Get the chunk after the given position. The returned string - should start at `from` and, if that isn't the end of the - document, may be of any length greater than zero. + Set inclusivity at the end. */ - chunk(from: number): string; + inclusiveEnd?: boolean; /** - Indicates whether the chunks already end at line breaks, so that - client code that wants to work by-line can avoid re-scanning - them for line breaks. When this is true, the result of `chunk()` - should either be a single line break, or the content between - `from` and the next line break. + Whether this is a block-level decoration. Defaults to false. */ - readonly lineChunks: boolean; + block?: boolean; /** - Read the part of the document between the given positions. + Other properties are allowed. */ - read(from: number, to: number): string; + [other: string]: any; } -/** -Parse wrapper functions are supported by some parsers to inject -additional parsing logic. -*/ -type ParseWrapper = (inner: PartialParse, input: Input, fragments: readonly TreeFragment[], ranges: readonly { - from: number; - to: number; -}[]) => PartialParse; -/** -Each [node type](#common.NodeType) or [individual tree](#common.Tree) -can have metadata associated with it in props. Instances of this -class represent prop names. -*/ -declare class NodeProp { +interface LineDecorationSpec { /** - Indicates whether this prop is stored per [node - type](#common.NodeType) or per [tree node](#common.Tree). + DOM attributes to add to the element wrapping the line. */ - perNode: boolean; + attributes?: { + [key: string]: string; + }; /** - A method that deserializes a value of this prop from a string. - Can be used to allow a prop to be directly written in a grammar - file. + Shorthand for `{attributes: {class: value}}`. */ - deserialize: (str: string) => T; + class?: string; /** - Create a new node prop type. + Other properties are allowed. */ - constructor(config?: { - /** - The [deserialize](#common.NodeProp.deserialize) function to - use for this prop, used for example when directly providing - the prop from a grammar file. Defaults to a function that - raises an error. - */ - deserialize?: (str: string) => T; - /** - By default, node props are stored in the [node - type](#common.NodeType). It can sometimes be useful to directly - store information (usually related to the parsing algorithm) - in [nodes](#common.Tree) themselves. Set this to true to enable - that for this prop. - */ - perNode?: boolean; - }); + [other: string]: any; +} +/** +Widgets added to the content are described by subclasses of this +class. Using a description object like that makes it possible to +delay creating of the DOM structure for a widget until it is +needed, and to avoid redrawing widgets even if the decorations +that define them are recreated. +*/ +declare abstract class WidgetType { /** - This is meant to be used with - [`NodeSet.extend`](#common.NodeSet.extend) or - [`LRParser.configure`](#lr.ParserConfig.props) to compute - prop values for each node type in the set. Takes a [match - object](#common.NodeType^match) or function that returns undefined - if the node type doesn't get this prop, and the prop's value if - it does. + Build the DOM structure for this widget instance. */ - add(match: { - [selector: string]: T; - } | ((type: NodeType) => T | undefined)): NodePropSource; + abstract toDOM(view: EditorView): HTMLElement; /** - Prop that is used to describe matching delimiters. For opening - delimiters, this holds an array of node names (written as a - space-separated string when declaring this prop in a grammar) - for the node types of closing delimiters that match it. + Compare this instance to another instance of the same type. + (TypeScript can't express this, but only instances of the same + specific class will be passed to this method.) This is used to + avoid redrawing widgets when they are replaced by a new + decoration of the same type. The default implementation just + returns `false`, which will cause new instances of the widget to + always be redrawn. */ - static closedBy: NodeProp; + eq(widget: WidgetType): boolean; /** - The inverse of [`closedBy`](#common.NodeProp^closedBy). This is - attached to closing delimiters, holding an array of node names - of types of matching opening delimiters. + Update a DOM element created by a widget of the same type (but + different, non-`eq` content) to reflect this widget. May return + true to indicate that it could update, false to indicate it + couldn't (in which case the widget will be redrawn). The default + implementation just returns false. */ - static openedBy: NodeProp; + updateDOM(dom: HTMLElement, view: EditorView): boolean; /** - Used to assign node types to groups (for example, all node - types that represent an expression could be tagged with an - `"Expression"` group). + The estimated height this widget will have, to be used when + estimating the height of content that hasn't been drawn. May + return -1 to indicate you don't know. The default implementation + returns -1. */ - static group: NodeProp; + get estimatedHeight(): number; /** - Attached to nodes to indicate these should be - [displayed](https://codemirror.net/docs/ref/#language.syntaxTree) - in a bidirectional text isolate, so that direction-neutral - characters on their sides don't incorrectly get associated with - surrounding text. You'll generally want to set this for nodes - that contain arbitrary text, like strings and comments, and for - nodes that appear _inside_ arbitrary text, like HTML tags. When - not given a value, in a grammar declaration, defaults to - `"auto"`. + For inline widgets that are displayed inline (as opposed to + `inline-block`) and introduce line breaks (through `
` tags + or textual newlines), this must indicate the amount of line + breaks they introduce. Defaults to 0. */ - static isolate: NodeProp<"rtl" | "ltr" | "auto">; + get lineBreaks(): number; /** - The hash of the [context](#lr.ContextTracker.constructor) - that the node was parsed in, if any. Used to limit reuse of - contextual nodes. + Can be used to configure which kinds of events inside the widget + should be ignored by the editor. The default is to ignore all + events. */ - static contextHash: NodeProp; + ignoreEvent(event: Event): boolean; /** - The distance beyond the end of the node that the tokenizer - looked ahead for any of the tokens inside the node. (The LR - parser only stores this when it is larger than 25, for - efficiency reasons.) + Override the way screen coordinates for positions at/in the + widget are found. `pos` will be the offset into the widget, and + `side` the side of the position that is being queried—less than + zero for before, greater than zero for after, and zero for + directly at that position. */ - static lookAhead: NodeProp; + coordsAt(dom: HTMLElement, pos: number, side: number): Rect | null; /** - This per-node prop is used to replace a given node, or part of a - node, with another tree. This is useful to include trees from - different languages in mixed-language parsers. + This is called when the an instance of the widget is removed + from the editor view. */ - static mounted: NodeProp; + destroy(dom: HTMLElement): void; } /** -A mounted tree, which can be [stored](#common.NodeProp^mounted) on -a tree node to indicate that parts of its content are -represented by another tree. +A decoration set represents a collection of decorated ranges, +organized for efficient access and mapping. See +[`RangeSet`](https://codemirror.net/6/docs/ref/#state.RangeSet) for its methods. */ -declare class MountedTree { - /** - The inner tree. - */ - readonly tree: Tree; - /** - If this is null, this tree replaces the entire node (it will - be included in the regular iteration instead of its host - node). If not, only the given ranges are considered to be - covered by this tree. This is used for trees that are mixed in - a way that isn't strictly hierarchical. Such mounted trees are - only entered by [`resolveInner`](#common.Tree.resolveInner) - and [`enter`](#common.SyntaxNode.enter). - */ - readonly overlay: readonly { - from: number; - to: number; - }[] | null; +type DecorationSet = RangeSet; +/** +The different types of blocks that can occur in an editor view. +*/ +declare enum BlockType { /** - The parser used to create this subtree. + A line of text. */ - readonly parser: Parser; - constructor( + Text = 0, /** - The inner tree. + A block widget associated with the position after it. */ - tree: Tree, + WidgetBefore = 1, /** - If this is null, this tree replaces the entire node (it will - be included in the regular iteration instead of its host - node). If not, only the given ranges are considered to be - covered by this tree. This is used for trees that are mixed in - a way that isn't strictly hierarchical. Such mounted trees are - only entered by [`resolveInner`](#common.Tree.resolveInner) - and [`enter`](#common.SyntaxNode.enter). + A block widget associated with the position before it. */ - overlay: readonly { - from: number; - to: number; - }[] | null, + WidgetAfter = 2, /** - The parser used to create this subtree. + A block widget [replacing](https://codemirror.net/6/docs/ref/#view.Decoration^replace) a range of content. */ - parser: Parser); + WidgetRange = 3 } /** -Type returned by [`NodeProp.add`](#common.NodeProp.add). Describes -whether a prop should be added to a given node type in a node set, -and what value it should have. -*/ -type NodePropSource = (type: NodeType) => null | [NodeProp, any]; -/** -Each node in a syntax tree has a node type associated with it. +A decoration provides information on how to draw or style a piece +of content. You'll usually use it wrapped in a +[`Range`](https://codemirror.net/6/docs/ref/#state.Range), which adds a start and end position. +@nonabstract */ -declare class NodeType { +declare abstract class Decoration extends RangeValue { /** - The name of the node type. Not necessarily unique, but if the - grammar was written properly, different node types with the - same name within a node set should play the same semantic - role. + The config object used to create this decoration. You can + include additional properties in there to store metadata about + your decoration. */ - readonly name: string; + readonly spec: any; + protected constructor( /** - The id of this node in its set. Corresponds to the term ids - used in the parser. + @internal */ - readonly id: number; + startSide: number, /** - Define a node type. + @internal */ - static define(spec: { - /** - The ID of the node type. When this type is used in a - [set](#common.NodeSet), the ID must correspond to its index in - the type array. - */ - id: number; - /** - The name of the node type. Leave empty to define an anonymous - node. - */ - name?: string; - /** - [Node props](#common.NodeProp) to assign to the type. The value - given for any given prop should correspond to the prop's type. - */ - props?: readonly ([NodeProp, any] | NodePropSource)[]; - /** - Whether this is a [top node](#common.NodeType.isTop). - */ - top?: boolean; - /** - Whether this node counts as an [error - node](#common.NodeType.isError). - */ - error?: boolean; - /** - Whether this node is a [skipped](#common.NodeType.isSkipped) - node. - */ - skipped?: boolean; - }): NodeType; + endSide: number, /** - Retrieves a node prop for this type. Will return `undefined` if - the prop isn't present on this node. + @internal */ - prop(prop: NodeProp): T | undefined; + widget: WidgetType | null, /** - True when this is the top node of a grammar. + The config object used to create this decoration. You can + include additional properties in there to store metadata about + your decoration. */ - get isTop(): boolean; + spec: any); + abstract eq(other: Decoration): boolean; /** - True when this node is produced by a skip rule. + Create a mark decoration, which influences the styling of the + content in its range. Nested mark decorations will cause nested + DOM elements to be created. Nesting order is determined by + precedence of the [facet](https://codemirror.net/6/docs/ref/#view.EditorView^decorations), with + the higher-precedence decorations creating the inner DOM nodes. + Such elements are split on line boundaries and on the boundaries + of lower-precedence decorations. */ - get isSkipped(): boolean; + static mark(spec: MarkDecorationSpec): Decoration; /** - Indicates whether this is an error node. + Create a widget decoration, which displays a DOM element at the + given position. */ - get isError(): boolean; + static widget(spec: WidgetDecorationSpec): Decoration; /** - When true, this node type doesn't correspond to a user-declared - named node, for example because it is used to cache repetition. + Create a replace decoration which replaces the given range with + a widget, or simply hides it. */ - get isAnonymous(): boolean; + static replace(spec: ReplaceDecorationSpec): Decoration; /** - Returns true when this node's name or one of its - [groups](#common.NodeProp^group) matches the given string. + Create a line decoration, which can add DOM attributes to the + line starting at the given position. */ - is(name: string | number): boolean; + static line(spec: LineDecorationSpec): Decoration; /** - An empty dummy node type to use when no actual type is available. + Build a [`DecorationSet`](https://codemirror.net/6/docs/ref/#view.DecorationSet) from the given + decorated range or ranges. If the ranges aren't already sorted, + pass `true` for `sort` to make the library sort them for you. */ - static none: NodeType; + static set(of: Range | readonly Range[], sort?: boolean): DecorationSet; /** - Create a function from node types to arbitrary values by - specifying an object whose property names are node or - [group](#common.NodeProp^group) names. Often useful with - [`NodeProp.add`](#common.NodeProp.add). You can put multiple - names, separated by spaces, in a single property name to map - multiple node names to a single value. + The empty set of decorations. */ - static match(map: { - [selector: string]: T; - }): (node: NodeType) => T | undefined; + static none: DecorationSet; } + /** -A node set holds a collection of node types. It is used to -compactly represent trees by storing their type ids, rather than a -full pointer to the type object, in a numeric array. Each parser -[has](#lr.LRParser.nodeSet) a node set, and [tree -buffers](#common.TreeBuffer) can only store collections of nodes -from the same set. A set can have a maximum of 2**16 (65536) node -types in it, so that the ids fit into 16-bit typed array slots. +Command functions are used in key bindings and other types of user +actions. Given an editor view, they check whether their effect can +apply to the editor, and if it can, perform it as a side effect +(which usually means [dispatching](https://codemirror.net/6/docs/ref/#view.EditorView.dispatch) a +transaction) and return `true`. */ -declare class NodeSet { - /** - The node types in this set, by id. - */ - readonly types: readonly NodeType[]; +type Command = (target: EditorView) => boolean; +declare class ScrollTarget { + readonly range: SelectionRange; + readonly y: ScrollStrategy; + readonly x: ScrollStrategy; + readonly yMargin: number; + readonly xMargin: number; + readonly isSnapshot: boolean; + constructor(range: SelectionRange, y?: ScrollStrategy, x?: ScrollStrategy, yMargin?: number, xMargin?: number, isSnapshot?: boolean); + map(changes: ChangeDesc): ScrollTarget; + clip(state: EditorState): ScrollTarget; +} +/** +This is the interface plugin objects conform to. +*/ +interface PluginValue extends Object { /** - Create a set with the given types. The `id` property of each - type should correspond to its position within the array. + Notifies the plugin of an update that happened in the view. This + is called _before_ the view updates its own DOM. It is + responsible for updating the plugin's internal state (including + any state that may be read by plugin fields) and _writing_ to + the DOM for the changes in the update. To avoid unnecessary + layout recomputations, it should _not_ read the DOM layout—use + [`requestMeasure`](https://codemirror.net/6/docs/ref/#view.EditorView.requestMeasure) to schedule + your code in a DOM reading phase if you need to. */ - constructor( + update?(update: ViewUpdate): void; /** - The node types in this set, by id. + Called when the document view is updated (due to content, + decoration, or viewport changes). Should not try to immediately + start another view update. Often useful for calling + [`requestMeasure`](https://codemirror.net/6/docs/ref/#view.EditorView.requestMeasure). */ - types: readonly NodeType[]); + docViewUpdate?(view: EditorView): void; /** - Create a copy of this set with some node properties added. The - arguments to this method can be created with - [`NodeProp.add`](#common.NodeProp.add). + Called when the plugin is no longer going to be used. Should + revert any changes the plugin made to the DOM. */ - extend(...props: NodePropSource[]): NodeSet; + destroy?(): void; } /** -Options that control iteration. Can be combined with the `|` -operator to enable multiple ones. +Provides additional information when defining a [view +plugin](https://codemirror.net/6/docs/ref/#view.ViewPlugin). */ -declare enum IterMode { +interface PluginSpec { /** - When enabled, iteration will only visit [`Tree`](#common.Tree) - objects, not nodes packed into - [`TreeBuffer`](#common.TreeBuffer)s. + Register the given [event + handlers](https://codemirror.net/6/docs/ref/#view.EditorView^domEventHandlers) for the plugin. + When called, these will have their `this` bound to the plugin + value. */ - ExcludeBuffers = 1, + eventHandlers?: DOMEventHandlers; /** - Enable this to make iteration include anonymous nodes (such as - the nodes that wrap repeated grammar constructs into a balanced - tree). + Registers [event observers](https://codemirror.net/6/docs/ref/#view.EditorView^domEventObservers) + for the plugin. Will, when called, have their `this` bound to + the plugin value. */ - IncludeAnonymous = 2, + eventObservers?: DOMEventHandlers; /** - By default, regular [mounted](#common.NodeProp^mounted) nodes - replace their base node in iteration. Enable this to ignore them - instead. + Specify that the plugin provides additional extensions when + added to an editor configuration. */ - IgnoreMounts = 4, + provide?: (plugin: ViewPlugin) => Extension; /** - This option only applies in - [`enter`](#common.SyntaxNode.enter)-style methods. It tells the - library to not enter mounted overlays if one covers the given - position. + Allow the plugin to provide decorations. When given, this should + be a function that take the plugin value and return a + [decoration set](https://codemirror.net/6/docs/ref/#view.DecorationSet). See also the caveat about + [layout-changing decorations](https://codemirror.net/6/docs/ref/#view.EditorView^decorations) that + depend on the view. */ - IgnoreOverlays = 8 + decorations?: (value: V) => DecorationSet; } /** -A piece of syntax tree. There are two ways to approach these -trees: the way they are actually stored in memory, and the -convenient way. - -Syntax trees are stored as a tree of `Tree` and `TreeBuffer` -objects. By packing detail information into `TreeBuffer` leaf -nodes, the representation is made a lot more memory-efficient. - -However, when you want to actually work with tree nodes, this -representation is very awkward, so most client code will want to -use the [`TreeCursor`](#common.TreeCursor) or -[`SyntaxNode`](#common.SyntaxNode) interface instead, which provides -a view on some part of this data structure, and can be used to -move around to adjacent nodes. +View plugins associate stateful values with a view. They can +influence the way the content is drawn, and are notified of things +that happen in the view. */ -declare class Tree { +declare class ViewPlugin { /** - The type of the top node. + Instances of this class act as extensions. */ - readonly type: NodeType; + extension: Extension; + private constructor(); /** - This node's child nodes. + Define a plugin from a constructor function that creates the + plugin's value, given an editor view. */ - readonly children: readonly (Tree | TreeBuffer)[]; + static define(create: (view: EditorView) => V, spec?: PluginSpec): ViewPlugin; /** - The positions (offsets relative to the start of this tree) of - the children. + Create a plugin for a class whose constructor takes a single + editor view as argument. */ - readonly positions: readonly number[]; + static fromClass(cls: { + new (view: EditorView): V; + }, spec?: PluginSpec): ViewPlugin; +} +interface MeasureRequest { /** - The total length of this tree + Called in a DOM read phase to gather information that requires + DOM layout. Should _not_ mutate the document. */ - readonly length: number; + read(view: EditorView): T; /** - Construct a new tree. See also [`Tree.build`](#common.Tree^build). + Called in a DOM write phase to update the document. Should _not_ + do anything that triggers DOM layout. */ - constructor( + write?(measure: T, view: EditorView): void; /** - The type of the top node. + When multiple requests with the same key are scheduled, only the + last one will actually be run. */ - type: NodeType, + key?: any; +} +type AttrSource = Attrs | ((view: EditorView) => Attrs | null); +/** +View [plugins](https://codemirror.net/6/docs/ref/#view.ViewPlugin) are given instances of this +class, which describe what happened, whenever the view is updated. +*/ +declare class ViewUpdate { /** - This node's child nodes. + The editor view that the update is associated with. */ - children: readonly (Tree | TreeBuffer)[], + readonly view: EditorView; /** - The positions (offsets relative to the start of this tree) of - the children. + The new editor state. */ - positions: readonly number[], + readonly state: EditorState; /** - The total length of this tree - */ - length: number, - /** - Per-node [node props](#common.NodeProp) to associate with this node. - */ - props?: readonly [NodeProp | number, any][]); - /** - The empty tree - */ - static empty: Tree; - /** - Get a [tree cursor](#common.TreeCursor) positioned at the top of - the tree. Mode can be used to [control](#common.IterMode) which - nodes the cursor visits. + The transactions involved in the update. May be empty. */ - cursor(mode?: IterMode): TreeCursor; + readonly transactions: readonly Transaction[]; /** - Get a [tree cursor](#common.TreeCursor) pointing into this tree - at the given position and side (see - [`moveTo`](#common.TreeCursor.moveTo). + The changes made to the document by this update. */ - cursorAt(pos: number, side?: -1 | 0 | 1, mode?: IterMode): TreeCursor; + readonly changes: ChangeSet; /** - Get a [syntax node](#common.SyntaxNode) object for the top of the - tree. + The previous editor state. */ - get topNode(): SyntaxNode; + readonly startState: EditorState; + private constructor(); /** - Get the [syntax node](#common.SyntaxNode) at the given position. - If `side` is -1, this will move into nodes that end at the - position. If 1, it'll move into nodes that start at the - position. With 0, it'll only enter nodes that cover the position - from both sides. - - Note that this will not enter - [overlays](#common.MountedTree.overlay), and you often want - [`resolveInner`](#common.Tree.resolveInner) instead. + Tells you whether the [viewport](https://codemirror.net/6/docs/ref/#view.EditorView.viewport) or + [visible ranges](https://codemirror.net/6/docs/ref/#view.EditorView.visibleRanges) changed in this + update. */ - resolve(pos: number, side?: -1 | 0 | 1): SyntaxNode; + get viewportChanged(): boolean; /** - Like [`resolve`](#common.Tree.resolve), but will enter - [overlaid](#common.MountedTree.overlay) nodes, producing a syntax node - pointing into the innermost overlaid tree at the given position - (with parent links going through all parent structure, including - the host trees). + Indicates whether the height of a block element in the editor + changed in this update. */ - resolveInner(pos: number, side?: -1 | 0 | 1): SyntaxNode; + get heightChanged(): boolean; /** - In some situations, it can be useful to iterate through all - nodes around a position, including those in overlays that don't - directly cover the position. This method gives you an iterator - that will produce all nodes, from small to big, around the given - position. + Returns true when the document was modified or the size of the + editor, or elements within the editor, changed. */ - resolveStack(pos: number, side?: -1 | 0 | 1): NodeIterator; + get geometryChanged(): boolean; /** - Iterate over the tree and its children, calling `enter` for any - node that touches the `from`/`to` region (if given) before - running over such a node's children, and `leave` (if given) when - leaving the node. When `enter` returns `false`, that node will - not have its children iterated over (or `leave` called). + True when this update indicates a focus change. */ - iterate(spec: { - enter(node: SyntaxNodeRef): boolean | void; - leave?(node: SyntaxNodeRef): void; - from?: number; - to?: number; - mode?: IterMode; - }): void; + get focusChanged(): boolean; /** - Get the value of the given [node prop](#common.NodeProp) for this - node. Works with both per-node and per-type props. + Whether the document changed in this update. */ - prop(prop: NodeProp): T | undefined; + get docChanged(): boolean; /** - Returns the node's [per-node props](#common.NodeProp.perNode) in a - format that can be passed to the [`Tree`](#common.Tree) - constructor. + Whether the selection was explicitly set in this update. */ - get propValues(): readonly [NodeProp | number, any][]; + get selectionSet(): boolean; +} + +/** +Interface that objects registered with +[`EditorView.mouseSelectionStyle`](https://codemirror.net/6/docs/ref/#view.EditorView^mouseSelectionStyle) +must conform to. +*/ +interface MouseSelectionStyle { /** - Balance the direct children of this tree, producing a copy of - which may have children grouped into subtrees with type - [`NodeType.none`](#common.NodeType^none). + Return a new selection for the mouse gesture that starts with + the event that was originally given to the constructor, and ends + with the event passed here. In case of a plain click, those may + both be the `mousedown` event, in case of a drag gesture, the + latest `mousemove` event will be passed. + + When `extend` is true, that means the new selection should, if + possible, extend the start selection. If `multiple` is true, the + new selection should be added to the original selection. */ - balance(config?: { - /** - Function to create the newly balanced subtrees. - */ - makeTree?: (children: readonly (Tree | TreeBuffer)[], positions: readonly number[], length: number) => Tree; - }): Tree; + get: (curEvent: MouseEvent, extend: boolean, multiple: boolean) => EditorSelection; /** - Build a tree from a postfix-ordered buffer of node information, - or a cursor over such a buffer. + Called when the view is updated while the gesture is in + progress. When the document changes, it may be necessary to map + some data (like the original selection or start position) + through the changes. + + This may return `true` to indicate that the `get` method should + get queried again after the update, because something in the + update could change its result. Be wary of infinite loops when + using this (where `get` returns a new selection, which will + trigger `update`, which schedules another `get` in response). */ - static build(data: BuildData): Tree; + update: (update: ViewUpdate) => boolean | void; } +type MakeSelectionStyle = (view: EditorView, event: MouseEvent) => MouseSelectionStyle | null; + /** -Represents a sequence of nodes. +Record used to represent information about a block-level element +in the editor view. */ -type NodeIterator = { - node: SyntaxNode; - next: NodeIterator | null; -}; -type BuildData = { +declare class BlockInfo { /** - The buffer or buffer cursor to read the node data from. - - When this is an array, it should contain four values for every - node in the tree. - - - The first holds the node's type, as a node ID pointing into - the given `NodeSet`. - - The second holds the node's start offset. - - The third the end offset. - - The fourth the amount of space taken up in the array by this - node and its children. Since there's four values per node, - this is the total number of nodes inside this node (children - and transitive children) plus one for the node itself, times - four. - - Parent nodes should appear _after_ child nodes in the array. As - an example, a node of type 10 spanning positions 0 to 4, with - two children, of type 11 and 12, might look like this: - - [11, 0, 1, 4, 12, 2, 4, 4, 10, 0, 4, 12] + The start of the element in the document. */ - buffer: BufferCursor | readonly number[]; + readonly from: number; /** - The node types to use. + The length of the element. */ - nodeSet: NodeSet; + readonly length: number; /** - The id of the top node type. + The top position of the element (relative to the top of the + document). */ - topID: number; + readonly top: number; /** - The position the tree should start at. Defaults to 0. + Its height. */ - start?: number; + readonly height: number; /** - The position in the buffer where the function should stop - reading. Defaults to 0. + The type of element this is. When querying lines, this may be + an array of all the blocks that make up the line. */ - bufferStart?: number; + get type(): BlockType | readonly BlockInfo[]; /** - The length of the wrapping node. The end offset of the last - child is used when not provided. + The end of the element as a document position. */ - length?: number; + get to(): number; /** - The maximum buffer length to use. Defaults to - [`DefaultBufferLength`](#common.DefaultBufferLength). + The bottom position of the element. */ - maxBufferLength?: number; + get bottom(): number; /** - An optional array holding reused nodes that the buffer can refer - to. + If this is a widget block, this will return the widget + associated with it. */ - reused?: readonly Tree[]; + get widget(): WidgetType | null; /** - The first node type that indicates repeat constructs in this - grammar. + If this is a textblock, this holds the number of line breaks + that appear in widgets inside the block. */ - minRepeatType?: number; -}; + get widgetLineBreaks(): number; +} + /** -This is used by `Tree.build` as an abstraction for iterating over -a tree buffer. A cursor initially points at the very last element -in the buffer. Every time `next()` is called it moves on to the -previous one. +The type of object given to the [`EditorView`](https://codemirror.net/6/docs/ref/#view.EditorView) +constructor. */ -interface BufferCursor { +interface EditorViewConfig extends EditorStateConfig { /** - The current buffer position (four times the number of nodes - remaining). + The view's initial state. If not given, a new state is created + by passing this configuration object to + [`EditorState.create`](https://codemirror.net/6/docs/ref/#state.EditorState^create), using its + `doc`, `selection`, and `extensions` field (if provided). */ - pos: number; + state?: EditorState; /** - The node ID of the next node in the buffer. + When given, the editor is immediately appended to the given + element on creation. (Otherwise, you'll have to place the view's + [`dom`](https://codemirror.net/6/docs/ref/#view.EditorView.dom) element in the document yourself.) */ - id: number; + parent?: Element | DocumentFragment; /** - The start position of the next node in the buffer. + If the view is going to be mounted in a shadow root or document + other than the one held by the global variable `document` (the + default), you should pass it here. If you provide `parent`, but + not this option, the editor will automatically look up a root + from the parent. */ - start: number; + root?: Document | ShadowRoot; /** - The end position of the next node. + Pass an effect created with + [`EditorView.scrollIntoView`](https://codemirror.net/6/docs/ref/#view.EditorView^scrollIntoView) or + [`EditorView.scrollSnapshot`](https://codemirror.net/6/docs/ref/#view.EditorView.scrollSnapshot) + here to set an initial scroll position. */ - end: number; + scrollTo?: StateEffect; /** - The size of the next node (the number of nodes inside, counting - the node itself, times 4). + Override the way transactions are + [dispatched](https://codemirror.net/6/docs/ref/#view.EditorView.dispatch) for this editor view. + Your implementation, if provided, should probably call the + view's [`update` method](https://codemirror.net/6/docs/ref/#view.EditorView.update). */ - size: number; + dispatchTransactions?: (trs: readonly Transaction[], view: EditorView) => void; /** - Moves `this.pos` down by 4. + **Deprecated** single-transaction version of + `dispatchTransactions`. Will force transactions to be dispatched + one at a time when used. */ - next(): void; - /** - Create a copy of this cursor. - */ - fork(): BufferCursor; + dispatch?: (tr: Transaction, view: EditorView) => void; } /** -Tree buffers contain (type, start, end, endIndex) quads for each -node. In such a buffer, nodes are stored in prefix order (parents -before children, with the endIndex of the parent indicating which -children belong to it). +An editor view represents the editor's user interface. It holds +the editable DOM surface, and possibly other elements such as the +line number gutter. It handles events and dispatches state +transactions for editing actions. */ -declare class TreeBuffer { - /** - The buffer's content. - */ - readonly buffer: Uint16Array; +declare class EditorView { /** - The total length of the group of nodes in the buffer. + The current editor state. */ - readonly length: number; + get state(): EditorState; /** - The node set used in this buffer. + To be able to display large documents without consuming too much + memory or overloading the browser, CodeMirror only draws the + code that is visible (plus a margin around it) to the DOM. This + property tells you the extent of the current drawn viewport, in + document positions. */ - readonly set: NodeSet; + get viewport(): { + from: number; + to: number; + }; /** - Create a tree buffer. + When there are, for example, large collapsed ranges in the + viewport, its size can be a lot bigger than the actual visible + content. Thus, if you are doing something like styling the + content in the viewport, it is preferable to only do so for + these ranges, which are the subset of the viewport that is + actually drawn. */ - constructor( + get visibleRanges(): readonly { + from: number; + to: number; + }[]; /** - The buffer's content. + Returns false when the editor is entirely scrolled out of view + or otherwise hidden. */ - buffer: Uint16Array, + get inView(): boolean; /** - The total length of the group of nodes in the buffer. + Indicates whether the user is currently composing text via + [IME](https://en.wikipedia.org/wiki/Input_method), and at least + one change has been made in the current composition. */ - length: number, + get composing(): boolean; /** - The node set used in this buffer. + Indicates whether the user is currently in composing state. Note + that on some platforms, like Android, this will be the case a + lot, since just putting the cursor on a word starts a + composition there. */ - set: NodeSet); -} -/** -The set of properties provided by both [`SyntaxNode`](#common.SyntaxNode) -and [`TreeCursor`](#common.TreeCursor). Note that, if you need -an object that is guaranteed to stay stable in the future, you -need to use the [`node`](#common.SyntaxNodeRef.node) accessor. -*/ -interface SyntaxNodeRef { + get compositionStarted(): boolean; + private dispatchTransactions; + private _root; /** - The start position of the node. + The document or shadow root that the view lives in. */ - readonly from: number; + get root(): DocumentOrShadowRoot; /** - The end position of the node. + The DOM element that wraps the entire editor view. */ - readonly to: number; + readonly dom: HTMLElement; /** - The type of the node. + The DOM element that can be styled to scroll. (Note that it may + not have been, so you can't assume this is scrollable.) */ - readonly type: NodeType; + readonly scrollDOM: HTMLElement; /** - The name of the node (`.type.name`). + The editable DOM element holding the editor content. You should + not, usually, interact with this content directly though the + DOM, since the editor will immediately undo most of the changes + you make. Instead, [dispatch](https://codemirror.net/6/docs/ref/#view.EditorView.dispatch) + [transactions](https://codemirror.net/6/docs/ref/#state.Transaction) to modify content, and + [decorations](https://codemirror.net/6/docs/ref/#view.Decoration) to style it. */ - readonly name: string; + readonly contentDOM: HTMLElement; + private announceDOM; + private plugins; + private pluginMap; + private editorAttrs; + private contentAttrs; + private styleModules; + private bidiCache; + private destroyed; /** - Get the [tree](#common.Tree) that represents the current node, - if any. Will return null when the node is in a [tree - buffer](#common.TreeBuffer). + Construct a new view. You'll want to either provide a `parent` + option, or put `view.dom` into your document after creating a + view, so that the user can see the editor. */ - readonly tree: Tree | null; + constructor(config?: EditorViewConfig); /** - Retrieve a stable [syntax node](#common.SyntaxNode) at this - position. + All regular editor state updates should go through this. It + takes a transaction, array of transactions, or transaction spec + and updates the view to show the new state produced by that + transaction. Its implementation can be overridden with an + [option](https://codemirror.net/6/docs/ref/#view.EditorView.constructor^config.dispatchTransactions). + This function is bound to the view instance, so it does not have + to be called as a method. + + Note that when multiple `TransactionSpec` arguments are + provided, these define a single transaction (the specs will be + merged), not a sequence of transactions. */ - readonly node: SyntaxNode; + dispatch(tr: Transaction): void; + dispatch(trs: readonly Transaction[]): void; + dispatch(...specs: TransactionSpec[]): void; /** - Test whether the node matches a given context—a sequence of - direct parent nodes. Empty strings in the context array act as - wildcards, other strings must match the ancestor node's name. + Update the view for the given array of transactions. This will + update the visible document and selection to match the state + produced by the transactions, and notify view plugins of the + change. You should usually call + [`dispatch`](https://codemirror.net/6/docs/ref/#view.EditorView.dispatch) instead, which uses this + as a primitive. */ - matchContext(context: readonly string[]): boolean; -} -/** -A syntax node provides an immutable pointer to a given node in a -tree. When iterating over large amounts of nodes, you may want to -use a mutable [cursor](#common.TreeCursor) instead, which is more -efficient. -*/ -interface SyntaxNode extends SyntaxNodeRef { + update(transactions: readonly Transaction[]): void; /** - The node's parent node, if any. + Reset the view to the given state. (This will cause the entire + document to be redrawn and all view plugins to be reinitialized, + so you should probably only use it when the new state isn't + derived from the old state. Otherwise, use + [`dispatch`](https://codemirror.net/6/docs/ref/#view.EditorView.dispatch) instead.) */ - parent: SyntaxNode | null; + setState(newState: EditorState): void; + private updatePlugins; + private docViewUpdate; /** - The first child, if the node has children. + Get the CSS classes for the currently active editor themes. */ - firstChild: SyntaxNode | null; + get themeClasses(): string; + private updateAttrs; + private showAnnouncements; + private mountStyles; + private readMeasured; /** - The node's last child, if available. + Schedule a layout measurement, optionally providing callbacks to + do custom DOM measuring followed by a DOM write phase. Using + this is preferable reading DOM layout directly from, for + example, an event handler, because it'll make sure measuring and + drawing done by other components is synchronized, avoiding + unnecessary DOM layout computations. */ - lastChild: SyntaxNode | null; + requestMeasure(request?: MeasureRequest): void; /** - The first child that ends after `pos`. + Get the value of a specific plugin, if present. Note that + plugins that crash can be dropped from a view, so even when you + know you registered a given plugin, it is recommended to check + the return value of this method. */ - childAfter(pos: number): SyntaxNode | null; + plugin(plugin: ViewPlugin): T | null; /** - The last child that starts before `pos`. + The top position of the document, in screen coordinates. This + may be negative when the editor is scrolled down. Points + directly to the top of the first line, not above the padding. */ - childBefore(pos: number): SyntaxNode | null; + get documentTop(): number; /** - Enter the child at the given position. If side is -1 the child - may end at that position, when 1 it may start there. - - This will by default enter - [overlaid](#common.MountedTree.overlay) - [mounted](#common.NodeProp^mounted) trees. You can set - `overlays` to false to disable that. - - Similarly, when `buffers` is false this will not enter - [buffers](#common.TreeBuffer), only [nodes](#common.Tree) (which - is mostly useful when looking for props, which cannot exist on - buffer-allocated nodes). + Reports the padding above and below the document. */ - enter(pos: number, side: -1 | 0 | 1, mode?: IterMode): SyntaxNode | null; + get documentPadding(): { + top: number; + bottom: number; + }; /** - This node's next sibling, if any. + If the editor is transformed with CSS, this provides the scale + along the X axis. Otherwise, it will just be 1. Note that + transforms other than translation and scaling are not supported. */ - nextSibling: SyntaxNode | null; + get scaleX(): number; /** - This node's previous sibling. + Provide the CSS transformed scale along the Y axis. */ - prevSibling: SyntaxNode | null; + get scaleY(): number; /** - A [tree cursor](#common.TreeCursor) starting at this node. + Find the text line or block widget at the given vertical + position (which is interpreted as relative to the [top of the + document](https://codemirror.net/6/docs/ref/#view.EditorView.documentTop)). */ - cursor(mode?: IterMode): TreeCursor; + elementAtHeight(height: number): BlockInfo; /** - Find the node around, before (if `side` is -1), or after (`side` - is 1) the given position. Will look in parent nodes if the - position is outside this node. + Find the line block (see + [`lineBlockAt`](https://codemirror.net/6/docs/ref/#view.EditorView.lineBlockAt) at the given + height, again interpreted relative to the [top of the + document](https://codemirror.net/6/docs/ref/#view.EditorView.documentTop). */ - resolve(pos: number, side?: -1 | 0 | 1): SyntaxNode; + lineBlockAtHeight(height: number): BlockInfo; /** - Similar to `resolve`, but enter - [overlaid](#common.MountedTree.overlay) nodes. + Get the extent and vertical position of all [line + blocks](https://codemirror.net/6/docs/ref/#view.EditorView.lineBlockAt) in the viewport. Positions + are relative to the [top of the + document](https://codemirror.net/6/docs/ref/#view.EditorView.documentTop); */ - resolveInner(pos: number, side?: -1 | 0 | 1): SyntaxNode; + get viewportLineBlocks(): BlockInfo[]; /** - Move the position to the innermost node before `pos` that looks - like it is unfinished (meaning it ends in an error node or has a - child ending in an error node right at its end). + Find the line block around the given document position. A line + block is a range delimited on both sides by either a + non-[hidden](https://codemirror.net/6/docs/ref/#view.Decoration^replace) line breaks, or the + start/end of the document. It will usually just hold a line of + text, but may be broken into multiple textblocks by block + widgets. */ - enterUnfinishedNodesBefore(pos: number): SyntaxNode; + lineBlockAt(pos: number): BlockInfo; /** - Get a [tree](#common.Tree) for this node. Will allocate one if it - points into a buffer. + The editor's total content height. */ - toTree(): Tree; + get contentHeight(): number; /** - Get the first child of the given type (which may be a [node - name](#common.NodeType.name) or a [group - name](#common.NodeProp^group)). If `before` is non-null, only - return children that occur somewhere after a node with that name - or group. If `after` is non-null, only return children that - occur somewhere before a node with that name or group. + Move a cursor position by [grapheme + cluster](https://codemirror.net/6/docs/ref/#state.findClusterBreak). `forward` determines whether + the motion is away from the line start, or towards it. In + bidirectional text, the line is traversed in visual order, using + the editor's [text direction](https://codemirror.net/6/docs/ref/#view.EditorView.textDirection). + When the start position was the last one on the line, the + returned position will be across the line break. If there is no + further line, the original position is returned. + + By default, this method moves over a single cluster. The + optional `by` argument can be used to move across more. It will + be called with the first cluster as argument, and should return + a predicate that determines, for each subsequent cluster, + whether it should also be moved over. */ - getChild(type: string | number, before?: string | number | null, after?: string | number | null): SyntaxNode | null; + moveByChar(start: SelectionRange, forward: boolean, by?: (initial: string) => (next: string) => boolean): SelectionRange; /** - Like [`getChild`](#common.SyntaxNode.getChild), but return all - matching children, not just the first. + Move a cursor position across the next group of either + [letters](https://codemirror.net/6/docs/ref/#state.EditorState.charCategorizer) or non-letter + non-whitespace characters. */ - getChildren(type: string | number, before?: string | number | null, after?: string | number | null): SyntaxNode[]; -} -/** -A tree cursor object focuses on a given node in a syntax tree, and -allows you to move to adjacent nodes. -*/ -declare class TreeCursor implements SyntaxNodeRef { + moveByGroup(start: SelectionRange, forward: boolean): SelectionRange; /** - The node's type. + Get the cursor position visually at the start or end of a line. + Note that this may differ from the _logical_ position at its + start or end (which is simply at `line.from`/`line.to`) if text + at the start or end goes against the line's base text direction. */ - type: NodeType; + visualLineSide(line: Line$1, end: boolean): SelectionRange; /** - Shorthand for `.type.name`. + Move to the next line boundary in the given direction. If + `includeWrap` is true, line wrapping is on, and there is a + further wrap point on the current line, the wrap point will be + returned. Otherwise this function will return the start or end + of the line. */ - get name(): string; + moveToLineBoundary(start: SelectionRange, forward: boolean, includeWrap?: boolean): SelectionRange; /** - The start source offset of this node. + Move a cursor position vertically. When `distance` isn't given, + it defaults to moving to the next line (including wrapped + lines). Otherwise, `distance` should provide a positive distance + in pixels. + + When `start` has a + [`goalColumn`](https://codemirror.net/6/docs/ref/#state.SelectionRange.goalColumn), the vertical + motion will use that as a target horizontal position. Otherwise, + the cursor's own horizontal position is used. The returned + cursor will have its goal column set to whichever column was + used. */ - from: number; + moveVertically(start: SelectionRange, forward: boolean, distance?: number): SelectionRange; /** - The end source offset. + Find the DOM parent node and offset (child offset if `node` is + an element, character offset when it is a text node) at the + given document position. + + Note that for positions that aren't currently in + `visibleRanges`, the resulting DOM position isn't necessarily + meaningful (it may just point before or after a placeholder + element). */ - to: number; - private stack; - private bufferNode; - private yieldNode; - private yieldBuf; + domAtPos(pos: number): { + node: Node; + offset: number; + }; /** - Move the cursor to this node's first child. When this returns - false, the node has no child, and the cursor has not been moved. + Find the document position at the given DOM node. Can be useful + for associating positions with DOM events. Will raise an error + when `node` isn't part of the editor content. */ - firstChild(): boolean; + posAtDOM(node: Node, offset?: number): number; /** - Move the cursor to this node's last child. + Get the document position at the given screen coordinates. For + positions not covered by the visible viewport's DOM structure, + this will return null, unless `false` is passed as second + argument, in which case it'll return an estimated position that + would be near the coordinates if it were rendered. */ - lastChild(): boolean; + posAtCoords(coords: { + x: number; + y: number; + }, precise: false): number; + posAtCoords(coords: { + x: number; + y: number; + }): number | null; /** - Move the cursor to the first child that ends after `pos`. + Get the screen coordinates at the given document position. + `side` determines whether the coordinates are based on the + element before (-1) or after (1) the position (if no element is + available on the given side, the method will transparently use + another strategy to get reasonable coordinates). */ - childAfter(pos: number): boolean; + coordsAtPos(pos: number, side?: -1 | 1): Rect | null; /** - Move to the last child that starts before `pos`. + Return the rectangle around a given character. If `pos` does not + point in front of a character that is in the viewport and + rendered (i.e. not replaced, not a line break), this will return + null. For space characters that are a line wrap point, this will + return the position before the line break. */ - childBefore(pos: number): boolean; + coordsForChar(pos: number): Rect | null; /** - Move the cursor to the child around `pos`. If side is -1 the - child may end at that position, when 1 it may start there. This - will also enter [overlaid](#common.MountedTree.overlay) - [mounted](#common.NodeProp^mounted) trees unless `overlays` is - set to false. + The default width of a character in the editor. May not + accurately reflect the width of all characters (given variable + width fonts or styling of invididual ranges). */ - enter(pos: number, side: -1 | 0 | 1, mode?: IterMode): boolean; + get defaultCharacterWidth(): number; /** - Move to the node's parent node, if this isn't the top node. + The default height of a line in the editor. May not be accurate + for all lines. */ - parent(): boolean; + get defaultLineHeight(): number; /** - Move to this node's next sibling, if any. + The text direction + ([`direction`](https://developer.mozilla.org/en-US/docs/Web/CSS/direction) + CSS property) of the editor's content element. */ - nextSibling(): boolean; + get textDirection(): Direction; /** - Move to this node's previous sibling, if any. + Find the text direction of the block at the given position, as + assigned by CSS. If + [`perLineTextDirection`](https://codemirror.net/6/docs/ref/#view.EditorView^perLineTextDirection) + isn't enabled, or the given position is outside of the viewport, + this will always return the same as + [`textDirection`](https://codemirror.net/6/docs/ref/#view.EditorView.textDirection). Note that + this may trigger a DOM layout. */ - prevSibling(): boolean; - private atLastNode; - private move; + textDirectionAt(pos: number): Direction; /** - Move to the next node in a - [pre-order](https://en.wikipedia.org/wiki/Tree_traversal#Pre-order,_NLR) - traversal, going from a node to its first child or, if the - current node is empty or `enter` is false, its next sibling or - the next sibling of the first parent node that has one. + Whether this editor [wraps lines](https://codemirror.net/6/docs/ref/#view.EditorView.lineWrapping) + (as determined by the + [`white-space`](https://developer.mozilla.org/en-US/docs/Web/CSS/white-space) + CSS property of its content element). */ - next(enter?: boolean): boolean; + get lineWrapping(): boolean; /** - Move to the next node in a last-to-first pre-order traveral. A - node is followed by its last child or, if it has none, its - previous sibling or the previous sibling of the first parent - node that has one. + Returns the bidirectional text structure of the given line + (which should be in the current document) as an array of span + objects. The order of these spans matches the [text + direction](https://codemirror.net/6/docs/ref/#view.EditorView.textDirection)—if that is + left-to-right, the leftmost spans come first, otherwise the + rightmost spans come first. */ - prev(enter?: boolean): boolean; + bidiSpans(line: Line$1): readonly BidiSpan[]; /** - Move the cursor to the innermost node that covers `pos`. If - `side` is -1, it will enter nodes that end at `pos`. If it is 1, - it will enter nodes that start at `pos`. + Check whether the editor has focus. */ - moveTo(pos: number, side?: -1 | 0 | 1): this; + get hasFocus(): boolean; /** - Get a [syntax node](#common.SyntaxNode) at the cursor's current - position. + Put focus on the editor. */ - get node(): SyntaxNode; + focus(): void; /** - Get the [tree](#common.Tree) that represents the current node, if - any. Will return null when the node is in a [tree - buffer](#common.TreeBuffer). + Update the [root](https://codemirror.net/6/docs/ref/##view.EditorViewConfig.root) in which the editor lives. This is only + necessary when moving the editor's existing DOM to a new window or shadow root. */ - get tree(): Tree | null; + setRoot(root: Document | ShadowRoot): void; /** - Iterate over the current node and all its descendants, calling - `enter` when entering a node and `leave`, if given, when leaving - one. When `enter` returns `false`, any children of that node are - skipped, and `leave` isn't called for it. + Clean up this editor view, removing its element from the + document, unregistering event handlers, and notifying + plugins. The view instance can no longer be used after + calling this. */ - iterate(enter: (node: SyntaxNodeRef) => boolean | void, leave?: (node: SyntaxNodeRef) => void): void; + destroy(): void; /** - Test whether the current node matches a given context—a sequence - of direct parent node names. Empty strings in the context array - are treated as wildcards. + Returns an effect that can be + [added](https://codemirror.net/6/docs/ref/#state.TransactionSpec.effects) to a transaction to + cause it to scroll the given position or range into view. */ - matchContext(context: readonly string[]): boolean; -} - -/** -Objects returned by the function passed to -[`parseMixed`](#common.parseMixed) should conform to this -interface. -*/ -interface NestedParse { + static scrollIntoView(pos: number | SelectionRange, options?: { + /** + By default (`"nearest"`) the position will be vertically + scrolled only the minimal amount required to move the given + position into view. You can set this to `"start"` to move it + to the top of the view, `"end"` to move it to the bottom, or + `"center"` to move it to the center. + */ + y?: ScrollStrategy; + /** + Effect similar to + [`y`](https://codemirror.net/6/docs/ref/#view.EditorView^scrollIntoView^options.y), but for the + horizontal scroll position. + */ + x?: ScrollStrategy; + /** + Extra vertical distance to add when moving something into + view. Not used with the `"center"` strategy. Defaults to 5. + Must be less than the height of the editor. + */ + yMargin?: number; + /** + Extra horizontal distance to add. Not used with the `"center"` + strategy. Defaults to 5. Must be less than the width of the + editor. + */ + xMargin?: number; + }): StateEffect; /** - The parser to use for the inner region. + Return an effect that resets the editor to its current (at the + time this method was called) scroll position. Note that this + only affects the editor's own scrollable element, not parents. + See also + [`EditorViewConfig.scrollTo`](https://codemirror.net/6/docs/ref/#view.EditorViewConfig.scrollTo). + + The effect should be used with a document identical to the one + it was created for. Failing to do so is not an error, but may + not scroll to the expected position. You can + [map](https://codemirror.net/6/docs/ref/#state.StateEffect.map) the effect to account for changes. */ - parser: Parser; + scrollSnapshot(): StateEffect; /** - When this property is not given, the entire node is parsed with - this parser, and it is [mounted](#common.NodeProp^mounted) as a - non-overlay node, replacing its host node in tree iteration. - - When an array of ranges is given, only those ranges are parsed, - and the tree is mounted as an - [overlay](#common.MountedTree.overlay). + Enable or disable tab-focus mode, which disables key bindings + for Tab and Shift-Tab, letting the browser's default + focus-changing behavior go through instead. This is useful to + prevent trapping keyboard users in your editor. - When a function is given, that function will be called for - descendant nodes of the target node, not including child nodes - that are covered by another nested parse, to determine the - overlay ranges. When it returns true, the entire descendant is - included, otherwise just the range given. The mixed parser will - optimize range-finding in reused nodes, which means it's a good - idea to use a function here when the target node is expected to - have a large, deep structure. + Without argument, this toggles the mode. With a boolean, it + enables (true) or disables it (false). Given a number, it + temporarily enables the mode until that number of milliseconds + have passed or another non-Tab key is pressed. */ - overlay?: readonly { - from: number; - to: number; - }[] | ((node: SyntaxNodeRef) => { - from: number; - to: number; - } | boolean); -} -/** -Create a parse wrapper that, after the inner parse completes, -scans its tree for mixed language regions with the `nest` -function, runs the resulting [inner parses](#common.NestedParse), -and then [mounts](#common.NodeProp^mounted) their results onto the -tree. -*/ -declare function parseMixed(nest: (node: SyntaxNodeRef, input: Input) => NestedParse | null): ParseWrapper; - -/** -A parse stack. These are used internally by the parser to track -parsing progress. They also provide some properties and methods -that external code such as a tokenizer can use to get information -about the parse state. -*/ -declare class Stack { + setTabFocusMode(to?: boolean | number): void; /** - The input position up to which this stack has parsed. + Facet to add a [style + module](https://github.com/marijnh/style-mod#documentation) to + an editor view. The view will ensure that the module is + mounted in its [document + root](https://codemirror.net/6/docs/ref/#view.EditorView.constructor^config.root). */ - pos: number; + static styleModule: Facet; /** - The stack's current [context](#lr.ContextTracker) value, if - any. Its type will depend on the context tracker's type - parameter, or it will be `null` if there is no context - tracker. + Returns an extension that can be used to add DOM event handlers. + The value should be an object mapping event names to handler + functions. For any given event, such functions are ordered by + extension precedence, and the first handler to return true will + be assumed to have handled that event, and no other handlers or + built-in behavior will be activated for it. These are registered + on the [content element](https://codemirror.net/6/docs/ref/#view.EditorView.contentDOM), except + for `scroll` handlers, which will be called any time the + editor's [scroll element](https://codemirror.net/6/docs/ref/#view.EditorView.scrollDOM) or one of + its parent nodes is scrolled. */ - get context(): any; + static domEventHandlers(handlers: DOMEventHandlers): Extension; /** - Check if the given term would be able to be shifted (optionally - after some reductions) on this stack. This can be useful for - external tokenizers that want to make sure they only provide a - given token when it applies. + Create an extension that registers DOM event observers. Contrary + to event [handlers](https://codemirror.net/6/docs/ref/#view.EditorView^domEventHandlers), + observers can't be prevented from running by a higher-precedence + handler returning true. They also don't prevent other handlers + and observers from running when they return true, and should not + call `preventDefault`. */ - canShift(term: number): boolean; + static domEventObservers(observers: DOMEventHandlers): Extension; /** - Get the parser used by this stack. + An input handler can override the way changes to the editable + DOM content are handled. Handlers are passed the document + positions between which the change was found, and the new + content. When one returns true, no further input handlers are + called and the default behavior is prevented. + + The `insert` argument can be used to get the default transaction + that would be applied for this input. This can be useful when + dispatching the custom behavior as a separate transaction. */ - get parser(): LRParser; + static inputHandler: Facet<(view: EditorView, from: number, to: number, text: string, insert: () => Transaction) => boolean, readonly ((view: EditorView, from: number, to: number, text: string, insert: () => Transaction) => boolean)[]>; /** - Test whether a given dialect (by numeric ID, as exported from - the terms file) is enabled. + Scroll handlers can override how things are scrolled into view. + If they return `true`, no further handling happens for the + scrolling. If they return false, the default scroll behavior is + applied. Scroll handlers should never initiate editor updates. */ - dialectEnabled(dialectID: number): boolean; - private shiftContext; - private reduceContext; - private updateContext; -} - -/** -[Tokenizers](#lr.ExternalTokenizer) interact with the input -through this interface. It presents the input as a stream of -characters, tracking lookahead and hiding the complexity of -[ranges](#common.Parser.parse^ranges) from tokenizer code. -*/ -declare class InputStream { + static scrollHandler: Facet<(view: EditorView, range: SelectionRange, options: { + x: ScrollStrategy; + y: ScrollStrategy; + xMargin: number; + yMargin: number; + }) => boolean, readonly ((view: EditorView, range: SelectionRange, options: { + x: ScrollStrategy; + y: ScrollStrategy; + xMargin: number; + yMargin: number; + }) => boolean)[]>; /** - Backup chunk + This facet can be used to provide functions that create effects + to be dispatched when the editor's focus state changes. */ - private chunk2; - private chunk2Pos; + static focusChangeEffect: Facet<(state: EditorState, focusing: boolean) => StateEffect | null, readonly ((state: EditorState, focusing: boolean) => StateEffect | null)[]>; /** - The character code of the next code unit in the input, or -1 - when the stream is at the end of the input. + By default, the editor assumes all its content has the same + [text direction](https://codemirror.net/6/docs/ref/#view.Direction). Configure this with a `true` + value to make it read the text direction of every (rendered) + line separately. */ - next: number; + static perLineTextDirection: Facet; /** - The current position of the stream. Note that, due to parses - being able to cover non-contiguous - [ranges](#common.Parser.startParse), advancing the stream does - not always mean its position moves a single unit. + Allows you to provide a function that should be called when the + library catches an exception from an extension (mostly from view + plugins, but may be used by other extensions to route exceptions + from user-code-provided callbacks). This is mostly useful for + debugging and logging. See [`logException`](https://codemirror.net/6/docs/ref/#view.logException). */ - pos: number; - private rangeIndex; - private range; + static exceptionSink: Facet<(exception: any) => void, readonly ((exception: any) => void)[]>; /** - Look at a code unit near the stream position. `.peek(0)` equals - `.next`, `.peek(-1)` gives you the previous character, and so - on. - - Note that looking around during tokenizing creates dependencies - on potentially far-away content, which may reduce the - effectiveness incremental parsing—when looking forward—or even - cause invalid reparses when looking backward more than 25 code - units, since the library does not track lookbehind. + A facet that can be used to register a function to be called + every time the view updates. */ - peek(offset: number): number; + static updateListener: Facet<(update: ViewUpdate) => void, readonly ((update: ViewUpdate) => void)[]>; /** - Accept a token. By default, the end of the token is set to the - current stream position, but you can pass an offset (relative to - the stream position) to change that. + Facet that controls whether the editor content DOM is editable. + When its highest-precedence value is `false`, the element will + not have its `contenteditable` attribute set. (Note that this + doesn't affect API calls that change the editor content, even + when those are bound to keys or buttons. See the + [`readOnly`](https://codemirror.net/6/docs/ref/#state.EditorState.readOnly) facet for that.) */ - acceptToken(token: number, endOffset?: number): void; - private getChunk; - private readNext; + static editable: Facet; /** - Move the stream forward N (defaults to 1) code units. Returns - the new value of [`next`](#lr.InputStream.next). + Allows you to influence the way mouse selection happens. The + functions in this facet will be called for a `mousedown` event + on the editor, and can return an object that overrides the way a + selection is computed from that mouse click or drag. */ - advance(n?: number): number; - private setDone; -} -interface ExternalOptions { + static mouseSelectionStyle: Facet; /** - When set to true, mark this tokenizer as depending on the - current parse stack, which prevents its result from being cached - between parser actions at the same positions. + Facet used to configure whether a given selection drag event + should move or copy the selection. The given predicate will be + called with the `mousedown` event, and can return `true` when + the drag should move the content. */ - contextual?: boolean; + static dragMovesSelection: Facet<(event: MouseEvent) => boolean, readonly ((event: MouseEvent) => boolean)[]>; /** - By defaults, when a tokenizer returns a token, that prevents - tokenizers with lower precedence from even running. When - `fallback` is true, the tokenizer is allowed to run when a - previous tokenizer returned a token that didn't match any of the - current state's actions. + Facet used to configure whether a given selecting click adds a + new range to the existing selection or replaces it entirely. The + default behavior is to check `event.metaKey` on macOS, and + `event.ctrlKey` elsewhere. */ - fallback?: boolean; + static clickAddsSelectionRange: Facet<(event: MouseEvent) => boolean, readonly ((event: MouseEvent) => boolean)[]>; /** - When set to true, tokenizing will not stop after this tokenizer - has produced a token. (But it will still fail to reach this one - if a higher-precedence tokenizer produced a token.) + A facet that determines which [decorations](https://codemirror.net/6/docs/ref/#view.Decoration) + are shown in the view. Decorations can be provided in two + ways—directly, or via a function that takes an editor view. + + Only decoration sets provided directly are allowed to influence + the editor's vertical layout structure. The ones provided as + functions are called _after_ the new viewport has been computed, + and thus **must not** introduce block widgets or replacing + decorations that cover line breaks. + + If you want decorated ranges to behave like atomic units for + cursor motion and deletion purposes, also provide the range set + containing the decorations to + [`EditorView.atomicRanges`](https://codemirror.net/6/docs/ref/#view.EditorView^atomicRanges). */ - extend?: boolean; -} -/** -`@external tokens` declarations in the grammar should resolve to -an instance of this class. -*/ -declare class ExternalTokenizer { + static decorations: Facet DecorationSet), readonly (DecorationSet | ((view: EditorView) => DecorationSet))[]>; /** - Create a tokenizer. The first argument is the function that, - given an input stream, scans for the types of tokens it - recognizes at the stream's position, and calls - [`acceptToken`](#lr.InputStream.acceptToken) when it finds - one. + Facet that works much like + [`decorations`](https://codemirror.net/6/docs/ref/#view.EditorView^decorations), but puts its + inputs at the very bottom of the precedence stack, meaning mark + decorations provided here will only be split by other, partially + overlapping \`outerDecorations\` ranges, and wrap around all + regular decorations. Use this for mark elements that should, as + much as possible, remain in one piece. */ - constructor( + static outerDecorations: Facet DecorationSet), readonly (DecorationSet | ((view: EditorView) => DecorationSet))[]>; /** - @internal + Used to provide ranges that should be treated as atoms as far as + cursor motion is concerned. This causes methods like + [`moveByChar`](https://codemirror.net/6/docs/ref/#view.EditorView.moveByChar) and + [`moveVertically`](https://codemirror.net/6/docs/ref/#view.EditorView.moveVertically) (and the + commands built on top of them) to skip across such regions when + a selection endpoint would enter them. This does _not_ prevent + direct programmatic [selection + updates](https://codemirror.net/6/docs/ref/#state.TransactionSpec.selection) from moving into such + regions. */ - token: (input: InputStream, stack: Stack) => void, options?: ExternalOptions); -} - -/** -Context trackers are used to track stateful context (such as -indentation in the Python grammar, or parent elements in the XML -grammar) needed by external tokenizers. You declare them in a -grammar file as `@context exportName from "module"`. - -Context values should be immutable, and can be updated (replaced) -on shift or reduce actions. - -The export used in a `@context` declaration should be of this -type. -*/ -declare class ContextTracker { + static atomicRanges: Facet<(view: EditorView) => RangeSet, readonly ((view: EditorView) => RangeSet)[]>; /** - Define a context tracker. + When range decorations add a `unicode-bidi: isolate` style, they + should also include a + [`bidiIsolate`](https://codemirror.net/6/docs/ref/#view.MarkDecorationSpec.bidiIsolate) property + in their decoration spec, and be exposed through this facet, so + that the editor can compute the proper text order. (Other values + for `unicode-bidi`, except of course `normal`, are not + supported.) */ - constructor(spec: { - /** - The initial value of the context at the start of the parse. - */ - start: T; - /** - Update the context when the parser executes a - [shift](https://en.wikipedia.org/wiki/LR_parser#Shift_and_reduce_actions) - action. - */ - shift?(context: T, term: number, stack: Stack, input: InputStream): T; - /** - Update the context when the parser executes a reduce action. - */ - reduce?(context: T, term: number, stack: Stack, input: InputStream): T; - /** - Update the context when the parser reuses a node from a tree - fragment. - */ - reuse?(context: T, node: Tree, stack: Stack, input: InputStream): T; - /** - Reduce a context value to a number (for cheap storage and - comparison). Only needed for strict contexts. - */ - hash?(context: T): number; - /** - By default, nodes can only be reused during incremental - parsing if they were created in the same context as the one in - which they are reused. Set this to false to disable that - check (and the overhead of storing the hashes). - */ - strict?: boolean; - }); -} -/** -Configuration options when -[reconfiguring](#lr.LRParser.configure) a parser. -*/ -interface ParserConfig { + static bidiIsolatedRanges: Facet DecorationSet), readonly (DecorationSet | ((view: EditorView) => DecorationSet))[]>; /** - Node prop values to add to the parser's node set. + Facet that allows extensions to provide additional scroll + margins (space around the sides of the scrolling element that + should be considered invisible). This can be useful when the + plugin introduces elements that cover part of that element (for + example a horizontally fixed gutter). */ - props?: readonly NodePropSource[]; + static scrollMargins: Facet<(view: EditorView) => Partial | null, readonly ((view: EditorView) => Partial | null)[]>; /** - The name of the `@top` declaration to parse from. If not - specified, the first top rule declaration in the grammar is - used. + Create a theme extension. The first argument can be a + [`style-mod`](https://github.com/marijnh/style-mod#documentation) + style spec providing the styles for the theme. These will be + prefixed with a generated class for the style. + + Because the selectors will be prefixed with a scope class, rule + that directly match the editor's [wrapper + element](https://codemirror.net/6/docs/ref/#view.EditorView.dom)—to which the scope class will be + added—need to be explicitly differentiated by adding an `&` to + the selector for that element—for example + `&.cm-focused`. + + When `dark` is set to true, the theme will be marked as dark, + which will cause the `&dark` rules from [base + themes](https://codemirror.net/6/docs/ref/#view.EditorView^baseTheme) to be used (as opposed to + `&light` when a light theme is active). */ - top?: string; + static theme(spec: { + [selector: string]: StyleSpec; + }, options?: { + dark?: boolean; + }): Extension; /** - A space-separated string of dialects to enable. + This facet records whether a dark theme is active. The extension + returned by [`theme`](https://codemirror.net/6/docs/ref/#view.EditorView^theme) automatically + includes an instance of this when the `dark` option is set to + true. */ - dialect?: string; + static darkTheme: Facet; /** - Replace the given external tokenizers with new ones. + Create an extension that adds styles to the base theme. Like + with [`theme`](https://codemirror.net/6/docs/ref/#view.EditorView^theme), use `&` to indicate the + place of the editor wrapper element when directly targeting + that. You can also use `&dark` or `&light` instead to only + target editors with a dark or light theme. */ - tokenizers?: { - from: ExternalTokenizer; - to: ExternalTokenizer; - }[]; + static baseTheme(spec: { + [selector: string]: StyleSpec; + }): Extension; /** - Replace external specializers with new ones. + Provides a Content Security Policy nonce to use when creating + the style sheets for the editor. Holds the empty string when no + nonce has been provided. */ - specializers?: { - from: (value: string, stack: Stack) => number; - to: (value: string, stack: Stack) => number; - }[]; + static cspNonce: Facet; /** - Replace the context tracker with a new one. + Facet that provides additional DOM attributes for the editor's + editable DOM element. */ - contextTracker?: ContextTracker; + static contentAttributes: Facet; /** - When true, the parser will raise an exception, rather than run - its error-recovery strategies, when the input doesn't match the - grammar. + Facet that provides DOM attributes for the editor's outer + element. */ - strict?: boolean; + static editorAttributes: Facet; /** - Add a wrapper, which can extend parses created by this parser - with additional logic (usually used to add - [mixed-language](#common.parseMixed) parsing). + An extension that enables line wrapping in the editor (by + setting CSS `white-space` to `pre-wrap` in the content). */ - wrap?: ParseWrapper; + static lineWrapping: Extension; /** - The maximum length of the TreeBuffers generated in the output - tree. Defaults to 1024. + State effect used to include screen reader announcements in a + transaction. These will be added to the DOM in a visually hidden + element with `aria-live="polite"` set, and should be used to + describe effects that are visually obvious but may not be + noticed by screen reader users (such as moving to the next + search match). */ - bufferLength?: number; + static announce: StateEffectType; + /** + Retrieve an editor view instance from the view's DOM + representation. + */ + static findFromDOM(dom: HTMLElement): EditorView | null; } /** -Holds the parse tables for a given grammar, as generated by -`lezer-generator`, and provides [methods](#common.Parser) to parse -content with. +Helper type that maps event names to event object types, or the +`any` type for unknown events. */ -declare class LRParser extends Parser { +interface DOMEventMap extends HTMLElementEventMap { + [other: string]: any; +} +/** +Event handlers are specified with objects like this. For event +types known by TypeScript, this will infer the event argument type +to hold the appropriate event object type. For unknown events, it +is inferred to `any`, and should be explicitly set if you want type +checking. +*/ +type DOMEventHandlers = { + [event in keyof DOMEventMap]?: (this: This, event: DOMEventMap[event], view: EditorView) => boolean | void; +}; + +/** +Key bindings associate key names with +[command](https://codemirror.net/6/docs/ref/#view.Command)-style functions. + +Key names may be strings like `"Shift-Ctrl-Enter"`—a key identifier +prefixed with zero or more modifiers. Key identifiers are based on +the strings that can appear in +[`KeyEvent.key`](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key). +Use lowercase letters to refer to letter keys (or uppercase letters +if you want shift to be held). You may use `"Space"` as an alias +for the `" "` name. + +Modifiers can be given in any order. `Shift-` (or `s-`), `Alt-` (or +`a-`), `Ctrl-` (or `c-` or `Control-`) and `Cmd-` (or `m-` or +`Meta-`) are recognized. + +When a key binding contains multiple key names separated by +spaces, it represents a multi-stroke binding, which will fire when +the user presses the given keys after each other. + +You can use `Mod-` as a shorthand for `Cmd-` on Mac and `Ctrl-` on +other platforms. So `Mod-b` is `Ctrl-b` on Linux but `Cmd-b` on +macOS. +*/ +interface KeyBinding { /** - The nodes used in the trees emitted by this parser. + The key name to use for this binding. If the platform-specific + property (`mac`, `win`, or `linux`) for the current platform is + used as well in the binding, that one takes precedence. If `key` + isn't defined and the platform-specific binding isn't either, + a binding is ignored. */ - readonly nodeSet: NodeSet; - createParse(input: Input, fragments: readonly TreeFragment[], ranges: readonly { - from: number; - to: number; - }[]): PartialParse; + key?: string; /** - Configure the parser. Returns a new parser instance that has the - given settings modified. Settings not provided in `config` are - kept from the original parser. + Key to use specifically on macOS. */ - configure(config: ParserConfig): LRParser; + mac?: string; /** - Tells you whether any [parse wrappers](#lr.ParserConfig.wrap) - are registered for this parser. + Key to use specifically on Windows. */ - hasWrappers(): boolean; + win?: string; /** - Returns the name associated with a given term. This will only - work for all terms when the parser was generated with the - `--names` option. By default, only the names of tagged terms are - stored. + Key to use specifically on Linux. */ - getName(term: number): string; + linux?: string; /** - The type of top node produced by the parser. + The command to execute when this binding is triggered. When the + command function returns `false`, further bindings will be tried + for the key. */ - get topNode(): NodeType; + run?: Command; /** - Used by the output of the parser generator. Not available to - user code. @hide + When given, this defines a second binding, using the (possibly + platform-specific) key name prefixed with `Shift-` to activate + this command. */ - static deserialize(spec: any): LRParser; -} - -declare class StyleModule { - constructor(spec: {[selector: string]: StyleSpec}, options?: { - finish?(sel: string): string - }) - getRules(): string - static mount( - root: Document | ShadowRoot | DocumentOrShadowRoot, - module: StyleModule | ReadonlyArray, - options?: {nonce?: string} - ): void - static newName(): string -} - -type StyleSpec = { - [propOrSelector: string]: string | number | StyleSpec | null -} - -/** -Used to indicate [text direction](https://codemirror.net/6/docs/ref/#view.EditorView.textDirection). -*/ -declare enum Direction { + shift?: Command; + /** + When this property is present, the function is called for every + key that is not a multi-stroke prefix. + */ + any?: (view: EditorView, event: KeyboardEvent) => boolean; + /** + By default, key bindings apply when focus is on the editor + content (the `"editor"` scope). Some extensions, mostly those + that define their own panels, might want to allow you to + register bindings local to that panel. Such bindings should use + a custom scope name. You may also assign multiple scope names to + a binding, separating them by spaces. + */ + scope?: string; /** - Left-to-right. + When set to true (the default is false), this will always + prevent the further handling for the bound key, even if the + command(s) return false. This can be useful for cases where the + native behavior of the key is annoying or irrelevant but the + command doesn't always apply (such as, Mod-u for undo selection, + which would cause the browser to view source instead when no + selection can be undone). */ - LTR = 0, + preventDefault?: boolean; /** - Right-to-left. + When set to true, `stopPropagation` will be called on keyboard + events that have their `preventDefault` called in response to + this key binding (see also + [`preventDefault`](https://codemirror.net/6/docs/ref/#view.KeyBinding.preventDefault)). */ - RTL = 1 + stopPropagation?: boolean; } /** -Represents a contiguous range of text that has a single direction -(as in left-to-right or right-to-left). +Facet used for registering keymaps. + +You can add multiple keymaps to an editor. Their priorities +determine their precedence (the ones specified early or with high +priority get checked first). When a handler has returned `true` +for a given key, no further handlers are called. */ -declare class BidiSpan { +declare const keymap: Facet; + +type SelectionConfig = { /** - The start of the span (relative to the start of the line). + The length of a full cursor blink cycle, in milliseconds. + Defaults to 1200. Can be set to 0 to disable blinking. */ - readonly from: number; + cursorBlinkRate?: number; /** - The end of the span. + Whether to show a cursor for non-empty ranges. Defaults to + true. */ - readonly to: number; + drawRangeCursor?: boolean; +}; +/** +Returns an extension that hides the browser's native selection and +cursor, replacing the selection with a background behind the text +(with the `cm-selectionBackground` class), and the +cursors with elements overlaid over the code (using +`cm-cursor-primary` and `cm-cursor-secondary`). + +This allows the editor to display secondary selection ranges, and +tends to produce a type of selection more in line with that users +expect in a text editor (the native selection styling will often +leave gaps between lines and won't fill the horizontal space after +a line when the selection continues past it). + +It does have a performance cost, in that it requires an extra DOM +layout cycle for many updates (the selection is drawn based on DOM +layout information that's only available after laying out the +content). +*/ +declare function drawSelection(config?: SelectionConfig): Extension; + +interface SpecialCharConfig { /** - The ["bidi - level"](https://unicode.org/reports/tr9/#Basic_Display_Algorithm) - of the span (in this context, 0 means - left-to-right, 1 means right-to-left, 2 means left-to-right - number inside right-to-left text). + An optional function that renders the placeholder elements. + + The `description` argument will be text that clarifies what the + character is, which should be provided to screen readers (for + example with the + [`aria-label`](https://www.w3.org/TR/wai-aria/#aria-label) + attribute) and optionally shown to the user in other ways (such + as the + [`title`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/title) + attribute). + + The given placeholder string is a suggestion for how to display + the character visually. */ - readonly level: number; + render?: ((code: number, description: string | null, placeholder: string) => HTMLElement) | null; /** - The direction of this span. + Regular expression that matches the special characters to + highlight. Must have its 'g'/global flag set. */ - get dir(): Direction; + specialChars?: RegExp; + /** + Regular expression that can be used to add characters to the + default set of characters to highlight. + */ + addSpecialChars?: RegExp | null; } +/** +Returns an extension that installs highlighting of special +characters. +*/ +declare function highlightSpecialChars( +/** +Configuration options. +*/ +config?: SpecialCharConfig): Extension; -type Attrs = { - [name: string]: string; -}; +/** +Mark lines that have a cursor on them with the `"cm-activeLine"` +DOM class. +*/ +declare function highlightActiveLine(): Extension; /** -Basic rectangle type. +Extension that enables a placeholder—a piece of example content +to show when the editor is empty. */ -interface Rect { - readonly left: number; - readonly right: number; - readonly top: number; - readonly bottom: number; -} -type ScrollStrategy = "nearest" | "start" | "end" | "center"; +declare function placeholder(content: string | HTMLElement): Extension; -interface MarkDecorationSpec { +/** +Helper class used to make it easier to maintain decorations on +visible code that matches a given regular expression. To be used +in a [view plugin](https://codemirror.net/6/docs/ref/#view.ViewPlugin). Instances of this object +represent a matching configuration. +*/ +declare class MatchDecorator { + private regexp; + private addMatch; + private boundary; + private maxLength; /** - Whether the mark covers its start and end position or not. This - influences whether content inserted at those positions becomes - part of the mark. Defaults to false. + Create a decorator. */ - inclusive?: boolean; + constructor(config: { + /** + The regular expression to match against the content. Will only + be matched inside lines (not across them). Should have its 'g' + flag set. + */ + regexp: RegExp; + /** + The decoration to apply to matches, either directly or as a + function of the match. + */ + decoration?: Decoration | ((match: RegExpExecArray, view: EditorView, pos: number) => Decoration | null); + /** + Customize the way decorations are added for matches. This + function, when given, will be called for matches and should + call `add` to create decorations for them. Note that the + decorations should appear *in* the given range, and the + function should have no side effects beyond calling `add`. + + The `decoration` option is ignored when `decorate` is + provided. + */ + decorate?: (add: (from: number, to: number, decoration: Decoration) => void, from: number, to: number, match: RegExpExecArray, view: EditorView) => void; + /** + By default, changed lines are re-matched entirely. You can + provide a boundary expression, which should match single + character strings that can never occur in `regexp`, to reduce + the amount of re-matching. + */ + boundary?: RegExp; + /** + Matching happens by line, by default, but when lines are + folded or very long lines are only partially drawn, the + decorator may avoid matching part of them for speed. This + controls how much additional invisible content it should + include in its matches. Defaults to 1000. + */ + maxLength?: number; + }); /** - Specify whether the start position of the marked range should be - inclusive. Overrides `inclusive`, when both are present. + Compute the full set of decorations for matches in the given + view's viewport. You'll want to call this when initializing your + plugin. */ - inclusiveStart?: boolean; + createDeco(view: EditorView): RangeSet; /** - Whether the end should be inclusive. + Update a set of decorations for a view update. `deco` _must_ be + the set of decorations produced by _this_ `MatchDecorator` for + the view state before the update. */ - inclusiveEnd?: boolean; + updateDeco(update: ViewUpdate, deco: DecorationSet): DecorationSet; + private updateRange; +} + +/** +Create an extension that enables rectangular selections. By +default, it will react to left mouse drag with the Alt key held +down. When such a selection occurs, the text within the rectangle +that was dragged over will be selected, as one selection +[range](https://codemirror.net/6/docs/ref/#state.SelectionRange) per line. +*/ +declare function rectangularSelection(options?: { /** - Add attributes to the DOM elements that hold the text in the - marked range. + A custom predicate function, which takes a `mousedown` event and + returns true if it should be used for rectangular selection. */ - attributes?: { - [key: string]: string; - }; + eventFilter?: (event: MouseEvent) => boolean; +}): Extension; + +/** +Creates an extension that configures tooltip behavior. +*/ +declare function tooltips(config?: { /** - Shorthand for `{attributes: {class: value}}`. + By default, tooltips use `"fixed"` + [positioning](https://developer.mozilla.org/en-US/docs/Web/CSS/position), + which has the advantage that tooltips don't get cut off by + scrollable parent elements. However, CSS rules like `contain: + layout` can break fixed positioning in child nodes, which can be + worked about by using `"absolute"` here. + + On iOS, which at the time of writing still doesn't properly + support fixed positioning, the library always uses absolute + positioning. + + If the tooltip parent element sits in a transformed element, the + library also falls back to absolute positioning. */ - class?: string; + position?: "fixed" | "absolute"; /** - Add a wrapping element around the text in the marked range. Note - that there will not necessarily be a single element covering the - entire range—other decorations with lower precedence might split - this one if they partially overlap it, and line breaks always - end decoration elements. + The element to put the tooltips into. By default, they are put + in the editor (`cm-editor`) element, and that is usually what + you want. But in some layouts that can lead to positioning + issues, and you need to use a different parent to work around + those. */ - tagName?: string; + parent?: HTMLElement; /** - When using sets of decorations in - [`bidiIsolatedRanges`](https://codemirror.net/6/docs/ref/##view.EditorView^bidiIsolatedRanges), - this property provides the direction of the isolates. When null - or not given, it indicates the range has `dir=auto`, and its - direction should be derived from the first strong directional - character in it. + By default, when figuring out whether there is room for a + tooltip at a given position, the extension considers the entire + space between 0,0 and `innerWidth`,`innerHeight` to be available + for showing tooltips. You can provide a function here that + returns an alternative rectangle. */ - bidiIsolate?: Direction | null; + tooltipSpace?: (view: EditorView) => Rect; +}): Extension; +/** +Describes a tooltip. Values of this type, when provided through +the [`showTooltip`](https://codemirror.net/6/docs/ref/#view.showTooltip) facet, control the +individual tooltips on the editor. +*/ +interface Tooltip { /** - Decoration specs allow extra properties, which can be retrieved - through the decoration's [`spec`](https://codemirror.net/6/docs/ref/#view.Decoration.spec) - property. + The document position at which to show the tooltip. */ - [other: string]: any; -} -interface WidgetDecorationSpec { + pos: number; /** - The type of widget to draw here. + The end of the range annotated by this tooltip, if different + from `pos`. */ - widget: WidgetType; + end?: number; /** - Which side of the given position the widget is on. When this is - positive, the widget will be drawn after the cursor if the - cursor is on the same position. Otherwise, it'll be drawn before - it. When multiple widgets sit at the same position, their `side` - values will determine their ordering—those with a lower value - come first. Defaults to 0. May not be more than 10000 or less - than -10000. + A constructor function that creates the tooltip's [DOM + representation](https://codemirror.net/6/docs/ref/#view.TooltipView). */ - side?: number; + create(view: EditorView): TooltipView; /** - By default, to avoid unintended mixing of block and inline - widgets, block widgets with a positive `side` are always drawn - after all inline widgets at that position, and those with a - non-positive side before inline widgets. Setting this option to - `true` for a block widget will turn this off and cause it to be - rendered between the inline widgets, ordered by `side`. + Whether the tooltip should be shown above or below the target + position. Not guaranteed to be respected for hover tooltips + since all hover tooltips for the same range are always + positioned together. Defaults to false. */ - inlineOrder?: boolean; - /** - Determines whether this is a block widgets, which will be drawn - between lines, or an inline widget (the default) which is drawn - between the surrounding text. - - Note that block-level decorations should not have vertical - margins, and if you dynamically change their height, you should - make sure to call - [`requestMeasure`](https://codemirror.net/6/docs/ref/#view.EditorView.requestMeasure), so that the - editor can update its information about its vertical layout. + above?: boolean; + /** + Whether the `above` option should be honored when there isn't + enough space on that side to show the tooltip inside the + viewport. Defaults to false. */ - block?: boolean; + strictSide?: boolean; /** - Other properties are allowed. + When set to true, show a triangle connecting the tooltip element + to position `pos`. */ - [other: string]: any; + arrow?: boolean; } -interface ReplaceDecorationSpec { +/** +Describes the way a tooltip is displayed. +*/ +interface TooltipView { /** - An optional widget to drawn in the place of the replaced - content. + The DOM element to position over the editor. */ - widget?: WidgetType; + dom: HTMLElement; /** - Whether this range covers the positions on its sides. This - influences whether new content becomes part of the range and - whether the cursor can be drawn on its sides. Defaults to false - for inline replacements, and true for block replacements. + Adjust the position of the tooltip relative to its anchor + position. A positive `x` value will move the tooltip + horizontally along with the text direction (so right in + left-to-right context, left in right-to-left). A positive `y` + will move the tooltip up when it is above its anchor, and down + otherwise. */ - inclusive?: boolean; + offset?: { + x: number; + y: number; + }; /** - Set inclusivity at the start. + By default, a tooltip's screen position will be based on the + text position of its `pos` property. This method can be provided + to make the tooltip view itself responsible for finding its + screen position. */ - inclusiveStart?: boolean; + getCoords?: (pos: number) => Rect; /** - Set inclusivity at the end. + By default, tooltips are moved when they overlap with other + tooltips. Set this to `true` to disable that behavior for this + tooltip. */ - inclusiveEnd?: boolean; + overlap?: boolean; /** - Whether this is a block-level decoration. Defaults to false. + Called after the tooltip is added to the DOM for the first time. */ - block?: boolean; + mount?(view: EditorView): void; /** - Other properties are allowed. + Update the DOM element for a change in the view's state. */ - [other: string]: any; -} -interface LineDecorationSpec { + update?(update: ViewUpdate): void; /** - DOM attributes to add to the element wrapping the line. + Called when the tooltip is removed from the editor or the editor + is destroyed. */ - attributes?: { - [key: string]: string; - }; + destroy?(): void; /** - Shorthand for `{attributes: {class: value}}`. + Called when the tooltip has been (re)positioned. The argument is + the [space](https://codemirror.net/6/docs/ref/#view.tooltips^config.tooltipSpace) available to the + tooltip. */ - class?: string; + positioned?(space: Rect): void; /** - Other properties are allowed. + By default, the library will restrict the size of tooltips so + that they don't stick out of the available space. Set this to + false to disable that. */ - [other: string]: any; + resize?: boolean; } /** -Widgets added to the content are described by subclasses of this -class. Using a description object like that makes it possible to -delay creating of the DOM structure for a widget until it is -needed, and to avoid redrawing widgets even if the decorations -that define them are recreated. +Facet to which an extension can add a value to show a tooltip. */ -declare abstract class WidgetType { +declare const showTooltip: Facet; +type Handlers$1 = { + [event: string]: (view: EditorView, line: BlockInfo, event: Event) => boolean; +}; +interface LineNumberConfig { /** - Build the DOM structure for this widget instance. + How to display line numbers. Defaults to simply converting them + to string. */ - abstract toDOM(view: EditorView): HTMLElement; + formatNumber?: (lineNo: number, state: EditorState) => string; /** - Compare this instance to another instance of the same type. - (TypeScript can't express this, but only instances of the same - specific class will be passed to this method.) This is used to - avoid redrawing widgets when they are replaced by a new - decoration of the same type. The default implementation just - returns `false`, which will cause new instances of the widget to - always be redrawn. + Supply event handlers for DOM events on this gutter. */ - eq(widget: WidgetType): boolean; + domEventHandlers?: Handlers$1; +} +/** +Create a line number gutter extension. +*/ +declare function lineNumbers(config?: LineNumberConfig): Extension; + +interface HistoryConfig { /** - Update a DOM element created by a widget of the same type (but - different, non-`eq` content) to reflect this widget. May return - true to indicate that it could update, false to indicate it - couldn't (in which case the widget will be redrawn). The default - implementation just returns false. + The minimum depth (amount of events) to store. Defaults to 100. */ - updateDOM(dom: HTMLElement, view: EditorView): boolean; + minDepth?: number; /** - The estimated height this widget will have, to be used when - estimating the height of content that hasn't been drawn. May - return -1 to indicate you don't know. The default implementation - returns -1. + The maximum time (in milliseconds) that adjacent events can be + apart and still be grouped together. Defaults to 500. */ - get estimatedHeight(): number; + newGroupDelay?: number; /** - For inline widgets that are displayed inline (as opposed to - `inline-block`) and introduce line breaks (through `
` tags - or textual newlines), this must indicate the amount of line - breaks they introduce. Defaults to 0. + By default, when close enough together in time, changes are + joined into an existing undo event if they touch any of the + changed ranges from that event. You can pass a custom predicate + here to influence that logic. */ - get lineBreaks(): number; + joinToEvent?: (tr: Transaction, isAdjacent: boolean) => boolean; +} +/** +Create a history extension with the given configuration. +*/ +declare function history(config?: HistoryConfig): Extension; +/** +Default key bindings for the undo history. + +- Mod-z: [`undo`](https://codemirror.net/6/docs/ref/#commands.undo). +- Mod-y (Mod-Shift-z on macOS) + Ctrl-Shift-z on Linux: [`redo`](https://codemirror.net/6/docs/ref/#commands.redo). +- Mod-u: [`undoSelection`](https://codemirror.net/6/docs/ref/#commands.undoSelection). +- Alt-u (Mod-Shift-u on macOS): [`redoSelection`](https://codemirror.net/6/docs/ref/#commands.redoSelection). +*/ +declare const historyKeymap: readonly KeyBinding[]; +/** +Move the selected lines up one line. +*/ +declare const moveLineUp: StateCommand; +/** +Move the selected lines down one line. +*/ +declare const moveLineDown: StateCommand; +/** +Add a [unit](https://codemirror.net/6/docs/ref/#language.indentUnit) of indentation to all selected +lines. +*/ +declare const indentMore: StateCommand; +/** +Remove a [unit](https://codemirror.net/6/docs/ref/#language.indentUnit) of indentation from all +selected lines. +*/ +declare const indentLess: StateCommand; +/** +The default keymap. Includes all bindings from +[`standardKeymap`](https://codemirror.net/6/docs/ref/#commands.standardKeymap) plus the following: + +- Alt-ArrowLeft (Ctrl-ArrowLeft on macOS): [`cursorSyntaxLeft`](https://codemirror.net/6/docs/ref/#commands.cursorSyntaxLeft) ([`selectSyntaxLeft`](https://codemirror.net/6/docs/ref/#commands.selectSyntaxLeft) with Shift) +- Alt-ArrowRight (Ctrl-ArrowRight on macOS): [`cursorSyntaxRight`](https://codemirror.net/6/docs/ref/#commands.cursorSyntaxRight) ([`selectSyntaxRight`](https://codemirror.net/6/docs/ref/#commands.selectSyntaxRight) with Shift) +- Alt-ArrowUp: [`moveLineUp`](https://codemirror.net/6/docs/ref/#commands.moveLineUp) +- Alt-ArrowDown: [`moveLineDown`](https://codemirror.net/6/docs/ref/#commands.moveLineDown) +- Shift-Alt-ArrowUp: [`copyLineUp`](https://codemirror.net/6/docs/ref/#commands.copyLineUp) +- Shift-Alt-ArrowDown: [`copyLineDown`](https://codemirror.net/6/docs/ref/#commands.copyLineDown) +- Escape: [`simplifySelection`](https://codemirror.net/6/docs/ref/#commands.simplifySelection) +- Ctrl-Enter (Cmd-Enter on macOS): [`insertBlankLine`](https://codemirror.net/6/docs/ref/#commands.insertBlankLine) +- Alt-l (Ctrl-l on macOS): [`selectLine`](https://codemirror.net/6/docs/ref/#commands.selectLine) +- Ctrl-i (Cmd-i on macOS): [`selectParentSyntax`](https://codemirror.net/6/docs/ref/#commands.selectParentSyntax) +- Ctrl-[ (Cmd-[ on macOS): [`indentLess`](https://codemirror.net/6/docs/ref/#commands.indentLess) +- Ctrl-] (Cmd-] on macOS): [`indentMore`](https://codemirror.net/6/docs/ref/#commands.indentMore) +- Ctrl-Alt-\\ (Cmd-Alt-\\ on macOS): [`indentSelection`](https://codemirror.net/6/docs/ref/#commands.indentSelection) +- Shift-Ctrl-k (Shift-Cmd-k on macOS): [`deleteLine`](https://codemirror.net/6/docs/ref/#commands.deleteLine) +- Shift-Ctrl-\\ (Shift-Cmd-\\ on macOS): [`cursorMatchingBracket`](https://codemirror.net/6/docs/ref/#commands.cursorMatchingBracket) +- Ctrl-/ (Cmd-/ on macOS): [`toggleComment`](https://codemirror.net/6/docs/ref/#commands.toggleComment). +- Shift-Alt-a: [`toggleBlockComment`](https://codemirror.net/6/docs/ref/#commands.toggleBlockComment). +- Ctrl-m (Alt-Shift-m on macOS): [`toggleTabFocusMode`](https://codemirror.net/6/docs/ref/#commands.toggleTabFocusMode). +*/ +declare const defaultKeymap: readonly KeyBinding[]; + +/** +The [`TreeFragment.applyChanges`](#common.TreeFragment^applyChanges) +method expects changed ranges in this format. +*/ +interface ChangedRange { /** - Can be used to configure which kinds of events inside the widget - should be ignored by the editor. The default is to ignore all - events. + The start of the change in the start document */ - ignoreEvent(event: Event): boolean; + fromA: number; /** - Override the way screen coordinates for positions at/in the - widget are found. `pos` will be the offset into the widget, and - `side` the side of the position that is being queried—less than - zero for before, greater than zero for after, and zero for - directly at that position. + The end of the change in the start document */ - coordsAt(dom: HTMLElement, pos: number, side: number): Rect | null; + toA: number; /** - This is called when the an instance of the widget is removed - from the editor view. + The start of the replacement in the new document */ - destroy(dom: HTMLElement): void; + fromB: number; + /** + The end of the replacement in the new document + */ + toB: number; } /** -A decoration set represents a collection of decorated ranges, -organized for efficient access and mapping. See -[`RangeSet`](https://codemirror.net/6/docs/ref/#state.RangeSet) for its methods. -*/ -type DecorationSet = RangeSet; -/** -The different types of blocks that can occur in an editor view. +Tree fragments are used during [incremental +parsing](#common.Parser.startParse) to track parts of old trees +that can be reused in a new parse. An array of fragments is used +to track regions of an old tree whose nodes might be reused in new +parses. Use the static +[`applyChanges`](#common.TreeFragment^applyChanges) method to +update fragments for document changes. */ -declare enum BlockType { +declare class TreeFragment { /** - A line of text. + The start of the unchanged range pointed to by this fragment. + This refers to an offset in the _updated_ document (as opposed + to the original tree). */ - Text = 0, + readonly from: number; /** - A block widget associated with the position after it. + The end of the unchanged range. + */ + readonly to: number; + /** + The tree that this fragment is based on. + */ + readonly tree: Tree; + /** + The offset between the fragment's tree and the document that + this fragment can be used against. Add this when going from + document to tree positions, subtract it to go from tree to + document positions. */ - WidgetBefore = 1, + readonly offset: number; /** - A block widget associated with the position before it. + Construct a tree fragment. You'll usually want to use + [`addTree`](#common.TreeFragment^addTree) and + [`applyChanges`](#common.TreeFragment^applyChanges) instead of + calling this directly. */ - WidgetAfter = 2, + constructor( /** - A block widget [replacing](https://codemirror.net/6/docs/ref/#view.Decoration^replace) a range of content. + The start of the unchanged range pointed to by this fragment. + This refers to an offset in the _updated_ document (as opposed + to the original tree). */ - WidgetRange = 3 -} -/** -A decoration provides information on how to draw or style a piece -of content. You'll usually use it wrapped in a -[`Range`](https://codemirror.net/6/docs/ref/#state.Range), which adds a start and end position. -@nonabstract -*/ -declare abstract class Decoration extends RangeValue { + from: number, /** - The config object used to create this decoration. You can - include additional properties in there to store metadata about - your decoration. + The end of the unchanged range. */ - readonly spec: any; - protected constructor( + to: number, /** - @internal + The tree that this fragment is based on. */ - startSide: number, + tree: Tree, /** - @internal + The offset between the fragment's tree and the document that + this fragment can be used against. Add this when going from + document to tree positions, subtract it to go from tree to + document positions. */ - endSide: number, + offset: number, openStart?: boolean, openEnd?: boolean); /** - @internal + Whether the start of the fragment represents the start of a + parse, or the end of a change. (In the second case, it may not + be safe to reuse some nodes at the start, depending on the + parsing algorithm.) */ - widget: WidgetType | null, + get openStart(): boolean; /** - The config object used to create this decoration. You can - include additional properties in there to store metadata about - your decoration. + Whether the end of the fragment represents the end of a + full-document parse, or the start of a change. */ - spec: any); - abstract eq(other: Decoration): boolean; + get openEnd(): boolean; /** - Create a mark decoration, which influences the styling of the - content in its range. Nested mark decorations will cause nested - DOM elements to be created. Nesting order is determined by - precedence of the [facet](https://codemirror.net/6/docs/ref/#view.EditorView^decorations), with - the higher-precedence decorations creating the inner DOM nodes. - Such elements are split on line boundaries and on the boundaries - of lower-precedence decorations. + Create a set of fragments from a freshly parsed tree, or update + an existing set of fragments by replacing the ones that overlap + with a tree with content from the new tree. When `partial` is + true, the parse is treated as incomplete, and the resulting + fragment has [`openEnd`](#common.TreeFragment.openEnd) set to + true. */ - static mark(spec: MarkDecorationSpec): Decoration; + static addTree(tree: Tree, fragments?: readonly TreeFragment[], partial?: boolean): readonly TreeFragment[]; /** - Create a widget decoration, which displays a DOM element at the - given position. + Apply a set of edits to an array of fragments, removing or + splitting fragments as necessary to remove edited ranges, and + adjusting offsets for fragments that moved. */ - static widget(spec: WidgetDecorationSpec): Decoration; + static applyChanges(fragments: readonly TreeFragment[], changes: readonly ChangedRange[], minGap?: number): readonly TreeFragment[]; +} +/** +Interface used to represent an in-progress parse, which can be +moved forward piece-by-piece. +*/ +interface PartialParse { /** - Create a replace decoration which replaces the given range with - a widget, or simply hides it. + Advance the parse state by some amount. Will return the finished + syntax tree when the parse completes. */ - static replace(spec: ReplaceDecorationSpec): Decoration; + advance(): Tree | null; /** - Create a line decoration, which can add DOM attributes to the - line starting at the given position. + The position up to which the document has been parsed. Note + that, in multi-pass parsers, this will stay back until the last + pass has moved past a given position. */ - static line(spec: LineDecorationSpec): Decoration; + readonly parsedPos: number; /** - Build a [`DecorationSet`](https://codemirror.net/6/docs/ref/#view.DecorationSet) from the given - decorated range or ranges. If the ranges aren't already sorted, - pass `true` for `sort` to make the library sort them for you. + Tell the parse to not advance beyond the given position. + `advance` will return a tree when the parse has reached the + position. Note that, depending on the parser algorithm and the + state of the parse when `stopAt` was called, that tree may + contain nodes beyond the position. It is an error to call + `stopAt` with a higher position than it's [current + value](#common.PartialParse.stoppedAt). */ - static set(of: Range | readonly Range[], sort?: boolean): DecorationSet; + stopAt(pos: number): void; /** - The empty set of decorations. + Reports whether `stopAt` has been called on this parse. */ - static none: DecorationSet; -} - -/** -Command functions are used in key bindings and other types of user -actions. Given an editor view, they check whether their effect can -apply to the editor, and if it can, perform it as a side effect -(which usually means [dispatching](https://codemirror.net/6/docs/ref/#view.EditorView.dispatch) a -transaction) and return `true`. -*/ -type Command = (target: EditorView) => boolean; -declare class ScrollTarget { - readonly range: SelectionRange; - readonly y: ScrollStrategy; - readonly x: ScrollStrategy; - readonly yMargin: number; - readonly xMargin: number; - readonly isSnapshot: boolean; - constructor(range: SelectionRange, y?: ScrollStrategy, x?: ScrollStrategy, yMargin?: number, xMargin?: number, isSnapshot?: boolean); - map(changes: ChangeDesc): ScrollTarget; - clip(state: EditorState): ScrollTarget; + readonly stoppedAt: number | null; } /** -This is the interface plugin objects conform to. +A superclass that parsers should extend. */ -interface PluginValue extends Object { +declare abstract class Parser { /** - Notifies the plugin of an update that happened in the view. This - is called _before_ the view updates its own DOM. It is - responsible for updating the plugin's internal state (including - any state that may be read by plugin fields) and _writing_ to - the DOM for the changes in the update. To avoid unnecessary - layout recomputations, it should _not_ read the DOM layout—use - [`requestMeasure`](https://codemirror.net/6/docs/ref/#view.EditorView.requestMeasure) to schedule - your code in a DOM reading phase if you need to. + Start a parse for a single tree. This is the method concrete + parser implementations must implement. Called by `startParse`, + with the optional arguments resolved. */ - update?(update: ViewUpdate): void; + abstract createParse(input: Input, fragments: readonly TreeFragment[], ranges: readonly { + from: number; + to: number; + }[]): PartialParse; /** - Called when the document view is updated (due to content, - decoration, or viewport changes). Should not try to immediately - start another view update. Often useful for calling - [`requestMeasure`](https://codemirror.net/6/docs/ref/#view.EditorView.requestMeasure). + Start a parse, returning a [partial parse](#common.PartialParse) + object. [`fragments`](#common.TreeFragment) can be passed in to + make the parse incremental. + + By default, the entire input is parsed. You can pass `ranges`, + which should be a sorted array of non-empty, non-overlapping + ranges, to parse only those ranges. The tree returned in that + case will start at `ranges[0].from`. */ - docViewUpdate?(view: EditorView): void; + startParse(input: Input | string, fragments?: readonly TreeFragment[], ranges?: readonly { + from: number; + to: number; + }[]): PartialParse; /** - Called when the plugin is no longer going to be used. Should - revert any changes the plugin made to the DOM. + Run a full parse, returning the resulting tree. */ - destroy?(): void; + parse(input: Input | string, fragments?: readonly TreeFragment[], ranges?: readonly { + from: number; + to: number; + }[]): Tree; } /** -Provides additional information when defining a [view -plugin](https://codemirror.net/6/docs/ref/#view.ViewPlugin). +This is the interface parsers use to access the document. To run +Lezer directly on your own document data structure, you have to +write an implementation of it. */ -interface PluginSpec { +interface Input { /** - Register the given [event - handlers](https://codemirror.net/6/docs/ref/#view.EditorView^domEventHandlers) for the plugin. - When called, these will have their `this` bound to the plugin - value. + The length of the document. */ - eventHandlers?: DOMEventHandlers; + readonly length: number; /** - Registers [event observers](https://codemirror.net/6/docs/ref/#view.EditorView^domEventObservers) - for the plugin. Will, when called, have their `this` bound to - the plugin value. + Get the chunk after the given position. The returned string + should start at `from` and, if that isn't the end of the + document, may be of any length greater than zero. */ - eventObservers?: DOMEventHandlers; + chunk(from: number): string; /** - Specify that the plugin provides additional extensions when - added to an editor configuration. + Indicates whether the chunks already end at line breaks, so that + client code that wants to work by-line can avoid re-scanning + them for line breaks. When this is true, the result of `chunk()` + should either be a single line break, or the content between + `from` and the next line break. */ - provide?: (plugin: ViewPlugin) => Extension; + readonly lineChunks: boolean; /** - Allow the plugin to provide decorations. When given, this should - be a function that take the plugin value and return a - [decoration set](https://codemirror.net/6/docs/ref/#view.DecorationSet). See also the caveat about - [layout-changing decorations](https://codemirror.net/6/docs/ref/#view.EditorView^decorations) that - depend on the view. + Read the part of the document between the given positions. */ - decorations?: (value: V) => DecorationSet; + read(from: number, to: number): string; } /** -View plugins associate stateful values with a view. They can -influence the way the content is drawn, and are notified of things -that happen in the view. +Parse wrapper functions are supported by some parsers to inject +additional parsing logic. */ -declare class ViewPlugin { +type ParseWrapper = (inner: PartialParse, input: Input, fragments: readonly TreeFragment[], ranges: readonly { + from: number; + to: number; +}[]) => PartialParse; +/** +Each [node type](#common.NodeType) or [individual tree](#common.Tree) +can have metadata associated with it in props. Instances of this +class represent prop names. +*/ +declare class NodeProp { /** - Instances of this class act as extensions. + Indicates whether this prop is stored per [node + type](#common.NodeType) or per [tree node](#common.Tree). */ - extension: Extension; - private constructor(); + perNode: boolean; /** - Define a plugin from a constructor function that creates the - plugin's value, given an editor view. + A method that deserializes a value of this prop from a string. + Can be used to allow a prop to be directly written in a grammar + file. */ - static define(create: (view: EditorView) => V, spec?: PluginSpec): ViewPlugin; + deserialize: (str: string) => T; /** - Create a plugin for a class whose constructor takes a single - editor view as argument. - */ - static fromClass(cls: { - new (view: EditorView): V; - }, spec?: PluginSpec): ViewPlugin; -} -interface MeasureRequest { + Create a new node prop type. + */ + constructor(config?: { + /** + The [deserialize](#common.NodeProp.deserialize) function to + use for this prop, used for example when directly providing + the prop from a grammar file. Defaults to a function that + raises an error. + */ + deserialize?: (str: string) => T; + /** + By default, node props are stored in the [node + type](#common.NodeType). It can sometimes be useful to directly + store information (usually related to the parsing algorithm) + in [nodes](#common.Tree) themselves. Set this to true to enable + that for this prop. + */ + perNode?: boolean; + }); /** - Called in a DOM read phase to gather information that requires - DOM layout. Should _not_ mutate the document. + This is meant to be used with + [`NodeSet.extend`](#common.NodeSet.extend) or + [`LRParser.configure`](#lr.ParserConfig.props) to compute + prop values for each node type in the set. Takes a [match + object](#common.NodeType^match) or function that returns undefined + if the node type doesn't get this prop, and the prop's value if + it does. */ - read(view: EditorView): T; + add(match: { + [selector: string]: T; + } | ((type: NodeType) => T | undefined)): NodePropSource; /** - Called in a DOM write phase to update the document. Should _not_ - do anything that triggers DOM layout. + Prop that is used to describe matching delimiters. For opening + delimiters, this holds an array of node names (written as a + space-separated string when declaring this prop in a grammar) + for the node types of closing delimiters that match it. */ - write?(measure: T, view: EditorView): void; + static closedBy: NodeProp; /** - When multiple requests with the same key are scheduled, only the - last one will actually be run. + The inverse of [`closedBy`](#common.NodeProp^closedBy). This is + attached to closing delimiters, holding an array of node names + of types of matching opening delimiters. */ - key?: any; -} -type AttrSource = Attrs | ((view: EditorView) => Attrs | null); -/** -View [plugins](https://codemirror.net/6/docs/ref/#view.ViewPlugin) are given instances of this -class, which describe what happened, whenever the view is updated. -*/ -declare class ViewUpdate { + static openedBy: NodeProp; /** - The editor view that the update is associated with. + Used to assign node types to groups (for example, all node + types that represent an expression could be tagged with an + `"Expression"` group). */ - readonly view: EditorView; + static group: NodeProp; /** - The new editor state. + Attached to nodes to indicate these should be + [displayed](https://codemirror.net/docs/ref/#language.syntaxTree) + in a bidirectional text isolate, so that direction-neutral + characters on their sides don't incorrectly get associated with + surrounding text. You'll generally want to set this for nodes + that contain arbitrary text, like strings and comments, and for + nodes that appear _inside_ arbitrary text, like HTML tags. When + not given a value, in a grammar declaration, defaults to + `"auto"`. */ - readonly state: EditorState; + static isolate: NodeProp<"rtl" | "ltr" | "auto">; /** - The transactions involved in the update. May be empty. + The hash of the [context](#lr.ContextTracker.constructor) + that the node was parsed in, if any. Used to limit reuse of + contextual nodes. */ - readonly transactions: readonly Transaction[]; + static contextHash: NodeProp; /** - The changes made to the document by this update. + The distance beyond the end of the node that the tokenizer + looked ahead for any of the tokens inside the node. (The LR + parser only stores this when it is larger than 25, for + efficiency reasons.) */ - readonly changes: ChangeSet; + static lookAhead: NodeProp; /** - The previous editor state. + This per-node prop is used to replace a given node, or part of a + node, with another tree. This is useful to include trees from + different languages in mixed-language parsers. */ - readonly startState: EditorState; - private constructor(); + static mounted: NodeProp; +} +/** +A mounted tree, which can be [stored](#common.NodeProp^mounted) on +a tree node to indicate that parts of its content are +represented by another tree. +*/ +declare class MountedTree { /** - Tells you whether the [viewport](https://codemirror.net/6/docs/ref/#view.EditorView.viewport) or - [visible ranges](https://codemirror.net/6/docs/ref/#view.EditorView.visibleRanges) changed in this - update. + The inner tree. */ - get viewportChanged(): boolean; + readonly tree: Tree; /** - Indicates whether the height of a block element in the editor - changed in this update. + If this is null, this tree replaces the entire node (it will + be included in the regular iteration instead of its host + node). If not, only the given ranges are considered to be + covered by this tree. This is used for trees that are mixed in + a way that isn't strictly hierarchical. Such mounted trees are + only entered by [`resolveInner`](#common.Tree.resolveInner) + and [`enter`](#common.SyntaxNode.enter). */ - get heightChanged(): boolean; + readonly overlay: readonly { + from: number; + to: number; + }[] | null; /** - Returns true when the document was modified or the size of the - editor, or elements within the editor, changed. + The parser used to create this subtree. */ - get geometryChanged(): boolean; + readonly parser: Parser; + constructor( /** - True when this update indicates a focus change. + The inner tree. */ - get focusChanged(): boolean; + tree: Tree, /** - Whether the document changed in this update. + If this is null, this tree replaces the entire node (it will + be included in the regular iteration instead of its host + node). If not, only the given ranges are considered to be + covered by this tree. This is used for trees that are mixed in + a way that isn't strictly hierarchical. Such mounted trees are + only entered by [`resolveInner`](#common.Tree.resolveInner) + and [`enter`](#common.SyntaxNode.enter). */ - get docChanged(): boolean; + overlay: readonly { + from: number; + to: number; + }[] | null, /** - Whether the selection was explicitly set in this update. + The parser used to create this subtree. */ - get selectionSet(): boolean; + parser: Parser); } - /** -Interface that objects registered with -[`EditorView.mouseSelectionStyle`](https://codemirror.net/6/docs/ref/#view.EditorView^mouseSelectionStyle) -must conform to. +Type returned by [`NodeProp.add`](#common.NodeProp.add). Describes +whether a prop should be added to a given node type in a node set, +and what value it should have. */ -interface MouseSelectionStyle { +type NodePropSource = (type: NodeType) => null | [NodeProp, any]; +/** +Each node in a syntax tree has a node type associated with it. +*/ +declare class NodeType { /** - Return a new selection for the mouse gesture that starts with - the event that was originally given to the constructor, and ends - with the event passed here. In case of a plain click, those may - both be the `mousedown` event, in case of a drag gesture, the - latest `mousemove` event will be passed. - - When `extend` is true, that means the new selection should, if - possible, extend the start selection. If `multiple` is true, the - new selection should be added to the original selection. + The name of the node type. Not necessarily unique, but if the + grammar was written properly, different node types with the + same name within a node set should play the same semantic + role. */ - get: (curEvent: MouseEvent, extend: boolean, multiple: boolean) => EditorSelection; + readonly name: string; /** - Called when the view is updated while the gesture is in - progress. When the document changes, it may be necessary to map - some data (like the original selection or start position) - through the changes. - - This may return `true` to indicate that the `get` method should - get queried again after the update, because something in the - update could change its result. Be wary of infinite loops when - using this (where `get` returns a new selection, which will - trigger `update`, which schedules another `get` in response). + The id of this node in its set. Corresponds to the term ids + used in the parser. */ - update: (update: ViewUpdate) => boolean | void; -} -type MakeSelectionStyle = (view: EditorView, event: MouseEvent) => MouseSelectionStyle | null; - -/** -Record used to represent information about a block-level element -in the editor view. -*/ -declare class BlockInfo { + readonly id: number; /** - The start of the element in the document. + Define a node type. */ - readonly from: number; + static define(spec: { + /** + The ID of the node type. When this type is used in a + [set](#common.NodeSet), the ID must correspond to its index in + the type array. + */ + id: number; + /** + The name of the node type. Leave empty to define an anonymous + node. + */ + name?: string; + /** + [Node props](#common.NodeProp) to assign to the type. The value + given for any given prop should correspond to the prop's type. + */ + props?: readonly ([NodeProp, any] | NodePropSource)[]; + /** + Whether this is a [top node](#common.NodeType.isTop). + */ + top?: boolean; + /** + Whether this node counts as an [error + node](#common.NodeType.isError). + */ + error?: boolean; + /** + Whether this node is a [skipped](#common.NodeType.isSkipped) + node. + */ + skipped?: boolean; + }): NodeType; /** - The length of the element. + Retrieves a node prop for this type. Will return `undefined` if + the prop isn't present on this node. */ - readonly length: number; + prop(prop: NodeProp): T | undefined; /** - The top position of the element (relative to the top of the - document). + True when this is the top node of a grammar. */ - readonly top: number; + get isTop(): boolean; /** - Its height. + True when this node is produced by a skip rule. */ - readonly height: number; + get isSkipped(): boolean; /** - The type of element this is. When querying lines, this may be - an array of all the blocks that make up the line. + Indicates whether this is an error node. */ - get type(): BlockType | readonly BlockInfo[]; + get isError(): boolean; /** - The end of the element as a document position. + When true, this node type doesn't correspond to a user-declared + named node, for example because it is used to cache repetition. */ - get to(): number; + get isAnonymous(): boolean; /** - The bottom position of the element. + Returns true when this node's name or one of its + [groups](#common.NodeProp^group) matches the given string. */ - get bottom(): number; + is(name: string | number): boolean; /** - If this is a widget block, this will return the widget - associated with it. + An empty dummy node type to use when no actual type is available. */ - get widget(): WidgetType | null; + static none: NodeType; /** - If this is a textblock, this holds the number of line breaks - that appear in widgets inside the block. + Create a function from node types to arbitrary values by + specifying an object whose property names are node or + [group](#common.NodeProp^group) names. Often useful with + [`NodeProp.add`](#common.NodeProp.add). You can put multiple + names, separated by spaces, in a single property name to map + multiple node names to a single value. */ - get widgetLineBreaks(): number; + static match(map: { + [selector: string]: T; + }): (node: NodeType) => T | undefined; } - /** -The type of object given to the [`EditorView`](https://codemirror.net/6/docs/ref/#view.EditorView) -constructor. +A node set holds a collection of node types. It is used to +compactly represent trees by storing their type ids, rather than a +full pointer to the type object, in a numeric array. Each parser +[has](#lr.LRParser.nodeSet) a node set, and [tree +buffers](#common.TreeBuffer) can only store collections of nodes +from the same set. A set can have a maximum of 2**16 (65536) node +types in it, so that the ids fit into 16-bit typed array slots. */ -interface EditorViewConfig extends EditorStateConfig { +declare class NodeSet { /** - The view's initial state. If not given, a new state is created - by passing this configuration object to - [`EditorState.create`](https://codemirror.net/6/docs/ref/#state.EditorState^create), using its - `doc`, `selection`, and `extensions` field (if provided). + The node types in this set, by id. */ - state?: EditorState; + readonly types: readonly NodeType[]; /** - When given, the editor is immediately appended to the given - element on creation. (Otherwise, you'll have to place the view's - [`dom`](https://codemirror.net/6/docs/ref/#view.EditorView.dom) element in the document yourself.) + Create a set with the given types. The `id` property of each + type should correspond to its position within the array. */ - parent?: Element | DocumentFragment; + constructor( /** - If the view is going to be mounted in a shadow root or document - other than the one held by the global variable `document` (the - default), you should pass it here. If you provide `parent`, but - not this option, the editor will automatically look up a root - from the parent. + The node types in this set, by id. */ - root?: Document | ShadowRoot; + types: readonly NodeType[]); /** - Pass an effect created with - [`EditorView.scrollIntoView`](https://codemirror.net/6/docs/ref/#view.EditorView^scrollIntoView) or - [`EditorView.scrollSnapshot`](https://codemirror.net/6/docs/ref/#view.EditorView.scrollSnapshot) - here to set an initial scroll position. + Create a copy of this set with some node properties added. The + arguments to this method can be created with + [`NodeProp.add`](#common.NodeProp.add). */ - scrollTo?: StateEffect; + extend(...props: NodePropSource[]): NodeSet; +} +/** +Options that control iteration. Can be combined with the `|` +operator to enable multiple ones. +*/ +declare enum IterMode { /** - Override the way transactions are - [dispatched](https://codemirror.net/6/docs/ref/#view.EditorView.dispatch) for this editor view. - Your implementation, if provided, should probably call the - view's [`update` method](https://codemirror.net/6/docs/ref/#view.EditorView.update). + When enabled, iteration will only visit [`Tree`](#common.Tree) + objects, not nodes packed into + [`TreeBuffer`](#common.TreeBuffer)s. */ - dispatchTransactions?: (trs: readonly Transaction[], view: EditorView) => void; + ExcludeBuffers = 1, /** - **Deprecated** single-transaction version of - `dispatchTransactions`. Will force transactions to be dispatched - one at a time when used. + Enable this to make iteration include anonymous nodes (such as + the nodes that wrap repeated grammar constructs into a balanced + tree). */ - dispatch?: (tr: Transaction, view: EditorView) => void; + IncludeAnonymous = 2, + /** + By default, regular [mounted](#common.NodeProp^mounted) nodes + replace their base node in iteration. Enable this to ignore them + instead. + */ + IgnoreMounts = 4, + /** + This option only applies in + [`enter`](#common.SyntaxNode.enter)-style methods. It tells the + library to not enter mounted overlays if one covers the given + position. + */ + IgnoreOverlays = 8 } /** -An editor view represents the editor's user interface. It holds -the editable DOM surface, and possibly other elements such as the -line number gutter. It handles events and dispatches state -transactions for editing actions. +A piece of syntax tree. There are two ways to approach these +trees: the way they are actually stored in memory, and the +convenient way. + +Syntax trees are stored as a tree of `Tree` and `TreeBuffer` +objects. By packing detail information into `TreeBuffer` leaf +nodes, the representation is made a lot more memory-efficient. + +However, when you want to actually work with tree nodes, this +representation is very awkward, so most client code will want to +use the [`TreeCursor`](#common.TreeCursor) or +[`SyntaxNode`](#common.SyntaxNode) interface instead, which provides +a view on some part of this data structure, and can be used to +move around to adjacent nodes. */ -declare class EditorView { +declare class Tree { /** - The current editor state. + The type of the top node. */ - get state(): EditorState; + readonly type: NodeType; /** - To be able to display large documents without consuming too much - memory or overloading the browser, CodeMirror only draws the - code that is visible (plus a margin around it) to the DOM. This - property tells you the extent of the current drawn viewport, in - document positions. + This node's child nodes. */ - get viewport(): { - from: number; - to: number; - }; + readonly children: readonly (Tree | TreeBuffer)[]; /** - When there are, for example, large collapsed ranges in the - viewport, its size can be a lot bigger than the actual visible - content. Thus, if you are doing something like styling the - content in the viewport, it is preferable to only do so for - these ranges, which are the subset of the viewport that is - actually drawn. + The positions (offsets relative to the start of this tree) of + the children. */ - get visibleRanges(): readonly { - from: number; - to: number; - }[]; + readonly positions: readonly number[]; /** - Returns false when the editor is entirely scrolled out of view - or otherwise hidden. + The total length of this tree */ - get inView(): boolean; + readonly length: number; /** - Indicates whether the user is currently composing text via - [IME](https://en.wikipedia.org/wiki/Input_method), and at least - one change has been made in the current composition. + Construct a new tree. See also [`Tree.build`](#common.Tree^build). */ - get composing(): boolean; + constructor( /** - Indicates whether the user is currently in composing state. Note - that on some platforms, like Android, this will be the case a - lot, since just putting the cursor on a word starts a - composition there. + The type of the top node. */ - get compositionStarted(): boolean; - private dispatchTransactions; - private _root; + type: NodeType, /** - The document or shadow root that the view lives in. + This node's child nodes. */ - get root(): DocumentOrShadowRoot; + children: readonly (Tree | TreeBuffer)[], /** - The DOM element that wraps the entire editor view. + The positions (offsets relative to the start of this tree) of + the children. */ - readonly dom: HTMLElement; + positions: readonly number[], /** - The DOM element that can be styled to scroll. (Note that it may - not have been, so you can't assume this is scrollable.) + The total length of this tree */ - readonly scrollDOM: HTMLElement; + length: number, /** - The editable DOM element holding the editor content. You should - not, usually, interact with this content directly though the - DOM, since the editor will immediately undo most of the changes - you make. Instead, [dispatch](https://codemirror.net/6/docs/ref/#view.EditorView.dispatch) - [transactions](https://codemirror.net/6/docs/ref/#state.Transaction) to modify content, and - [decorations](https://codemirror.net/6/docs/ref/#view.Decoration) to style it. + Per-node [node props](#common.NodeProp) to associate with this node. */ - readonly contentDOM: HTMLElement; - private announceDOM; - private plugins; - private pluginMap; - private editorAttrs; - private contentAttrs; - private styleModules; - private bidiCache; - private destroyed; + props?: readonly [NodeProp | number, any][]); /** - Construct a new view. You'll want to either provide a `parent` - option, or put `view.dom` into your document after creating a - view, so that the user can see the editor. + The empty tree */ - constructor(config?: EditorViewConfig); + static empty: Tree; /** - All regular editor state updates should go through this. It - takes a transaction, array of transactions, or transaction spec - and updates the view to show the new state produced by that - transaction. Its implementation can be overridden with an - [option](https://codemirror.net/6/docs/ref/#view.EditorView.constructor^config.dispatchTransactions). - This function is bound to the view instance, so it does not have - to be called as a method. - - Note that when multiple `TransactionSpec` arguments are - provided, these define a single transaction (the specs will be - merged), not a sequence of transactions. + Get a [tree cursor](#common.TreeCursor) positioned at the top of + the tree. Mode can be used to [control](#common.IterMode) which + nodes the cursor visits. */ - dispatch(tr: Transaction): void; - dispatch(trs: readonly Transaction[]): void; - dispatch(...specs: TransactionSpec[]): void; + cursor(mode?: IterMode): TreeCursor; /** - Update the view for the given array of transactions. This will - update the visible document and selection to match the state - produced by the transactions, and notify view plugins of the - change. You should usually call - [`dispatch`](https://codemirror.net/6/docs/ref/#view.EditorView.dispatch) instead, which uses this - as a primitive. + Get a [tree cursor](#common.TreeCursor) pointing into this tree + at the given position and side (see + [`moveTo`](#common.TreeCursor.moveTo). */ - update(transactions: readonly Transaction[]): void; + cursorAt(pos: number, side?: -1 | 0 | 1, mode?: IterMode): TreeCursor; /** - Reset the view to the given state. (This will cause the entire - document to be redrawn and all view plugins to be reinitialized, - so you should probably only use it when the new state isn't - derived from the old state. Otherwise, use - [`dispatch`](https://codemirror.net/6/docs/ref/#view.EditorView.dispatch) instead.) + Get a [syntax node](#common.SyntaxNode) object for the top of the + tree. */ - setState(newState: EditorState): void; - private updatePlugins; - private docViewUpdate; + get topNode(): SyntaxNode; + /** + Get the [syntax node](#common.SyntaxNode) at the given position. + If `side` is -1, this will move into nodes that end at the + position. If 1, it'll move into nodes that start at the + position. With 0, it'll only enter nodes that cover the position + from both sides. + + Note that this will not enter + [overlays](#common.MountedTree.overlay), and you often want + [`resolveInner`](#common.Tree.resolveInner) instead. + */ + resolve(pos: number, side?: -1 | 0 | 1): SyntaxNode; /** - Get the CSS classes for the currently active editor themes. + Like [`resolve`](#common.Tree.resolve), but will enter + [overlaid](#common.MountedTree.overlay) nodes, producing a syntax node + pointing into the innermost overlaid tree at the given position + (with parent links going through all parent structure, including + the host trees). */ - get themeClasses(): string; - private updateAttrs; - private showAnnouncements; - private mountStyles; - private readMeasured; + resolveInner(pos: number, side?: -1 | 0 | 1): SyntaxNode; /** - Schedule a layout measurement, optionally providing callbacks to - do custom DOM measuring followed by a DOM write phase. Using - this is preferable reading DOM layout directly from, for - example, an event handler, because it'll make sure measuring and - drawing done by other components is synchronized, avoiding - unnecessary DOM layout computations. + In some situations, it can be useful to iterate through all + nodes around a position, including those in overlays that don't + directly cover the position. This method gives you an iterator + that will produce all nodes, from small to big, around the given + position. */ - requestMeasure(request?: MeasureRequest): void; + resolveStack(pos: number, side?: -1 | 0 | 1): NodeIterator; /** - Get the value of a specific plugin, if present. Note that - plugins that crash can be dropped from a view, so even when you - know you registered a given plugin, it is recommended to check - the return value of this method. + Iterate over the tree and its children, calling `enter` for any + node that touches the `from`/`to` region (if given) before + running over such a node's children, and `leave` (if given) when + leaving the node. When `enter` returns `false`, that node will + not have its children iterated over (or `leave` called). */ - plugin(plugin: ViewPlugin): T | null; + iterate(spec: { + enter(node: SyntaxNodeRef): boolean | void; + leave?(node: SyntaxNodeRef): void; + from?: number; + to?: number; + mode?: IterMode; + }): void; /** - The top position of the document, in screen coordinates. This - may be negative when the editor is scrolled down. Points - directly to the top of the first line, not above the padding. + Get the value of the given [node prop](#common.NodeProp) for this + node. Works with both per-node and per-type props. */ - get documentTop(): number; + prop(prop: NodeProp): T | undefined; /** - Reports the padding above and below the document. + Returns the node's [per-node props](#common.NodeProp.perNode) in a + format that can be passed to the [`Tree`](#common.Tree) + constructor. */ - get documentPadding(): { - top: number; - bottom: number; - }; + get propValues(): readonly [NodeProp | number, any][]; /** - If the editor is transformed with CSS, this provides the scale - along the X axis. Otherwise, it will just be 1. Note that - transforms other than translation and scaling are not supported. + Balance the direct children of this tree, producing a copy of + which may have children grouped into subtrees with type + [`NodeType.none`](#common.NodeType^none). */ - get scaleX(): number; + balance(config?: { + /** + Function to create the newly balanced subtrees. + */ + makeTree?: (children: readonly (Tree | TreeBuffer)[], positions: readonly number[], length: number) => Tree; + }): Tree; /** - Provide the CSS transformed scale along the Y axis. + Build a tree from a postfix-ordered buffer of node information, + or a cursor over such a buffer. */ - get scaleY(): number; + static build(data: BuildData): Tree; +} +/** +Represents a sequence of nodes. +*/ +type NodeIterator = { + node: SyntaxNode; + next: NodeIterator | null; +}; +type BuildData = { /** - Find the text line or block widget at the given vertical - position (which is interpreted as relative to the [top of the - document](https://codemirror.net/6/docs/ref/#view.EditorView.documentTop)). + The buffer or buffer cursor to read the node data from. + + When this is an array, it should contain four values for every + node in the tree. + + - The first holds the node's type, as a node ID pointing into + the given `NodeSet`. + - The second holds the node's start offset. + - The third the end offset. + - The fourth the amount of space taken up in the array by this + node and its children. Since there's four values per node, + this is the total number of nodes inside this node (children + and transitive children) plus one for the node itself, times + four. + + Parent nodes should appear _after_ child nodes in the array. As + an example, a node of type 10 spanning positions 0 to 4, with + two children, of type 11 and 12, might look like this: + + [11, 0, 1, 4, 12, 2, 4, 4, 10, 0, 4, 12] */ - elementAtHeight(height: number): BlockInfo; + buffer: BufferCursor | readonly number[]; /** - Find the line block (see - [`lineBlockAt`](https://codemirror.net/6/docs/ref/#view.EditorView.lineBlockAt) at the given - height, again interpreted relative to the [top of the - document](https://codemirror.net/6/docs/ref/#view.EditorView.documentTop). + The node types to use. */ - lineBlockAtHeight(height: number): BlockInfo; + nodeSet: NodeSet; /** - Get the extent and vertical position of all [line - blocks](https://codemirror.net/6/docs/ref/#view.EditorView.lineBlockAt) in the viewport. Positions - are relative to the [top of the - document](https://codemirror.net/6/docs/ref/#view.EditorView.documentTop); + The id of the top node type. */ - get viewportLineBlocks(): BlockInfo[]; + topID: number; /** - Find the line block around the given document position. A line - block is a range delimited on both sides by either a - non-[hidden](https://codemirror.net/6/docs/ref/#view.Decoration^replace) line breaks, or the - start/end of the document. It will usually just hold a line of - text, but may be broken into multiple textblocks by block - widgets. + The position the tree should start at. Defaults to 0. */ - lineBlockAt(pos: number): BlockInfo; + start?: number; /** - The editor's total content height. + The position in the buffer where the function should stop + reading. Defaults to 0. */ - get contentHeight(): number; + bufferStart?: number; /** - Move a cursor position by [grapheme - cluster](https://codemirror.net/6/docs/ref/#state.findClusterBreak). `forward` determines whether - the motion is away from the line start, or towards it. In - bidirectional text, the line is traversed in visual order, using - the editor's [text direction](https://codemirror.net/6/docs/ref/#view.EditorView.textDirection). - When the start position was the last one on the line, the - returned position will be across the line break. If there is no - further line, the original position is returned. - - By default, this method moves over a single cluster. The - optional `by` argument can be used to move across more. It will - be called with the first cluster as argument, and should return - a predicate that determines, for each subsequent cluster, - whether it should also be moved over. + The length of the wrapping node. The end offset of the last + child is used when not provided. */ - moveByChar(start: SelectionRange, forward: boolean, by?: (initial: string) => (next: string) => boolean): SelectionRange; + length?: number; /** - Move a cursor position across the next group of either - [letters](https://codemirror.net/6/docs/ref/#state.EditorState.charCategorizer) or non-letter - non-whitespace characters. + The maximum buffer length to use. Defaults to + [`DefaultBufferLength`](#common.DefaultBufferLength). */ - moveByGroup(start: SelectionRange, forward: boolean): SelectionRange; + maxBufferLength?: number; /** - Get the cursor position visually at the start or end of a line. - Note that this may differ from the _logical_ position at its - start or end (which is simply at `line.from`/`line.to`) if text - at the start or end goes against the line's base text direction. + An optional array holding reused nodes that the buffer can refer + to. */ - visualLineSide(line: Line$1, end: boolean): SelectionRange; + reused?: readonly Tree[]; /** - Move to the next line boundary in the given direction. If - `includeWrap` is true, line wrapping is on, and there is a - further wrap point on the current line, the wrap point will be - returned. Otherwise this function will return the start or end - of the line. + The first node type that indicates repeat constructs in this + grammar. */ - moveToLineBoundary(start: SelectionRange, forward: boolean, includeWrap?: boolean): SelectionRange; + minRepeatType?: number; +}; +/** +This is used by `Tree.build` as an abstraction for iterating over +a tree buffer. A cursor initially points at the very last element +in the buffer. Every time `next()` is called it moves on to the +previous one. +*/ +interface BufferCursor { /** - Move a cursor position vertically. When `distance` isn't given, - it defaults to moving to the next line (including wrapped - lines). Otherwise, `distance` should provide a positive distance - in pixels. - - When `start` has a - [`goalColumn`](https://codemirror.net/6/docs/ref/#state.SelectionRange.goalColumn), the vertical - motion will use that as a target horizontal position. Otherwise, - the cursor's own horizontal position is used. The returned - cursor will have its goal column set to whichever column was - used. + The current buffer position (four times the number of nodes + remaining). */ - moveVertically(start: SelectionRange, forward: boolean, distance?: number): SelectionRange; + pos: number; /** - Find the DOM parent node and offset (child offset if `node` is - an element, character offset when it is a text node) at the - given document position. - - Note that for positions that aren't currently in - `visibleRanges`, the resulting DOM position isn't necessarily - meaningful (it may just point before or after a placeholder - element). + The node ID of the next node in the buffer. */ - domAtPos(pos: number): { - node: Node; - offset: number; - }; + id: number; /** - Find the document position at the given DOM node. Can be useful - for associating positions with DOM events. Will raise an error - when `node` isn't part of the editor content. + The start position of the next node in the buffer. */ - posAtDOM(node: Node, offset?: number): number; + start: number; /** - Get the document position at the given screen coordinates. For - positions not covered by the visible viewport's DOM structure, - this will return null, unless `false` is passed as second - argument, in which case it'll return an estimated position that - would be near the coordinates if it were rendered. + The end position of the next node. */ - posAtCoords(coords: { - x: number; - y: number; - }, precise: false): number; - posAtCoords(coords: { - x: number; - y: number; - }): number | null; + end: number; /** - Get the screen coordinates at the given document position. - `side` determines whether the coordinates are based on the - element before (-1) or after (1) the position (if no element is - available on the given side, the method will transparently use - another strategy to get reasonable coordinates). + The size of the next node (the number of nodes inside, counting + the node itself, times 4). */ - coordsAtPos(pos: number, side?: -1 | 1): Rect | null; + size: number; /** - Return the rectangle around a given character. If `pos` does not - point in front of a character that is in the viewport and - rendered (i.e. not replaced, not a line break), this will return - null. For space characters that are a line wrap point, this will - return the position before the line break. + Moves `this.pos` down by 4. */ - coordsForChar(pos: number): Rect | null; + next(): void; /** - The default width of a character in the editor. May not - accurately reflect the width of all characters (given variable - width fonts or styling of invididual ranges). + Create a copy of this cursor. */ - get defaultCharacterWidth(): number; + fork(): BufferCursor; +} +/** +Tree buffers contain (type, start, end, endIndex) quads for each +node. In such a buffer, nodes are stored in prefix order (parents +before children, with the endIndex of the parent indicating which +children belong to it). +*/ +declare class TreeBuffer { /** - The default height of a line in the editor. May not be accurate - for all lines. + The buffer's content. */ - get defaultLineHeight(): number; + readonly buffer: Uint16Array; /** - The text direction - ([`direction`](https://developer.mozilla.org/en-US/docs/Web/CSS/direction) - CSS property) of the editor's content element. + The total length of the group of nodes in the buffer. */ - get textDirection(): Direction; + readonly length: number; /** - Find the text direction of the block at the given position, as - assigned by CSS. If - [`perLineTextDirection`](https://codemirror.net/6/docs/ref/#view.EditorView^perLineTextDirection) - isn't enabled, or the given position is outside of the viewport, - this will always return the same as - [`textDirection`](https://codemirror.net/6/docs/ref/#view.EditorView.textDirection). Note that - this may trigger a DOM layout. + The node set used in this buffer. */ - textDirectionAt(pos: number): Direction; + readonly set: NodeSet; /** - Whether this editor [wraps lines](https://codemirror.net/6/docs/ref/#view.EditorView.lineWrapping) - (as determined by the - [`white-space`](https://developer.mozilla.org/en-US/docs/Web/CSS/white-space) - CSS property of its content element). + Create a tree buffer. */ - get lineWrapping(): boolean; + constructor( /** - Returns the bidirectional text structure of the given line - (which should be in the current document) as an array of span - objects. The order of these spans matches the [text - direction](https://codemirror.net/6/docs/ref/#view.EditorView.textDirection)—if that is - left-to-right, the leftmost spans come first, otherwise the - rightmost spans come first. + The buffer's content. */ - bidiSpans(line: Line$1): readonly BidiSpan[]; + buffer: Uint16Array, /** - Check whether the editor has focus. + The total length of the group of nodes in the buffer. */ - get hasFocus(): boolean; + length: number, /** - Put focus on the editor. + The node set used in this buffer. */ - focus(): void; + set: NodeSet); +} +/** +The set of properties provided by both [`SyntaxNode`](#common.SyntaxNode) +and [`TreeCursor`](#common.TreeCursor). Note that, if you need +an object that is guaranteed to stay stable in the future, you +need to use the [`node`](#common.SyntaxNodeRef.node) accessor. +*/ +interface SyntaxNodeRef { /** - Update the [root](https://codemirror.net/6/docs/ref/##view.EditorViewConfig.root) in which the editor lives. This is only - necessary when moving the editor's existing DOM to a new window or shadow root. + The start position of the node. */ - setRoot(root: Document | ShadowRoot): void; + readonly from: number; /** - Clean up this editor view, removing its element from the - document, unregistering event handlers, and notifying - plugins. The view instance can no longer be used after - calling this. + The end position of the node. */ - destroy(): void; + readonly to: number; /** - Returns an effect that can be - [added](https://codemirror.net/6/docs/ref/#state.TransactionSpec.effects) to a transaction to - cause it to scroll the given position or range into view. + The type of the node. */ - static scrollIntoView(pos: number | SelectionRange, options?: { - /** - By default (`"nearest"`) the position will be vertically - scrolled only the minimal amount required to move the given - position into view. You can set this to `"start"` to move it - to the top of the view, `"end"` to move it to the bottom, or - `"center"` to move it to the center. - */ - y?: ScrollStrategy; - /** - Effect similar to - [`y`](https://codemirror.net/6/docs/ref/#view.EditorView^scrollIntoView^options.y), but for the - horizontal scroll position. - */ - x?: ScrollStrategy; - /** - Extra vertical distance to add when moving something into - view. Not used with the `"center"` strategy. Defaults to 5. - Must be less than the height of the editor. - */ - yMargin?: number; - /** - Extra horizontal distance to add. Not used with the `"center"` - strategy. Defaults to 5. Must be less than the width of the - editor. - */ - xMargin?: number; - }): StateEffect; + readonly type: NodeType; /** - Return an effect that resets the editor to its current (at the - time this method was called) scroll position. Note that this - only affects the editor's own scrollable element, not parents. - See also - [`EditorViewConfig.scrollTo`](https://codemirror.net/6/docs/ref/#view.EditorViewConfig.scrollTo). - - The effect should be used with a document identical to the one - it was created for. Failing to do so is not an error, but may - not scroll to the expected position. You can - [map](https://codemirror.net/6/docs/ref/#state.StateEffect.map) the effect to account for changes. + The name of the node (`.type.name`). */ - scrollSnapshot(): StateEffect; + readonly name: string; /** - Facet to add a [style - module](https://github.com/marijnh/style-mod#documentation) to - an editor view. The view will ensure that the module is - mounted in its [document - root](https://codemirror.net/6/docs/ref/#view.EditorView.constructor^config.root). + Get the [tree](#common.Tree) that represents the current node, + if any. Will return null when the node is in a [tree + buffer](#common.TreeBuffer). */ - static styleModule: Facet; + readonly tree: Tree | null; /** - Returns an extension that can be used to add DOM event handlers. - The value should be an object mapping event names to handler - functions. For any given event, such functions are ordered by - extension precedence, and the first handler to return true will - be assumed to have handled that event, and no other handlers or - built-in behavior will be activated for it. These are registered - on the [content element](https://codemirror.net/6/docs/ref/#view.EditorView.contentDOM), except - for `scroll` handlers, which will be called any time the - editor's [scroll element](https://codemirror.net/6/docs/ref/#view.EditorView.scrollDOM) or one of - its parent nodes is scrolled. + Retrieve a stable [syntax node](#common.SyntaxNode) at this + position. */ - static domEventHandlers(handlers: DOMEventHandlers): Extension; + readonly node: SyntaxNode; /** - Create an extension that registers DOM event observers. Contrary - to event [handlers](https://codemirror.net/6/docs/ref/#view.EditorView^domEventHandlers), - observers can't be prevented from running by a higher-precedence - handler returning true. They also don't prevent other handlers - and observers from running when they return true, and should not - call `preventDefault`. + Test whether the node matches a given context—a sequence of + direct parent nodes. Empty strings in the context array act as + wildcards, other strings must match the ancestor node's name. */ - static domEventObservers(observers: DOMEventHandlers): Extension; + matchContext(context: readonly string[]): boolean; +} +/** +A syntax node provides an immutable pointer to a given node in a +tree. When iterating over large amounts of nodes, you may want to +use a mutable [cursor](#common.TreeCursor) instead, which is more +efficient. +*/ +interface SyntaxNode extends SyntaxNodeRef { /** - An input handler can override the way changes to the editable - DOM content are handled. Handlers are passed the document - positions between which the change was found, and the new - content. When one returns true, no further input handlers are - called and the default behavior is prevented. - - The `insert` argument can be used to get the default transaction - that would be applied for this input. This can be useful when - dispatching the custom behavior as a separate transaction. + The node's parent node, if any. */ - static inputHandler: Facet<(view: EditorView, from: number, to: number, text: string, insert: () => Transaction) => boolean, readonly ((view: EditorView, from: number, to: number, text: string, insert: () => Transaction) => boolean)[]>; + parent: SyntaxNode | null; /** - Scroll handlers can override how things are scrolled into view. - If they return `true`, no further handling happens for the - scrolling. If they return false, the default scroll behavior is - applied. Scroll handlers should never initiate editor updates. + The first child, if the node has children. */ - static scrollHandler: Facet<(view: EditorView, range: SelectionRange, options: { - x: ScrollStrategy; - y: ScrollStrategy; - xMargin: number; - yMargin: number; - }) => boolean, readonly ((view: EditorView, range: SelectionRange, options: { - x: ScrollStrategy; - y: ScrollStrategy; - xMargin: number; - yMargin: number; - }) => boolean)[]>; + firstChild: SyntaxNode | null; /** - This facet can be used to provide functions that create effects - to be dispatched when the editor's focus state changes. + The node's last child, if available. */ - static focusChangeEffect: Facet<(state: EditorState, focusing: boolean) => StateEffect | null, readonly ((state: EditorState, focusing: boolean) => StateEffect | null)[]>; + lastChild: SyntaxNode | null; /** - By default, the editor assumes all its content has the same - [text direction](https://codemirror.net/6/docs/ref/#view.Direction). Configure this with a `true` - value to make it read the text direction of every (rendered) - line separately. + The first child that ends after `pos`. */ - static perLineTextDirection: Facet; + childAfter(pos: number): SyntaxNode | null; /** - Allows you to provide a function that should be called when the - library catches an exception from an extension (mostly from view - plugins, but may be used by other extensions to route exceptions - from user-code-provided callbacks). This is mostly useful for - debugging and logging. See [`logException`](https://codemirror.net/6/docs/ref/#view.logException). + The last child that starts before `pos`. */ - static exceptionSink: Facet<(exception: any) => void, readonly ((exception: any) => void)[]>; + childBefore(pos: number): SyntaxNode | null; /** - A facet that can be used to register a function to be called - every time the view updates. + Enter the child at the given position. If side is -1 the child + may end at that position, when 1 it may start there. + + This will by default enter + [overlaid](#common.MountedTree.overlay) + [mounted](#common.NodeProp^mounted) trees. You can set + `overlays` to false to disable that. + + Similarly, when `buffers` is false this will not enter + [buffers](#common.TreeBuffer), only [nodes](#common.Tree) (which + is mostly useful when looking for props, which cannot exist on + buffer-allocated nodes). */ - static updateListener: Facet<(update: ViewUpdate) => void, readonly ((update: ViewUpdate) => void)[]>; + enter(pos: number, side: -1 | 0 | 1, mode?: IterMode): SyntaxNode | null; /** - Facet that controls whether the editor content DOM is editable. - When its highest-precedence value is `false`, the element will - not have its `contenteditable` attribute set. (Note that this - doesn't affect API calls that change the editor content, even - when those are bound to keys or buttons. See the - [`readOnly`](https://codemirror.net/6/docs/ref/#state.EditorState.readOnly) facet for that.) + This node's next sibling, if any. */ - static editable: Facet; + nextSibling: SyntaxNode | null; /** - Allows you to influence the way mouse selection happens. The - functions in this facet will be called for a `mousedown` event - on the editor, and can return an object that overrides the way a - selection is computed from that mouse click or drag. + This node's previous sibling. */ - static mouseSelectionStyle: Facet; + prevSibling: SyntaxNode | null; /** - Facet used to configure whether a given selection drag event - should move or copy the selection. The given predicate will be - called with the `mousedown` event, and can return `true` when - the drag should move the content. + A [tree cursor](#common.TreeCursor) starting at this node. */ - static dragMovesSelection: Facet<(event: MouseEvent) => boolean, readonly ((event: MouseEvent) => boolean)[]>; + cursor(mode?: IterMode): TreeCursor; /** - Facet used to configure whether a given selecting click adds a - new range to the existing selection or replaces it entirely. The - default behavior is to check `event.metaKey` on macOS, and - `event.ctrlKey` elsewhere. + Find the node around, before (if `side` is -1), or after (`side` + is 1) the given position. Will look in parent nodes if the + position is outside this node. */ - static clickAddsSelectionRange: Facet<(event: MouseEvent) => boolean, readonly ((event: MouseEvent) => boolean)[]>; + resolve(pos: number, side?: -1 | 0 | 1): SyntaxNode; /** - A facet that determines which [decorations](https://codemirror.net/6/docs/ref/#view.Decoration) - are shown in the view. Decorations can be provided in two - ways—directly, or via a function that takes an editor view. - - Only decoration sets provided directly are allowed to influence - the editor's vertical layout structure. The ones provided as - functions are called _after_ the new viewport has been computed, - and thus **must not** introduce block widgets or replacing - decorations that cover line breaks. - - If you want decorated ranges to behave like atomic units for - cursor motion and deletion purposes, also provide the range set - containing the decorations to - [`EditorView.atomicRanges`](https://codemirror.net/6/docs/ref/#view.EditorView^atomicRanges). + Similar to `resolve`, but enter + [overlaid](#common.MountedTree.overlay) nodes. */ - static decorations: Facet DecorationSet), readonly (DecorationSet | ((view: EditorView) => DecorationSet))[]>; + resolveInner(pos: number, side?: -1 | 0 | 1): SyntaxNode; /** - Facet that works much like - [`decorations`](https://codemirror.net/6/docs/ref/#view.EditorView^decorations), but puts its - inputs at the very bottom of the precedence stack, meaning mark - decorations provided here will only be split by other, partially - overlapping \`outerDecorations\` ranges, and wrap around all - regular decorations. Use this for mark elements that should, as - much as possible, remain in one piece. + Move the position to the innermost node before `pos` that looks + like it is unfinished (meaning it ends in an error node or has a + child ending in an error node right at its end). */ - static outerDecorations: Facet DecorationSet), readonly (DecorationSet | ((view: EditorView) => DecorationSet))[]>; + enterUnfinishedNodesBefore(pos: number): SyntaxNode; /** - Used to provide ranges that should be treated as atoms as far as - cursor motion is concerned. This causes methods like - [`moveByChar`](https://codemirror.net/6/docs/ref/#view.EditorView.moveByChar) and - [`moveVertically`](https://codemirror.net/6/docs/ref/#view.EditorView.moveVertically) (and the - commands built on top of them) to skip across such regions when - a selection endpoint would enter them. This does _not_ prevent - direct programmatic [selection - updates](https://codemirror.net/6/docs/ref/#state.TransactionSpec.selection) from moving into such - regions. + Get a [tree](#common.Tree) for this node. Will allocate one if it + points into a buffer. */ - static atomicRanges: Facet<(view: EditorView) => RangeSet, readonly ((view: EditorView) => RangeSet)[]>; + toTree(): Tree; /** - When range decorations add a `unicode-bidi: isolate` style, they - should also include a - [`bidiIsolate`](https://codemirror.net/6/docs/ref/#view.MarkDecorationSpec.bidiIsolate) property - in their decoration spec, and be exposed through this facet, so - that the editor can compute the proper text order. (Other values - for `unicode-bidi`, except of course `normal`, are not - supported.) + Get the first child of the given type (which may be a [node + name](#common.NodeType.name) or a [group + name](#common.NodeProp^group)). If `before` is non-null, only + return children that occur somewhere after a node with that name + or group. If `after` is non-null, only return children that + occur somewhere before a node with that name or group. */ - static bidiIsolatedRanges: Facet DecorationSet), readonly (DecorationSet | ((view: EditorView) => DecorationSet))[]>; + getChild(type: string | number, before?: string | number | null, after?: string | number | null): SyntaxNode | null; /** - Facet that allows extensions to provide additional scroll - margins (space around the sides of the scrolling element that - should be considered invisible). This can be useful when the - plugin introduces elements that cover part of that element (for - example a horizontally fixed gutter). + Like [`getChild`](#common.SyntaxNode.getChild), but return all + matching children, not just the first. */ - static scrollMargins: Facet<(view: EditorView) => Partial | null, readonly ((view: EditorView) => Partial | null)[]>; + getChildren(type: string | number, before?: string | number | null, after?: string | number | null): SyntaxNode[]; +} +/** +A tree cursor object focuses on a given node in a syntax tree, and +allows you to move to adjacent nodes. +*/ +declare class TreeCursor implements SyntaxNodeRef { /** - Create a theme extension. The first argument can be a - [`style-mod`](https://github.com/marijnh/style-mod#documentation) - style spec providing the styles for the theme. These will be - prefixed with a generated class for the style. - - Because the selectors will be prefixed with a scope class, rule - that directly match the editor's [wrapper - element](https://codemirror.net/6/docs/ref/#view.EditorView.dom)—to which the scope class will be - added—need to be explicitly differentiated by adding an `&` to - the selector for that element—for example - `&.cm-focused`. - - When `dark` is set to true, the theme will be marked as dark, - which will cause the `&dark` rules from [base - themes](https://codemirror.net/6/docs/ref/#view.EditorView^baseTheme) to be used (as opposed to - `&light` when a light theme is active). + The node's type. */ - static theme(spec: { - [selector: string]: StyleSpec; - }, options?: { - dark?: boolean; - }): Extension; + type: NodeType; /** - This facet records whether a dark theme is active. The extension - returned by [`theme`](https://codemirror.net/6/docs/ref/#view.EditorView^theme) automatically - includes an instance of this when the `dark` option is set to - true. + Shorthand for `.type.name`. */ - static darkTheme: Facet; + get name(): string; /** - Create an extension that adds styles to the base theme. Like - with [`theme`](https://codemirror.net/6/docs/ref/#view.EditorView^theme), use `&` to indicate the - place of the editor wrapper element when directly targeting - that. You can also use `&dark` or `&light` instead to only - target editors with a dark or light theme. + The start source offset of this node. */ - static baseTheme(spec: { - [selector: string]: StyleSpec; - }): Extension; + from: number; /** - Provides a Content Security Policy nonce to use when creating - the style sheets for the editor. Holds the empty string when no - nonce has been provided. + The end source offset. */ - static cspNonce: Facet; + to: number; + private stack; + private bufferNode; + private yieldNode; + private yieldBuf; /** - Facet that provides additional DOM attributes for the editor's - editable DOM element. + Move the cursor to this node's first child. When this returns + false, the node has no child, and the cursor has not been moved. */ - static contentAttributes: Facet; + firstChild(): boolean; /** - Facet that provides DOM attributes for the editor's outer - element. + Move the cursor to this node's last child. */ - static editorAttributes: Facet; + lastChild(): boolean; /** - An extension that enables line wrapping in the editor (by - setting CSS `white-space` to `pre-wrap` in the content). + Move the cursor to the first child that ends after `pos`. */ - static lineWrapping: Extension; + childAfter(pos: number): boolean; /** - State effect used to include screen reader announcements in a - transaction. These will be added to the DOM in a visually hidden - element with `aria-live="polite"` set, and should be used to - describe effects that are visually obvious but may not be - noticed by screen reader users (such as moving to the next - search match). + Move to the last child that starts before `pos`. */ - static announce: StateEffectType; + childBefore(pos: number): boolean; /** - Retrieve an editor view instance from the view's DOM - representation. + Move the cursor to the child around `pos`. If side is -1 the + child may end at that position, when 1 it may start there. This + will also enter [overlaid](#common.MountedTree.overlay) + [mounted](#common.NodeProp^mounted) trees unless `overlays` is + set to false. */ - static findFromDOM(dom: HTMLElement): EditorView | null; -} -/** -Helper type that maps event names to event object types, or the -`any` type for unknown events. -*/ -interface DOMEventMap extends HTMLElementEventMap { - [other: string]: any; -} -/** -Event handlers are specified with objects like this. For event -types known by TypeScript, this will infer the event argument type -to hold the appropriate event object type. For unknown events, it -is inferred to `any`, and should be explicitly set if you want type -checking. -*/ -type DOMEventHandlers = { - [event in keyof DOMEventMap]?: (this: This, event: DOMEventMap[event], view: EditorView) => boolean | void; -}; - -/** -Key bindings associate key names with -[command](https://codemirror.net/6/docs/ref/#view.Command)-style functions. - -Key names may be strings like `"Shift-Ctrl-Enter"`—a key identifier -prefixed with zero or more modifiers. Key identifiers are based on -the strings that can appear in -[`KeyEvent.key`](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key). -Use lowercase letters to refer to letter keys (or uppercase letters -if you want shift to be held). You may use `"Space"` as an alias -for the `" "` name. - -Modifiers can be given in any order. `Shift-` (or `s-`), `Alt-` (or -`a-`), `Ctrl-` (or `c-` or `Control-`) and `Cmd-` (or `m-` or -`Meta-`) are recognized. - -When a key binding contains multiple key names separated by -spaces, it represents a multi-stroke binding, which will fire when -the user presses the given keys after each other. - -You can use `Mod-` as a shorthand for `Cmd-` on Mac and `Ctrl-` on -other platforms. So `Mod-b` is `Ctrl-b` on Linux but `Cmd-b` on -macOS. -*/ -interface KeyBinding { + enter(pos: number, side: -1 | 0 | 1, mode?: IterMode): boolean; /** - The key name to use for this binding. If the platform-specific - property (`mac`, `win`, or `linux`) for the current platform is - used as well in the binding, that one takes precedence. If `key` - isn't defined and the platform-specific binding isn't either, - a binding is ignored. + Move to the node's parent node, if this isn't the top node. */ - key?: string; + parent(): boolean; /** - Key to use specifically on macOS. + Move to this node's next sibling, if any. */ - mac?: string; + nextSibling(): boolean; /** - Key to use specifically on Windows. + Move to this node's previous sibling, if any. */ - win?: string; + prevSibling(): boolean; + private atLastNode; + private move; /** - Key to use specifically on Linux. + Move to the next node in a + [pre-order](https://en.wikipedia.org/wiki/Tree_traversal#Pre-order,_NLR) + traversal, going from a node to its first child or, if the + current node is empty or `enter` is false, its next sibling or + the next sibling of the first parent node that has one. */ - linux?: string; + next(enter?: boolean): boolean; /** - The command to execute when this binding is triggered. When the - command function returns `false`, further bindings will be tried - for the key. + Move to the next node in a last-to-first pre-order traveral. A + node is followed by its last child or, if it has none, its + previous sibling or the previous sibling of the first parent + node that has one. */ - run?: Command; + prev(enter?: boolean): boolean; /** - When given, this defines a second binding, using the (possibly - platform-specific) key name prefixed with `Shift-` to activate - this command. + Move the cursor to the innermost node that covers `pos`. If + `side` is -1, it will enter nodes that end at `pos`. If it is 1, + it will enter nodes that start at `pos`. */ - shift?: Command; + moveTo(pos: number, side?: -1 | 0 | 1): this; /** - When this property is present, the function is called for every - key that is not a multi-stroke prefix. + Get a [syntax node](#common.SyntaxNode) at the cursor's current + position. */ - any?: (view: EditorView, event: KeyboardEvent) => boolean; + get node(): SyntaxNode; /** - By default, key bindings apply when focus is on the editor - content (the `"editor"` scope). Some extensions, mostly those - that define their own panels, might want to allow you to - register bindings local to that panel. Such bindings should use - a custom scope name. You may also assign multiple scope names to - a binding, separating them by spaces. + Get the [tree](#common.Tree) that represents the current node, if + any. Will return null when the node is in a [tree + buffer](#common.TreeBuffer). */ - scope?: string; + get tree(): Tree | null; /** - When set to true (the default is false), this will always - prevent the further handling for the bound key, even if the - command(s) return false. This can be useful for cases where the - native behavior of the key is annoying or irrelevant but the - command doesn't always apply (such as, Mod-u for undo selection, - which would cause the browser to view source instead when no - selection can be undone). + Iterate over the current node and all its descendants, calling + `enter` when entering a node and `leave`, if given, when leaving + one. When `enter` returns `false`, any children of that node are + skipped, and `leave` isn't called for it. */ - preventDefault?: boolean; + iterate(enter: (node: SyntaxNodeRef) => boolean | void, leave?: (node: SyntaxNodeRef) => void): void; /** - When set to true, `stopPropagation` will be called on keyboard - events that have their `preventDefault` called in response to - this key binding (see also - [`preventDefault`](https://codemirror.net/6/docs/ref/#view.KeyBinding.preventDefault)). + Test whether the current node matches a given context—a sequence + of direct parent node names. Empty strings in the context array + are treated as wildcards. */ - stopPropagation?: boolean; + matchContext(context: readonly string[]): boolean; } -/** -Facet used for registering keymaps. -You can add multiple keymaps to an editor. Their priorities -determine their precedence (the ones specified early or with high -priority get checked first). When a handler has returned `true` -for a given key, no further handlers are called. +/** +Objects returned by the function passed to +[`parseMixed`](#common.parseMixed) should conform to this +interface. */ -declare const keymap: Facet; - -type SelectionConfig = { +interface NestedParse { /** - The length of a full cursor blink cycle, in milliseconds. - Defaults to 1200. Can be set to 0 to disable blinking. + The parser to use for the inner region. */ - cursorBlinkRate?: number; + parser: Parser; /** - Whether to show a cursor for non-empty ranges. Defaults to - true. + When this property is not given, the entire node is parsed with + this parser, and it is [mounted](#common.NodeProp^mounted) as a + non-overlay node, replacing its host node in tree iteration. + + When an array of ranges is given, only those ranges are parsed, + and the tree is mounted as an + [overlay](#common.MountedTree.overlay). + + When a function is given, that function will be called for + descendant nodes of the target node, not including child nodes + that are covered by another nested parse, to determine the + overlay ranges. When it returns true, the entire descendant is + included, otherwise just the range given. The mixed parser will + optimize range-finding in reused nodes, which means it's a good + idea to use a function here when the target node is expected to + have a large, deep structure. */ - drawRangeCursor?: boolean; -}; + overlay?: readonly { + from: number; + to: number; + }[] | ((node: SyntaxNodeRef) => { + from: number; + to: number; + } | boolean); +} /** -Returns an extension that hides the browser's native selection and -cursor, replacing the selection with a background behind the text -(with the `cm-selectionBackground` class), and the -cursors with elements overlaid over the code (using -`cm-cursor-primary` and `cm-cursor-secondary`). - -This allows the editor to display secondary selection ranges, and -tends to produce a type of selection more in line with that users -expect in a text editor (the native selection styling will often -leave gaps between lines and won't fill the horizontal space after -a line when the selection continues past it). - -It does have a performance cost, in that it requires an extra DOM -layout cycle for many updates (the selection is drawn based on DOM -layout information that's only available after laying out the -content). +Create a parse wrapper that, after the inner parse completes, +scans its tree for mixed language regions with the `nest` +function, runs the resulting [inner parses](#common.NestedParse), +and then [mounts](#common.NodeProp^mounted) their results onto the +tree. */ -declare function drawSelection(config?: SelectionConfig): Extension; +declare function parseMixed(nest: (node: SyntaxNodeRef, input: Input) => NestedParse | null): ParseWrapper; -interface SpecialCharConfig { +/** +A parse stack. These are used internally by the parser to track +parsing progress. They also provide some properties and methods +that external code such as a tokenizer can use to get information +about the parse state. +*/ +declare class Stack { /** - An optional function that renders the placeholder elements. - - The `description` argument will be text that clarifies what the - character is, which should be provided to screen readers (for - example with the - [`aria-label`](https://www.w3.org/TR/wai-aria/#aria-label) - attribute) and optionally shown to the user in other ways (such - as the - [`title`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/title) - attribute). - - The given placeholder string is a suggestion for how to display - the character visually. + The input position up to which this stack has parsed. */ - render?: ((code: number, description: string | null, placeholder: string) => HTMLElement) | null; + pos: number; /** - Regular expression that matches the special characters to - highlight. Must have its 'g'/global flag set. + The stack's current [context](#lr.ContextTracker) value, if + any. Its type will depend on the context tracker's type + parameter, or it will be `null` if there is no context + tracker. */ - specialChars?: RegExp; + get context(): any; /** - Regular expression that can be used to add characters to the - default set of characters to highlight. + Check if the given term would be able to be shifted (optionally + after some reductions) on this stack. This can be useful for + external tokenizers that want to make sure they only provide a + given token when it applies. */ - addSpecialChars?: RegExp | null; + canShift(term: number): boolean; + /** + Get the parser used by this stack. + */ + get parser(): LRParser; + /** + Test whether a given dialect (by numeric ID, as exported from + the terms file) is enabled. + */ + dialectEnabled(dialectID: number): boolean; + private shiftContext; + private reduceContext; + private updateContext; } -/** -Returns an extension that installs highlighting of special -characters. -*/ -declare function highlightSpecialChars( -/** -Configuration options. -*/ -config?: SpecialCharConfig): Extension; - -/** -Extension that enables a placeholder—a piece of example content -to show when the editor is empty. -*/ -declare function placeholder(content: string | HTMLElement): Extension; /** -Create an extension that enables rectangular selections. By -default, it will react to left mouse drag with the Alt key held -down. When such a selection occurs, the text within the rectangle -that was dragged over will be selected, as one selection -[range](https://codemirror.net/6/docs/ref/#state.SelectionRange) per line. +[Tokenizers](#lr.ExternalTokenizer) interact with the input +through this interface. It presents the input as a stream of +characters, tracking lookahead and hiding the complexity of +[ranges](#common.Parser.parse^ranges) from tokenizer code. */ -declare function rectangularSelection(options?: { +declare class InputStream { /** - A custom predicate function, which takes a `mousedown` event and - returns true if it should be used for rectangular selection. + Backup chunk */ - eventFilter?: (event: MouseEvent) => boolean; -}): Extension; - -/** -Creates an extension that configures tooltip behavior. -*/ -declare function tooltips(config?: { + private chunk2; + private chunk2Pos; /** - By default, tooltips use `"fixed"` - [positioning](https://developer.mozilla.org/en-US/docs/Web/CSS/position), - which has the advantage that tooltips don't get cut off by - scrollable parent elements. However, CSS rules like `contain: - layout` can break fixed positioning in child nodes, which can be - worked about by using `"absolute"` here. - - On iOS, which at the time of writing still doesn't properly - support fixed positioning, the library always uses absolute - positioning. + The character code of the next code unit in the input, or -1 + when the stream is at the end of the input. + */ + next: number; + /** + The current position of the stream. Note that, due to parses + being able to cover non-contiguous + [ranges](#common.Parser.startParse), advancing the stream does + not always mean its position moves a single unit. + */ + pos: number; + private rangeIndex; + private range; + /** + Look at a code unit near the stream position. `.peek(0)` equals + `.next`, `.peek(-1)` gives you the previous character, and so + on. - If the tooltip parent element sits in a transformed element, the - library also falls back to absolute positioning. + Note that looking around during tokenizing creates dependencies + on potentially far-away content, which may reduce the + effectiveness incremental parsing—when looking forward—or even + cause invalid reparses when looking backward more than 25 code + units, since the library does not track lookbehind. */ - position?: "fixed" | "absolute"; + peek(offset: number): number; /** - The element to put the tooltips into. By default, they are put - in the editor (`cm-editor`) element, and that is usually what - you want. But in some layouts that can lead to positioning - issues, and you need to use a different parent to work around - those. + Accept a token. By default, the end of the token is set to the + current stream position, but you can pass an offset (relative to + the stream position) to change that. */ - parent?: HTMLElement; + acceptToken(token: number, endOffset?: number): void; /** - By default, when figuring out whether there is room for a - tooltip at a given position, the extension considers the entire - space between 0,0 and `innerWidth`,`innerHeight` to be available - for showing tooltips. You can provide a function here that - returns an alternative rectangle. + Accept a token ending at a specific given position. */ - tooltipSpace?: (view: EditorView) => Rect; -}): Extension; -/** -Describes a tooltip. Values of this type, when provided through -the [`showTooltip`](https://codemirror.net/6/docs/ref/#view.showTooltip) facet, control the -individual tooltips on the editor. -*/ -interface Tooltip { + acceptTokenTo(token: number, endPos: number): void; + private getChunk; + private readNext; /** - The document position at which to show the tooltip. + Move the stream forward N (defaults to 1) code units. Returns + the new value of [`next`](#lr.InputStream.next). */ - pos: number; + advance(n?: number): number; + private setDone; +} +interface ExternalOptions { /** - The end of the range annotated by this tooltip, if different - from `pos`. + When set to true, mark this tokenizer as depending on the + current parse stack, which prevents its result from being cached + between parser actions at the same positions. */ - end?: number; + contextual?: boolean; /** - A constructor function that creates the tooltip's [DOM - representation](https://codemirror.net/6/docs/ref/#view.TooltipView). + By defaults, when a tokenizer returns a token, that prevents + tokenizers with lower precedence from even running. When + `fallback` is true, the tokenizer is allowed to run when a + previous tokenizer returned a token that didn't match any of the + current state's actions. */ - create(view: EditorView): TooltipView; + fallback?: boolean; /** - Whether the tooltip should be shown above or below the target - position. Not guaranteed to be respected for hover tooltips - since all hover tooltips for the same range are always - positioned together. Defaults to false. + When set to true, tokenizing will not stop after this tokenizer + has produced a token. (But it will still fail to reach this one + if a higher-precedence tokenizer produced a token.) */ - above?: boolean; + extend?: boolean; +} +/** +`@external tokens` declarations in the grammar should resolve to +an instance of this class. +*/ +declare class ExternalTokenizer { /** - Whether the `above` option should be honored when there isn't - enough space on that side to show the tooltip inside the - viewport. Defaults to false. + Create a tokenizer. The first argument is the function that, + given an input stream, scans for the types of tokens it + recognizes at the stream's position, and calls + [`acceptToken`](#lr.InputStream.acceptToken) when it finds + one. */ - strictSide?: boolean; + constructor( /** - When set to true, show a triangle connecting the tooltip element - to position `pos`. + @internal */ - arrow?: boolean; + token: (input: InputStream, stack: Stack) => void, options?: ExternalOptions); } + /** -Describes the way a tooltip is displayed. +Context trackers are used to track stateful context (such as +indentation in the Python grammar, or parent elements in the XML +grammar) needed by external tokenizers. You declare them in a +grammar file as `@context exportName from "module"`. + +Context values should be immutable, and can be updated (replaced) +on shift or reduce actions. + +The export used in a `@context` declaration should be of this +type. */ -interface TooltipView { +declare class ContextTracker { /** - The DOM element to position over the editor. + Define a context tracker. */ - dom: HTMLElement; + constructor(spec: { + /** + The initial value of the context at the start of the parse. + */ + start: T; + /** + Update the context when the parser executes a + [shift](https://en.wikipedia.org/wiki/LR_parser#Shift_and_reduce_actions) + action. + */ + shift?(context: T, term: number, stack: Stack, input: InputStream): T; + /** + Update the context when the parser executes a reduce action. + */ + reduce?(context: T, term: number, stack: Stack, input: InputStream): T; + /** + Update the context when the parser reuses a node from a tree + fragment. + */ + reuse?(context: T, node: Tree, stack: Stack, input: InputStream): T; + /** + Reduce a context value to a number (for cheap storage and + comparison). Only needed for strict contexts. + */ + hash?(context: T): number; + /** + By default, nodes can only be reused during incremental + parsing if they were created in the same context as the one in + which they are reused. Set this to false to disable that + check (and the overhead of storing the hashes). + */ + strict?: boolean; + }); +} +/** +Configuration options when +[reconfiguring](#lr.LRParser.configure) a parser. +*/ +interface ParserConfig { /** - Adjust the position of the tooltip relative to its anchor - position. A positive `x` value will move the tooltip - horizontally along with the text direction (so right in - left-to-right context, left in right-to-left). A positive `y` - will move the tooltip up when it is above its anchor, and down - otherwise. + Node prop values to add to the parser's node set. */ - offset?: { - x: number; - y: number; - }; + props?: readonly NodePropSource[]; /** - By default, a tooltip's screen position will be based on the - text position of its `pos` property. This method can be provided - to make the tooltip view itself responsible for finding its - screen position. + The name of the `@top` declaration to parse from. If not + specified, the first top rule declaration in the grammar is + used. */ - getCoords?: (pos: number) => Rect; + top?: string; /** - By default, tooltips are moved when they overlap with other - tooltips. Set this to `true` to disable that behavior for this - tooltip. + A space-separated string of dialects to enable. */ - overlap?: boolean; + dialect?: string; /** - Called after the tooltip is added to the DOM for the first time. + Replace the given external tokenizers with new ones. */ - mount?(view: EditorView): void; + tokenizers?: { + from: ExternalTokenizer; + to: ExternalTokenizer; + }[]; /** - Update the DOM element for a change in the view's state. + Replace external specializers with new ones. */ - update?(update: ViewUpdate): void; + specializers?: { + from: (value: string, stack: Stack) => number; + to: (value: string, stack: Stack) => number; + }[]; /** - Called when the tooltip is removed from the editor or the editor - is destroyed. + Replace the context tracker with a new one. */ - destroy?(): void; + contextTracker?: ContextTracker; /** - Called when the tooltip has been (re)positioned. The argument is - the [space](https://codemirror.net/6/docs/ref/#view.tooltips^config.tooltipSpace) available to the - tooltip. + When true, the parser will raise an exception, rather than run + its error-recovery strategies, when the input doesn't match the + grammar. */ - positioned?(space: Rect): void; + strict?: boolean; /** - By default, the library will restrict the size of tooltips so - that they don't stick out of the available space. Set this to - false to disable that. + Add a wrapper, which can extend parses created by this parser + with additional logic (usually used to add + [mixed-language](#common.parseMixed) parsing). */ - resize?: boolean; + wrap?: ParseWrapper; + /** + The maximum length of the TreeBuffers generated in the output + tree. Defaults to 1024. + */ + bufferLength?: number; } /** -Facet to which an extension can add a value to show a tooltip. +Holds the parse tables for a given grammar, as generated by +`lezer-generator`, and provides [methods](#common.Parser) to parse +content with. */ -declare const showTooltip: Facet; -type Handlers$1 = { - [event: string]: (view: EditorView, line: BlockInfo, event: Event) => boolean; -}; -interface LineNumberConfig { +declare class LRParser extends Parser { /** - How to display line numbers. Defaults to simply converting them - to string. + The nodes used in the trees emitted by this parser. */ - formatNumber?: (lineNo: number, state: EditorState) => string; + readonly nodeSet: NodeSet; + createParse(input: Input, fragments: readonly TreeFragment[], ranges: readonly { + from: number; + to: number; + }[]): PartialParse; /** - Supply event handlers for DOM events on this gutter. + Configure the parser. Returns a new parser instance that has the + given settings modified. Settings not provided in `config` are + kept from the original parser. */ - domEventHandlers?: Handlers$1; + configure(config: ParserConfig): LRParser; + /** + Tells you whether any [parse wrappers](#lr.ParserConfig.wrap) + are registered for this parser. + */ + hasWrappers(): boolean; + /** + Returns the name associated with a given term. This will only + work for all terms when the parser was generated with the + `--names` option. By default, only the names of tagged terms are + stored. + */ + getName(term: number): string; + /** + The type of top node produced by the parser. + */ + get topNode(): NodeType; + /** + Used by the output of the parser generator. Not available to + user code. @hide + */ + static deserialize(spec: any): LRParser; } -/** -Create a line number gutter extension. -*/ -declare function lineNumbers(config?: LineNumberConfig): Extension; /** Highlighting tags are markers that denote a highlighting category. @@ -5583,101 +5747,22 @@ interface MatchResult { The extent of the bracket token found. */ start: { - from: number; - to: number; - }; - /** - The extent of the matched token, if any was found. - */ - end?: { - from: number; - to: number; - }; - /** - Whether the tokens match. This can be false even when `end` has - a value, if that token doesn't match the opening token. - */ - matched: boolean; -} - -type JuliaLanguageConfig = { - /** Enable keyword completion */ - enableKeywordCompletion?: boolean; -}; -declare function julia(config?: JuliaLanguageConfig): LanguageSupport; - -interface HistoryConfig { - /** - The minimum depth (amount of events) to store. Defaults to 100. - */ - minDepth?: number; + from: number; + to: number; + }; /** - The maximum time (in milliseconds) that adjacent events can be - apart and still be grouped together. Defaults to 500. + The extent of the matched token, if any was found. */ - newGroupDelay?: number; + end?: { + from: number; + to: number; + }; /** - By default, when close enough together in time, changes are - joined into an existing undo event if they touch any of the - changed ranges from that event. You can pass a custom predicate - here to influence that logic. + Whether the tokens match. This can be false even when `end` has + a value, if that token doesn't match the opening token. */ - joinToEvent?: (tr: Transaction, isAdjacent: boolean) => boolean; + matched: boolean; } -/** -Create a history extension with the given configuration. -*/ -declare function history(config?: HistoryConfig): Extension; -/** -Default key bindings for the undo history. - -- Mod-z: [`undo`](https://codemirror.net/6/docs/ref/#commands.undo). -- Mod-y (Mod-Shift-z on macOS) + Ctrl-Shift-z on Linux: [`redo`](https://codemirror.net/6/docs/ref/#commands.redo). -- Mod-u: [`undoSelection`](https://codemirror.net/6/docs/ref/#commands.undoSelection). -- Alt-u (Mod-Shift-u on macOS): [`redoSelection`](https://codemirror.net/6/docs/ref/#commands.redoSelection). -*/ -declare const historyKeymap: readonly KeyBinding[]; -/** -Move the selected lines up one line. -*/ -declare const moveLineUp: StateCommand; -/** -Move the selected lines down one line. -*/ -declare const moveLineDown: StateCommand; -/** -Add a [unit](https://codemirror.net/6/docs/ref/#language.indentUnit) of indentation to all selected -lines. -*/ -declare const indentMore: StateCommand; -/** -Remove a [unit](https://codemirror.net/6/docs/ref/#language.indentUnit) of indentation from all -selected lines. -*/ -declare const indentLess: StateCommand; -/** -The default keymap. Includes all bindings from -[`standardKeymap`](https://codemirror.net/6/docs/ref/#commands.standardKeymap) plus the following: - -- Alt-ArrowLeft (Ctrl-ArrowLeft on macOS): [`cursorSyntaxLeft`](https://codemirror.net/6/docs/ref/#commands.cursorSyntaxLeft) ([`selectSyntaxLeft`](https://codemirror.net/6/docs/ref/#commands.selectSyntaxLeft) with Shift) -- Alt-ArrowRight (Ctrl-ArrowRight on macOS): [`cursorSyntaxRight`](https://codemirror.net/6/docs/ref/#commands.cursorSyntaxRight) ([`selectSyntaxRight`](https://codemirror.net/6/docs/ref/#commands.selectSyntaxRight) with Shift) -- Alt-ArrowUp: [`moveLineUp`](https://codemirror.net/6/docs/ref/#commands.moveLineUp) -- Alt-ArrowDown: [`moveLineDown`](https://codemirror.net/6/docs/ref/#commands.moveLineDown) -- Shift-Alt-ArrowUp: [`copyLineUp`](https://codemirror.net/6/docs/ref/#commands.copyLineUp) -- Shift-Alt-ArrowDown: [`copyLineDown`](https://codemirror.net/6/docs/ref/#commands.copyLineDown) -- Escape: [`simplifySelection`](https://codemirror.net/6/docs/ref/#commands.simplifySelection) -- Ctrl-Enter (Cmd-Enter on macOS): [`insertBlankLine`](https://codemirror.net/6/docs/ref/#commands.insertBlankLine) -- Alt-l (Ctrl-l on macOS): [`selectLine`](https://codemirror.net/6/docs/ref/#commands.selectLine) -- Ctrl-i (Cmd-i on macOS): [`selectParentSyntax`](https://codemirror.net/6/docs/ref/#commands.selectParentSyntax) -- Ctrl-[ (Cmd-[ on macOS): [`indentLess`](https://codemirror.net/6/docs/ref/#commands.indentLess) -- Ctrl-] (Cmd-] on macOS): [`indentMore`](https://codemirror.net/6/docs/ref/#commands.indentMore) -- Ctrl-Alt-\\ (Cmd-Alt-\\ on macOS): [`indentSelection`](https://codemirror.net/6/docs/ref/#commands.indentSelection) -- Shift-Ctrl-k (Shift-Cmd-k on macOS): [`deleteLine`](https://codemirror.net/6/docs/ref/#commands.deleteLine) -- Shift-Ctrl-\\ (Shift-Cmd-\\ on macOS): [`cursorMatchingBracket`](https://codemirror.net/6/docs/ref/#commands.cursorMatchingBracket) -- Ctrl-/ (Cmd-/ on macOS): [`toggleComment`](https://codemirror.net/6/docs/ref/#commands.toggleComment). -- Shift-Alt-a: [`toggleBlockComment`](https://codemirror.net/6/docs/ref/#commands.toggleBlockComment). -*/ -declare const defaultKeymap: readonly KeyBinding[]; /** Objects type used to represent individual completions. @@ -5805,6 +5890,14 @@ declare class CompletionContext { */ readonly explicit: boolean; /** + The editor view. May be undefined if the context was created + in a situation where there is no such view available, such as + in synchronous updates via + [`CompletionResult.update`](https://codemirror.net/6/docs/ref/#autocomplete.CompletionResult.update) + or when called by test code. + */ + readonly view?: EditorView | undefined; + /** Create a new completion context. (Mostly useful for testing completion sources—in the editor, the extension will create these for you.) @@ -5824,7 +5917,15 @@ declare class CompletionContext { only return completions when either there is part of a completable entity before the cursor, or `explicit` is true. */ - explicit: boolean); + explicit: boolean, + /** + The editor view. May be undefined if the context was created + in a situation where there is no such view available, such as + in synchronous updates via + [`CompletionResult.update`](https://codemirror.net/6/docs/ref/#autocomplete.CompletionResult.update) + or when called by test code. + */ + view?: EditorView | undefined); /** Get the extent, content, and (if there is a token) type of the token before `this.pos`. @@ -5853,8 +5954,18 @@ declare class CompletionContext { Allows you to register abort handlers, which will be called when the query is [aborted](https://codemirror.net/6/docs/ref/#autocomplete.CompletionContext.aborted). - */ - addEventListener(type: "abort", listener: () => void): void; + + By default, running queries will not be aborted for regular + typing or backspacing, on the assumption that they are likely to + return a result with a + [`validFor`](https://codemirror.net/6/docs/ref/#autocomplete.CompletionResult.validFor) field that + allows the result to be used after all. Passing `onDocChange: + true` will cause this query to be aborted for any document + change. + */ + addEventListener(type: "abort", listener: () => void, options?: { + onDocChange: boolean; + }): void; } /** Given a a fixed array of options, return an autocompleter that @@ -5970,6 +6081,11 @@ interface CompletionConfig { */ activateOnTyping?: boolean; /** + When given, if a completion that matches the predicate is + picked, reactivate completion again as if it was typed normally. + */ + activateOnCompletion?: (completion: Completion) => boolean; + /** The amount of time to wait for further typing before querying completion sources via [`activateOnTyping`](https://codemirror.net/6/docs/ref/#autocomplete.autocompletion^config.activateOnTyping). @@ -6193,7 +6309,7 @@ interface CloseBracketConfig { /** The opening brackets to close. Defaults to `["(", "[", "{", "'", '"']`. Brackets may be single characters or a triple of quotes - (as in `"''''"`). + (as in `"'''"`). */ brackets?: string[]; /** @@ -6358,50 +6474,244 @@ declare namespace index_d { index_d_startCompletion as startCompletion, }; } - -type HighlightOptions = { + +type HighlightOptions = { + /** + Determines whether, when nothing is selected, the word around + the cursor is matched instead. Defaults to false. + */ + highlightWordAroundCursor?: boolean; + /** + The minimum length of the selection before it is highlighted. + Defaults to 1 (always highlight non-cursor selections). + */ + minSelectionLength?: number; + /** + The amount of matches (in the viewport) at which to disable + highlighting. Defaults to 100. + */ + maxMatches?: number; + /** + Whether to only highlight whole words. + */ + wholeWords?: boolean; +}; +/** +This extension highlights text that matches the selection. It uses +the `"cm-selectionMatch"` class for the highlighting. When +`highlightWordAroundCursor` is enabled, the word at the cursor +itself will be highlighted with `"cm-selectionMatch-main"`. +*/ +declare function highlightSelectionMatches(options?: HighlightOptions): Extension; +/** +Select next occurrence of the current selection. Expand selection +to the surrounding word when the selection is empty. +*/ +declare const selectNextOccurrence: StateCommand; +/** +Default search-related key bindings. + + - Mod-f: [`openSearchPanel`](https://codemirror.net/6/docs/ref/#search.openSearchPanel) + - F3, Mod-g: [`findNext`](https://codemirror.net/6/docs/ref/#search.findNext) + - Shift-F3, Shift-Mod-g: [`findPrevious`](https://codemirror.net/6/docs/ref/#search.findPrevious) + - Mod-Alt-g: [`gotoLine`](https://codemirror.net/6/docs/ref/#search.gotoLine) + - Mod-d: [`selectNextOccurrence`](https://codemirror.net/6/docs/ref/#search.selectNextOccurrence) +*/ +declare const searchKeymap: readonly KeyBinding[]; + +/** +An update is a set of changes and effects. +*/ +interface Update { + /** + The changes made by this update. + */ + changes: ChangeSet; + /** + The effects in this update. There'll only ever be effects here + when you configure your collab extension with a + [`sharedEffects`](https://codemirror.net/6/docs/ref/#collab.collab^config.sharedEffects) option. + */ + effects?: readonly StateEffect[]; + /** + The [ID](https://codemirror.net/6/docs/ref/#collab.collab^config.clientID) of the client who + created this update. + */ + clientID: string; +} +type CollabConfig = { + /** + The starting document version. Defaults to 0. + */ + startVersion?: number; + /** + This client's identifying [ID](https://codemirror.net/6/docs/ref/#collab.getClientID). Will be a + randomly generated string if not provided. + */ + clientID?: string; + /** + It is possible to share information other than document changes + through this extension. If you provide this option, your + function will be called on each transaction, and the effects it + returns will be sent to the server, much like changes are. Such + effects are automatically remapped when conflicting remote + changes come in. + */ + sharedEffects?: (tr: Transaction) => readonly StateEffect[]; +}; +/** +Create an instance of the collaborative editing plugin. +*/ +declare function collab(config?: CollabConfig): Extension; +/** +Create a transaction that represents a set of new updates received +from the authority. Applying this transaction moves the state +forward to adjust to the authority's view of the document. +*/ +declare function receiveUpdates(state: EditorState, updates: readonly Update[]): Transaction; +/** +Returns the set of locally made updates that still have to be sent +to the authority. The returned objects will also have an `origin` +property that points at the transaction that created them. This +may be useful if you want to send along metadata like timestamps. +(But note that the updates may have been mapped in the meantime, +whereas the transaction is just the original transaction that +created them.) +*/ +declare function sendableUpdates(state: EditorState): readonly (Update & { + origin: Transaction; +})[]; +/** +Get the version up to which the collab plugin has synced with the +central authority. +*/ +declare function getSyncedVersion(state: EditorState): number; +/** +Get this editor's collaborative editing client ID. +*/ +declare function getClientID(state: EditorState): string; + +type Severity = "hint" | "info" | "warning" | "error"; +/** +Describes a problem or hint for a piece of code. +*/ +interface Diagnostic { + /** + The start position of the relevant text. + */ + from: number; + /** + The end position. May be equal to `from`, though actually + covering text is preferable. + */ + to: number; + /** + The severity of the problem. This will influence how it is + displayed. + */ + severity: Severity; + /** + When given, add an extra CSS class to parts of the code that + this diagnostic applies to. + */ + markClass?: string; + /** + An optional source string indicating where the diagnostic is + coming from. You can put the name of your linter here, if + applicable. + */ + source?: string; + /** + The message associated with this diagnostic. + */ + message: string; + /** + An optional custom rendering function that displays the message + as a DOM node. + */ + renderMessage?: (view: EditorView) => Node; + /** + An optional array of actions that can be taken on this + diagnostic. + */ + actions?: readonly Action[]; +} +/** +An action associated with a diagnostic. +*/ +interface Action { + /** + The label to show to the user. Should be relatively short. + */ + name: string; + /** + The function to call when the user activates this action. Is + given the diagnostic's _current_ position, which may have + changed since the creation of the diagnostic, due to editing. + */ + apply: (view: EditorView, from: number, to: number) => void; +} +type DiagnosticFilter = (diagnostics: readonly Diagnostic[], state: EditorState) => Diagnostic[]; +interface LintConfig { /** - Determines whether, when nothing is selected, the word around - the cursor is matched instead. Defaults to false. + Time to wait (in milliseconds) after a change before running + the linter. Defaults to 750ms. */ - highlightWordAroundCursor?: boolean; + delay?: number; /** - The minimum length of the selection before it is highlighted. - Defaults to 1 (always highlight non-cursor selections). + Optional predicate that can be used to indicate when diagnostics + need to be recomputed. Linting is always re-done on document + changes. */ - minSelectionLength?: number; + needsRefresh?: null | ((update: ViewUpdate) => boolean); /** - The amount of matches (in the viewport) at which to disable - highlighting. Defaults to 100. + Optional filter to determine which diagnostics produce markers + in the content. */ - maxMatches?: number; + markerFilter?: null | DiagnosticFilter; /** - Whether to only highlight whole words. + Filter applied to a set of diagnostics shown in a tooltip. No + tooltip will appear if the empty set is returned. */ - wholeWords?: boolean; -}; + tooltipFilter?: null | DiagnosticFilter; + /** + Can be used to control what kind of transactions cause lint + hover tooltips associated with the given document range to be + hidden. By default any transactions that changes the line + around the range will hide it. Returning null falls back to this + behavior. + */ + hideOn?: (tr: Transaction, from: number, to: number) => boolean | null; + /** + When enabled (defaults to off), this will cause the lint panel + to automatically open when diagnostics are found, and close when + all diagnostics are resolved or removed. + */ + autoPanel?: boolean; +} /** -This extension highlights text that matches the selection. It uses -the `"cm-selectionMatch"` class for the highlighting. When -`highlightWordAroundCursor` is enabled, the word at the cursor -itself will be highlighted with `"cm-selectionMatch-main"`. +Returns a transaction spec which updates the current set of +diagnostics, and enables the lint extension if if wasn't already +active. */ -declare function highlightSelectionMatches(options?: HighlightOptions): Extension; +declare function setDiagnostics(state: EditorState, diagnostics: readonly Diagnostic[]): TransactionSpec; /** -Select next occurrence of the current selection. Expand selection -to the surrounding word when the selection is empty. +The type of a function that produces diagnostics. */ -declare const selectNextOccurrence: StateCommand; +type LintSource = (view: EditorView) => readonly Diagnostic[] | Promise; /** -Default search-related key bindings. - - - Mod-f: [`openSearchPanel`](https://codemirror.net/6/docs/ref/#search.openSearchPanel) - - F3, Mod-g: [`findNext`](https://codemirror.net/6/docs/ref/#search.findNext) - - Shift-F3, Shift-Mod-g: [`findPrevious`](https://codemirror.net/6/docs/ref/#search.findPrevious) - - Mod-Alt-g: [`gotoLine`](https://codemirror.net/6/docs/ref/#search.gotoLine) - - Mod-d: [`selectNextOccurrence`](https://codemirror.net/6/docs/ref/#search.selectNextOccurrence) +Given a diagnostic source, this function returns an extension that +enables linting with that source. It will be called whenever the +editor is idle (after its content changed). If `null` is given as +source, this only configures the lint extension. */ -declare const searchKeymap: readonly KeyBinding[]; +declare function linter(source: LintSource | null, config?: LintConfig): Extension; + +type JuliaLanguageConfig = { + /** Enable keyword completion */ + enableKeywordCompletion?: boolean; +}; +declare function julia(config?: JuliaLanguageConfig): LanguageSupport; declare class LeafBlock { readonly start: number; @@ -6510,6 +6820,7 @@ declare class InlineContext { get end(): number; slice(from: number, to: number): string; addDelimiter(type: DelimiterType, from: number, to: number, open: boolean, close: boolean): number; + get hasOpenLink(): boolean; addElement(elt: Element$1): number; findOpeningDelimiter(type: DelimiterType): number; takeContent(startIndex: number): any[]; @@ -6650,107 +6961,16 @@ declare function html(config?: { nestedAttributes?: NestedAttr[]; }): LanguageSupport; -type Severity = "hint" | "info" | "warning" | "error"; -/** -Describes a problem or hint for a piece of code. -*/ -interface Diagnostic { - /** - The start position of the relevant text. - */ - from: number; - /** - The end position. May be equal to `from`, though actually - covering text is preferable. - */ - to: number; - /** - The severity of the problem. This will influence how it is - displayed. - */ - severity: Severity; - /** - When given, add an extra CSS class to parts of the code that - this diagnostic applies to. - */ - markClass?: string; - /** - An optional source string indicating where the diagnostic is - coming from. You can put the name of your linter here, if - applicable. - */ - source?: string; - /** - The message associated with this diagnostic. - */ - message: string; - /** - An optional custom rendering function that displays the message - as a DOM node. - */ - renderMessage?: () => Node; - /** - An optional array of actions that can be taken on this - diagnostic. - */ - actions?: readonly Action[]; -} -/** -An action associated with a diagnostic. -*/ -interface Action { - /** - The label to show to the user. Should be relatively short. - */ - name: string; - /** - The function to call when the user activates this action. Is - given the diagnostic's _current_ position, which may have - changed since the creation of the diagnostic, due to editing. - */ - apply: (view: EditorView, from: number, to: number) => void; -} -type DiagnosticFilter = (diagnostics: readonly Diagnostic[], state: EditorState) => Diagnostic[]; -interface LintConfig { - /** - Time to wait (in milliseconds) after a change before running - the linter. Defaults to 750ms. - */ - delay?: number; - /** - Optional predicate that can be used to indicate when diagnostics - need to be recomputed. Linting is always re-done on document - changes. - */ - needsRefresh?: null | ((update: ViewUpdate) => boolean); - /** - Optional filter to determine which diagnostics produce markers - in the content. - */ - markerFilter?: null | DiagnosticFilter; - /** - Filter applied to a set of diagnostics shown in a tooltip. No - tooltip will appear if the empty set is returned. - */ - tooltipFilter?: null | DiagnosticFilter; -} /** -Returns a transaction spec which updates the current set of -diagnostics, and enables the lint extension if if wasn't already -active. -*/ -declare function setDiagnostics(state: EditorState, diagnostics: readonly Diagnostic[]): TransactionSpec; -/** -The type of a function that produces diagnostics. +A language provider based on the [Lezer CSS +parser](https://github.com/lezer-parser/css), extended with +highlighting and indentation information. */ -type LintSource = (view: EditorView) => readonly Diagnostic[] | Promise; +declare const cssLanguage: LRLanguage; /** -Given a diagnostic source, this function returns an extension that -enables linting with that source. It will be called whenever the -editor is idle (after its content changed). If `null` is given as -source, this only configures the lint extension. +Language support for CSS. */ -declare function linter(source: LintSource | null, config?: LintConfig): Extension; +declare function css(): LanguageSupport; /** A language provider based on the [Lezer JavaScript @@ -6767,17 +6987,6 @@ declare function javascript(config?: { typescript?: boolean; }): LanguageSupport; -/** -A language provider based on the [Lezer CSS -parser](https://github.com/lezer-parser/css), extended with -highlighting and indentation information. -*/ -declare const cssLanguage: LRLanguage; -/** -Language support for CSS. -*/ -declare function css(): LanguageSupport; - /** Configuration for an [SQL Dialect](https://codemirror.net/6/docs/ref/#lang-sql.SQLDialect). */ @@ -6846,6 +7055,12 @@ type SQLDialectSpec = { */ identifierQuotes?: string; /** + Controls whether identifiers are case-insensitive. Identifiers + with upper-case letters are quoted when set to false (which is + the default). + */ + caseInsensitiveIdentifiers?: boolean; + /** Controls whether bit values can be defined as 0b1010. Defaults to false. */ @@ -6951,76 +7166,4 @@ Python language support. */ declare function python(): LanguageSupport; -/** -An update is a set of changes and effects. -*/ -interface Update { - /** - The changes made by this update. - */ - changes: ChangeSet; - /** - The effects in this update. There'll only ever be effects here - when you configure your collab extension with a - [`sharedEffects`](https://codemirror.net/6/docs/ref/#collab.collab^config.sharedEffects) option. - */ - effects?: readonly StateEffect[]; - /** - The [ID](https://codemirror.net/6/docs/ref/#collab.collab^config.clientID) of the client who - created this update. - */ - clientID: string; -} -type CollabConfig = { - /** - The starting document version. Defaults to 0. - */ - startVersion?: number; - /** - This client's identifying [ID](https://codemirror.net/6/docs/ref/#collab.getClientID). Will be a - randomly generated string if not provided. - */ - clientID?: string; - /** - It is possible to share information other than document changes - through this extension. If you provide this option, your - function will be called on each transaction, and the effects it - returns will be sent to the server, much like changes are. Such - effects are automatically remapped when conflicting remote - changes come in. - */ - sharedEffects?: (tr: Transaction) => readonly StateEffect[]; -}; -/** -Create an instance of the collaborative editing plugin. -*/ -declare function collab(config?: CollabConfig): Extension; -/** -Create a transaction that represents a set of new updates received -from the authority. Applying this transaction moves the state -forward to adjust to the authority's view of the document. -*/ -declare function receiveUpdates(state: EditorState, updates: readonly Update[]): Transaction; -/** -Returns the set of locally made updates that still have to be sent -to the authority. The returned objects will also have an `origin` -property that points at the transaction that created them. This -may be useful if you want to send along metadata like timestamps. -(But note that the updates may have been mapped in the meantime, -whereas the transaction is just the original transaction that -created them.) -*/ -declare function sendableUpdates(state: EditorState): readonly (Update & { - origin: Transaction; -})[]; -/** -Get the version up to which the collab plugin has synced with the -central authority. -*/ -declare function getSyncedVersion(state: EditorState): number; -/** -Get this editor's collaborative editing client ID. -*/ -declare function getClientID(state: EditorState): string; - -export { Annotation, ChangeSet, Compartment, Decoration, Diagnostic, EditorSelection, EditorState, EditorView, Facet, HighlightStyle, NodeProp, PostgreSQL, SelectionRange, StateEffect, StateField, Text, Tooltip, Transaction, TreeCursor, ViewPlugin, ViewUpdate, WidgetType, index_d as autocomplete, bracketMatching, closeBrackets, closeBracketsKeymap, collab, combineConfig, completionKeymap, css, cssLanguage, defaultHighlightStyle, defaultKeymap, drawSelection, foldGutter, foldKeymap, getClientID, getSyncedVersion, highlightSelectionMatches, highlightSpecialChars, history, historyKeymap, html, htmlLanguage, indentLess, indentMore, indentOnInput, indentUnit, javascript, javascriptLanguage, julia as julia_andrey, keymap, lineNumbers, linter, markdown, markdownLanguage, moveLineDown, moveLineUp, parseCode, parseMixed, placeholder, python, pythonLanguage, receiveUpdates, rectangularSelection, searchKeymap, selectNextOccurrence, sendableUpdates, setDiagnostics, showTooltip, sql, syntaxHighlighting, syntaxTree, syntaxTreeAvailable, tags, tooltips }; +export { Annotation, ChangeSet, Compartment, Decoration, Diagnostic, EditorSelection, EditorState, EditorView, Facet, HighlightStyle, MatchDecorator, NodeProp, PostgreSQL, SelectionRange, StateEffect, StateField, Text, Tooltip, Transaction, TreeCursor, ViewPlugin, ViewUpdate, WidgetType, index_d as autocomplete, bracketMatching, closeBrackets, closeBracketsKeymap, collab, combineConfig, completionKeymap, css, cssLanguage, defaultHighlightStyle, defaultKeymap, drawSelection, foldGutter, foldKeymap, getClientID, getSyncedVersion, highlightActiveLine, highlightSelectionMatches, highlightSpecialChars, history, historyKeymap, html, htmlLanguage, indentLess, indentMore, indentOnInput, indentUnit, javascript, javascriptLanguage, julia, keymap, lineNumbers, linter, markdown, markdownLanguage, moveLineDown, moveLineUp, parseCode, parseMixed, placeholder, python, pythonLanguage, receiveUpdates, rectangularSelection, searchKeymap, selectNextOccurrence, sendableUpdates, setDiagnostics, showTooltip, sql, syntaxHighlighting, syntaxTree, syntaxTreeAvailable, tags, tooltips }; From 2478e627e882620b858585e48cfab0bfe89a6440 Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Sat, 10 Aug 2024 22:03:02 +0200 Subject: [PATCH 04/14] derp --- .../CellInput/LiveDocsFromCursor.js | 21 +++++++++-------- .../components/CellInput/lezer_template.js | 14 ++++++----- frontend/components/CellInput/mixedParsers.js | 9 +------- .../components/CellInput/pkg_bubble_plugin.js | 4 ++-- .../CellInput/pluto_autocomplete.js | 14 +++++------ .../CellInput/scopestate_statefield.js | 23 ++++++++++--------- 6 files changed, 41 insertions(+), 44 deletions(-) diff --git a/frontend/components/CellInput/LiveDocsFromCursor.js b/frontend/components/CellInput/LiveDocsFromCursor.js index a2cd0ffdbf..0c1b67b0a5 100644 --- a/frontend/components/CellInput/LiveDocsFromCursor.js +++ b/frontend/components/CellInput/LiveDocsFromCursor.js @@ -2,7 +2,7 @@ import { EditorState, syntaxTree } from "../../imports/CodemirrorPlutoSetup.js" import { ScopeStateField } from "./scopestate_statefield.js" let get_root_variable_from_expression = (cursor) => { - if (cursor.name === "SubscriptExpression") { + if (cursor.name === "IndexExpression") { cursor.firstChild() return get_root_variable_from_expression(cursor) } @@ -20,12 +20,13 @@ let get_root_variable_from_expression = (cursor) => { let VALID_DOCS_TYPES = [ "Identifier", "FieldExpression", - "SubscriptExpression", + "IndexExpression", "MacroFieldExpression", "MacroIdentifier", "Operator", - "Definition", - "ParameterizedIdentifier", + "TypeHead", + "Signature", + "ParametrizedExpression", ] let keywords_that_have_docs_and_are_cool = [ "import", @@ -49,12 +50,12 @@ let is_docs_searchable = (/** @type {import("../../imports/CodemirrorPlutoSetup. } else if (VALID_DOCS_TYPES.includes(cursor.name)) { if (cursor.firstChild()) { do { - // Numbers themselves can't be docs searched, but using numbers inside SubscriptExpression can be. - if (cursor.name === "Number") { + // Numbers themselves can't be docs searched, but using numbers inside IndexExpression can be. + if (cursor.name === "IntegerLiteral" || cursor.name === "FloatLiteral") { continue } // This is for the VERY specific case like `Vector{Int}(1,2,3,4) which I want to yield `Vector{Int}` - if (cursor.name === "TypeArgumentList") { + if (cursor.name === "BraceExpression") { continue } if (cursor.name === "FieldName" || cursor.name === "MacroName" || cursor.name === "MacroFieldName") { @@ -129,7 +130,7 @@ export let get_selected_doc_from_state = (/** @type {EditorState} */ state, verb // - Struct name is useless: you are looking at the definition // - Properties are just named, not in the workspace or anything // Only thing we do want, are types and the right hand side of `=`'s. - if (parents.includes("AssignmentExpression") && parents.indexOf("AssignmentExpression") < index_of_struct_in_parents) { + if (parents.includes("binding") && parents.indexOf("binding") < index_of_struct_in_parents) { // We're inside a `... = ...` inside the struct } else if (parents.includes("TypedExpression") && parents.indexOf("TypedExpression") < index_of_struct_in_parents) { // We're inside a `x::X` inside the struct @@ -229,7 +230,7 @@ export let get_selected_doc_from_state = (/** @type {EditorState} */ state, verb } // `a = 1` would yield `=`, `a += 1` would yield `+=` - if (cursor.name === "AssignmentExpression") { + if (cursor.name === "binding") { let end_of_first = cursor.node.firstChild.to let beginning_of_last = cursor.node.lastChild.from return state.doc.sliceString(end_of_first, beginning_of_last).trim() @@ -320,7 +321,7 @@ export let get_selected_doc_from_state = (/** @type {EditorState} */ state, verb return undefined } // If we are expanding to an AssigmentExpression, we DONT want to show `=` - if (parent.name === "AssignmentExpression") { + if (parent.name === "binding") { return undefined } } finally { diff --git a/frontend/components/CellInput/lezer_template.js b/frontend/components/CellInput/lezer_template.js index edfdcaeab5..9564a5ad38 100644 --- a/frontend/components/CellInput/lezer_template.js +++ b/frontend/components/CellInput/lezer_template.js @@ -261,7 +261,7 @@ export let match_template = (haystack_cursor, template, matches, verbose = false // Skip comments // TODO This is, I think, one of the few julia-only things right now..... // .... Any sane way to factor this out? - while (haystack_cursor.name === "Comment" || haystack_cursor.name === "BlockComment") { + while (haystack_cursor.name === "LineComment" || haystack_cursor.name === "BlockComment") { if (!haystack_cursor.nextSibling()) break } @@ -466,7 +466,11 @@ export let to_template = function* (julia_code_object) { } }) if (unused_substitutions.length > 0) { - throw new Error(`Some substitutions not applied, this means it couldn't be matched to a AST position: ${JSON.stringify(unused_substitutions)}`) + throw new Error( + `Some substitutions not applied, this means it couldn't be matched to a AST position.\n\nUnused substitutions: ${JSON.stringify( + unused_substitutions + )}\n` + ) } return result } else if (typeof julia_code_object === "function") { @@ -837,7 +841,7 @@ export const t = /** @type {const} */ ({ yield "69" return { pattern: function Number(haystack, matches, verbose = false) { - return haystack != null && narrow_name(haystack) === "Number" + return haystack != null && (narrow_name(haystack) === "IntegerLiteral" || narrow_name(haystack) === "FloatLiteral") }, } }, @@ -846,9 +850,7 @@ export const t = /** @type {const} */ ({ yield `"A113"` return { pattern: function String(haystack, matches, verbose = false) { - return ( - haystack != null && (narrow_name(haystack) === "StringWithoutInterpolation" || narrow_name(haystack) === "TripleStringWithoutInterpolation") - ) + return haystack != null && (narrow_name(haystack) === "StringLiteral" || narrow_name(haystack) === "NsStringLiteral") }, } }, diff --git a/frontend/components/CellInput/mixedParsers.js b/frontend/components/CellInput/mixedParsers.js index 11898e0539..62659fdec8 100644 --- a/frontend/components/CellInput/mixedParsers.js +++ b/frontend/components/CellInput/mixedParsers.js @@ -80,14 +80,7 @@ const overlayHack = (overlay, input) => { }) } -const STRING_NODE_NAMES = new Set([ - "TripleString", - "String", - "CommandString", - "TripleStringWithoutInterpolation", - "StringWithoutInterpolation", - "CommandStringWithoutInterpolation", -]) +export const STRING_NODE_NAMES = new Set(["StringLiteral", "CommandLiteral", "NsStringLiteral", "NsCommandLiteral"]) const juliaWrapper = parseMixed((node, input) => { if (!STRING_NODE_NAMES.has(node.type.name)) { diff --git a/frontend/components/CellInput/pkg_bubble_plugin.js b/frontend/components/CellInput/pkg_bubble_plugin.js index fcab9e6a8a..f38a5e9dee 100644 --- a/frontend/components/CellInput/pkg_bubble_plugin.js +++ b/frontend/components/CellInput/pkg_bubble_plugin.js @@ -39,7 +39,7 @@ function find_import_statements({ doc, tree, from, to }) { quotelevel++ } // `$(...)` when inside quote - if (cursor.name === "InterpolationExpression") { + if (cursor.name === "InterpExpression") { quotelevel-- } if (quotelevel !== 0) return @@ -160,7 +160,7 @@ function find_import_statements({ doc, tree, from, to }) { if (cursor.name === "QuoteExpression" || cursor.name === "QuoteStatement") { quotelevel-- } - if (cursor.name === "InterpolationExpression") { + if (cursor.name === "InterpExpression") { quotelevel++ } }, diff --git a/frontend/components/CellInput/pluto_autocomplete.js b/frontend/components/CellInput/pluto_autocomplete.js index edca7a4aee..e5d7b6cc67 100644 --- a/frontend/components/CellInput/pluto_autocomplete.js +++ b/frontend/components/CellInput/pluto_autocomplete.js @@ -7,6 +7,7 @@ import { ScopeStateField } from "./scopestate_statefield.js" import { open_bottom_right_panel } from "../BottomRightPanel.js" import { ENABLE_CM_AUTOCOMPLETE_ON_TYPE } from "../CellInput.js" import { GlobalDefinitionsFacet } from "./go_to_definition_plugin.js" +import { STRING_NODE_NAMES } from "./mixedParsers.js" let { autocompletion, completionKeymap, completionStatus, acceptCompletion, selectedCompletion } = autocomplete @@ -128,7 +129,7 @@ const match_symbol_complete = (/** @type {autocomplete.CompletionContext} */ ctx function match_string_complete(state, pos) { const tree = syntaxTree(state) const node = tree.resolve(pos) - if (node == null || (node.name !== "TripleString" && node.name !== "String")) { + if (node == null || !STRING_NODE_NAMES.has(node.name)) { return false } return true @@ -166,7 +167,7 @@ const julia_code_completions_to_cm = (/** @type {PlutoRequestAutocomplete} */ request_autocomplete) => async (/** @type {autocomplete.CompletionContext} */ ctx) => { if (match_special_symbol_complete(ctx)) return null if (!ctx.explicit && writing_variable_name_or_keyword(ctx)) return null - if (!ctx.explicit && ctx.tokenBefore(["Number", "Comment", "String", "TripleString"]) != null) return null + if (!ctx.explicit && ctx.tokenBefore(["IntegerLiteral", "FloatLiteral", "LineComment", "BlockComment", ...STRING_NODE_NAMES]) != null) return null let to_complete = /** @type {String} */ (ctx.state.sliceDoc(0, ctx.pos)) @@ -270,13 +271,12 @@ const julia_code_completions_to_cm = const complete_anyword = async (/** @type {autocomplete.CompletionContext} */ ctx) => { if (match_special_symbol_complete(ctx)) return null if (!ctx.explicit && writing_variable_name_or_keyword(ctx)) return null - if (!ctx.explicit && ctx.tokenBefore(["Number", "Comment", "String", "TripleString"]) != null) return null + if (!ctx.explicit && ctx.tokenBefore(["IntegerLiteral", "FloatLiteral", "LineComment", "BlockComment", ...STRING_NODE_NAMES]) != null) return null const results_from_cm = await autocomplete.completeAnyWord(ctx) if (results_from_cm === null) return null - const last_token = ctx.tokenBefore(["Identifier", "Number"]) - if (last_token == null || last_token.type?.name === "Number") return null + if (ctx.tokenBefore(["Identifier", "IntegerLiteral", "FloatLiteral"])) return null return { from: results_from_cm.from, @@ -321,7 +321,7 @@ const writing_variable_name_or_keyword = (/** @type {autocomplete.CompletionCont const global_variables_completion = async (/** @type {autocomplete.CompletionContext} */ ctx) => { if (match_special_symbol_complete(ctx)) return null if (!ctx.explicit && writing_variable_name_or_keyword(ctx)) return null - if (!ctx.explicit && ctx.tokenBefore(["Number", "Comment", "String", "TripleString"]) != null) return null + if (!ctx.explicit && ctx.tokenBefore(["IntegerLiteral", "FloatLiteral", "LineComment", "BlockComment", ...STRING_NODE_NAMES]) != null) return null const globals = ctx.state.facet(GlobalDefinitionsFacet) @@ -438,7 +438,7 @@ const special_symbols_completion = (/** @type {() => Promise} * return async (/** @type {autocomplete.CompletionContext} */ ctx) => { if (!match_special_symbol_complete(ctx)) return null if (!ctx.explicit && writing_variable_name_or_keyword(ctx)) return null - if (!ctx.explicit && ctx.tokenBefore(["Number", "Comment"]) != null) return null + if (!ctx.explicit && ctx.tokenBefore(["IntegerLiteral", "FloatLiteral", "LineComment", "BlockComment"]) != null) return null const result = await get_special_symbols() return await autocomplete.completeFromList(result ?? [])(ctx) diff --git a/frontend/components/CellInput/scopestate_statefield.js b/frontend/components/CellInput/scopestate_statefield.js index 2478d5ba65..027d9035b7 100644 --- a/frontend/components/CellInput/scopestate_statefield.js +++ b/frontend/components/CellInput/scopestate_statefield.js @@ -49,7 +49,7 @@ let merge_scope_state = (a, b) => { /** @param {TreeCursor} cursor */ let search_for_interpolations = function* (cursor) { for (let child of child_cursors(cursor)) { - if (child.name === "InterpolationExpression") { + if (child.name === "InterpExpression") { yield cursor } else if (child.name === "QuoteExpression" || child.name === "QuoteStatement") { for (let child_child of search_for_interpolations(child)) { @@ -470,36 +470,37 @@ export let explore_variable_usage = ( // Doing these checks in front seems to speed things up a bit. if ( cursor.type.is("keyword") || - cursor.name === "SourceFile" || - cursor.name === "BooleanLiteral" || - cursor.name === "Character" || + cursor.name === "Program" || + cursor.name === "BoolLiteral" || + cursor.name === "CharLiteral" || cursor.name === "String" || - cursor.name === "Number" || + cursor.name === "IntegerLiteral" || + cursor.name === "FloatLiteral" || cursor.name === "Comment" || cursor.name === "BinaryExpression" || cursor.name === "Operator" || cursor.name === "MacroArgumentList" || cursor.name === "ReturnStatement" || - cursor.name === "BareTupleExpression" || - cursor.name === "ParenthesizedExpression" || + cursor.name === "OpenTuple" || + cursor.name === "ParenExpression" || cursor.name === "Type" || - cursor.name === "InterpolationExpression" || + cursor.name === "InterpExpression" || cursor.name === "SpreadExpression" || cursor.name === "CompoundExpression" || cursor.name === "ParameterizedIdentifier" || - cursor.name === "TypeArgumentList" || + cursor.name === "BraceExpression" || cursor.name === "TernaryExpression" || cursor.name === "Coefficient" || cursor.name === "TripleString" || cursor.name === "RangeExpression" || - cursor.name === "SubscriptExpression" || + cursor.name === "IndexExpression" || cursor.name === "UnaryExpression" || cursor.name === "ConstStatement" || cursor.name === "GlobalStatement" || cursor.name === "ContinueStatement" || cursor.name === "MatrixExpression" || cursor.name === "MatrixRow" || - cursor.name === "ArrayExpression" + cursor.name === "VectorExpression" ) { for (let subcursor of child_cursors(cursor)) { scopestate = explore_variable_usage(subcursor, doc, scopestate, verbose) From d0f72d25c8bda3fee1440ff167def739452931cd Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Fri, 3 Jan 2025 13:38:44 +0100 Subject: [PATCH 05/14] upgrade CM --- frontend/imports/CodemirrorPlutoSetup.d.ts | 58 +++++++++++++++++++--- frontend/imports/CodemirrorPlutoSetup.js | 2 +- 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/frontend/imports/CodemirrorPlutoSetup.d.ts b/frontend/imports/CodemirrorPlutoSetup.d.ts index bc707cc8d6..4de86cb4d3 100644 --- a/frontend/imports/CodemirrorPlutoSetup.d.ts +++ b/frontend/imports/CodemirrorPlutoSetup.d.ts @@ -210,7 +210,7 @@ declare class ChangeDesc { Map this description, which should start with the same document as `other`, over another set of changes, so that it can be applied after it. When `before` is true, map as if the changes - in `other` happened before the ones in `this`. + in `this` happened before the ones in `other`. */ mapDesc(other: ChangeDesc, before?: boolean): ChangeDesc; /** @@ -1408,6 +1408,13 @@ interface RangeComparator { Notification for a changed (or inserted, or deleted) point range. */ comparePoint(from: number, to: number, pointA: T | null, pointB: T | null): void; + /** + Notification for a changed boundary between ranges. For example, + if the same span is covered by two partial ranges before and one + bigger range after, this is called at the point where the ranges + used to be split. + */ + boundChange?(pos: number): void; } /** Methods used when iterating over the spans created by a set of @@ -2105,6 +2112,13 @@ declare class ViewUpdate { */ get viewportChanged(): boolean; /** + Returns true when + [`viewportChanged`](https://codemirror.net/6/docs/ref/#view.ViewUpdate.viewportChanged) is true + and the viewport change is not just the result of mapping it in + response to document changes. + */ + get viewportMoved(): boolean; + /** Indicates whether the height of a block element in the editor changed in this update. */ @@ -2451,7 +2465,7 @@ declare class EditorView { /** Find the line block around the given document position. A line block is a range delimited on both sides by either a - non-[hidden](https://codemirror.net/6/docs/ref/#view.Decoration^replace) line breaks, or the + non-[hidden](https://codemirror.net/6/docs/ref/#view.Decoration^replace) line break, or the start/end of the document. It will usually just hold a line of text, but may be broken into multiple textblocks by block widgets. @@ -2728,6 +2742,15 @@ declare class EditorView { */ static inputHandler: Facet<(view: EditorView, from: number, to: number, text: string, insert: () => Transaction) => boolean, readonly ((view: EditorView, from: number, to: number, text: string, insert: () => Transaction) => boolean)[]>; /** + Functions provided in this facet will be used to transform text + pasted or dropped into the editor. + */ + static clipboardInputFilter: Facet<(text: string, state: EditorState) => string, readonly ((text: string, state: EditorState) => string)[]>; + /** + Transform text copied or dragged from the editor. + */ + static clipboardOutputFilter: Facet<(text: string, state: EditorState) => string, readonly ((text: string, state: EditorState) => string)[]>; + /** Scroll handlers can override how things are scrolled into view. If they return `true`, no further handling happens for the scrolling. If they return false, the default scroll behavior is @@ -3129,7 +3152,7 @@ declare function highlightActiveLine(): Extension; Extension that enables a placeholder—a piece of example content to show when the editor is empty. */ -declare function placeholder(content: string | HTMLElement): Extension; +declare function placeholder(content: string | HTMLElement | ((view: EditorView) => HTMLElement)): Extension; /** Helper class used to make it easier to maintain decorations on @@ -3289,6 +3312,12 @@ interface Tooltip { to position `pos`. */ arrow?: boolean; + /** + By default, tooltips are hidden when their position is outside + of the visible editor content. Set this to false to turn that + off. + */ + clip?: boolean; } /** Describes the way a tooltip is displayed. @@ -4467,7 +4496,7 @@ declare class TreeCursor implements SyntaxNodeRef { */ next(enter?: boolean): boolean; /** - Move to the next node in a last-to-first pre-order traveral. A + Move to the next node in a last-to-first pre-order traversal. A node is followed by its last child or, if it has none, its previous sibling or the previous sibling of the first parent node that has one. @@ -4860,6 +4889,7 @@ declare class Tag { this one itself and sorted in order of decreasing specificity. */ readonly set: Tag[]; + toString(): string; /** Define a new tag. If `parent` is given, the tag is treated as a sub-tag of that parent, and @@ -4867,6 +4897,7 @@ declare class Tag { this tag will try to fall back to the parent tag (or grandparent tag, etc). */ + static define(name?: string, parent?: Tag): Tag; static define(parent?: Tag): Tag; /** Define a tag _modifier_, which is a function that, given a tag, @@ -4880,7 +4911,7 @@ declare class Tag { example `m1(m2(m3(t1)))` is a subtype of `m1(m2(t1))`, `m1(m3(t1)`, and so on. */ - static defineModifier(): (tag: Tag) => Tag; + static defineModifier(name?: string): (tag: Tag) => Tag; } /** A highlighter defines a mapping from highlighting tags and @@ -5181,7 +5212,7 @@ declare const tags: { */ heading6: Tag; /** - A prose separator (such as a horizontal rule). + A prose [content](#highlight.tags.content) separator (such as a horizontal rule). */ contentSeparator: Tag; /** @@ -5622,7 +5653,7 @@ declare class HighlightStyle implements Highlighter { */ readonly module: StyleModule | null; readonly style: (tags: readonly Tag[]) => string | null; - readonly scope: ((type: NodeType) => boolean) | undefined; + readonly scope?: (type: NodeType) => boolean; private constructor(); /** Create a highlighter style that associates the given styles to @@ -6362,7 +6393,7 @@ declare function autocompletion(config?: CompletionConfig): Extension; /** Basic keybindings for autocompletion. - - Ctrl-Space: [`startCompletion`](https://codemirror.net/6/docs/ref/#autocomplete.startCompletion) + - Ctrl-Space (and Alt-\` on macOS): [`startCompletion`](https://codemirror.net/6/docs/ref/#autocomplete.startCompletion) - Escape: [`closeCompletion`](https://codemirror.net/6/docs/ref/#autocomplete.closeCompletion) - ArrowDown: [`moveCompletionSelection`](https://codemirror.net/6/docs/ref/#autocomplete.moveCompletionSelection)`(true)` - ArrowUp: [`moveCompletionSelection`](https://codemirror.net/6/docs/ref/#autocomplete.moveCompletionSelection)`(false)` @@ -6879,6 +6910,13 @@ declare function markdown(config?: { disable this. */ completeHTMLTags?: boolean; + /** + By default, HTML tags in the document are handled by the [HTML + language](https://github.com/codemirror/lang-html) package with + tag matching turned off. You can pass in an alternative language + configuration here if you want. + */ + htmlTagLanguage?: LanguageSupport; }): LanguageSupport; /** @@ -7143,6 +7181,10 @@ interface SQLConfig { When set to true, keyword completions will be upper-case. */ upperCaseKeywords?: boolean; + /** + Can be used to customize the completions generated for keywords. + */ + keywordCompletion?: (label: string, type: string) => Completion; } /** SQL language support for the given SQL dialect, with keyword diff --git a/frontend/imports/CodemirrorPlutoSetup.js b/frontend/imports/CodemirrorPlutoSetup.js index bdc95aeea8..55e25a5766 100644 --- a/frontend/imports/CodemirrorPlutoSetup.js +++ b/frontend/imports/CodemirrorPlutoSetup.js @@ -64,4 +64,4 @@ export { linter, setDiagnostics, //@ts-ignore -} from "https://cdn.jsdelivr.net/gh/JuliaPluto/codemirror-pluto-setup@07a6ae6/dist/index.es.min.js" +} from "https://cdn.jsdelivr.net/gh/JuliaPluto/codemirror-pluto-setup@f01836f/dist/index.es.min.js" From 08ed1972d39e6e98d61c8c5e998426adb07b4d0c Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Fri, 3 Jan 2025 16:38:04 +0100 Subject: [PATCH 06/14] asdf --- frontend/components/CellInput/lezer_template.js | 10 ++++++++-- .../components/CellInput/tests/scopestate_helpers.js | 8 +++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/frontend/components/CellInput/lezer_template.js b/frontend/components/CellInput/lezer_template.js index 9564a5ad38..64f4723d76 100644 --- a/frontend/components/CellInput/lezer_template.js +++ b/frontend/components/CellInput/lezer_template.js @@ -23,7 +23,7 @@ const TEMPLATE_CREATION_VERBOSE = false * * @type {"SPEED" | "VALIDITY"} */ -const PERFORMANCE_MODE = /** @type {any} */ ("SPEED") +const PERFORMANCE_MODE = /** @type {any} */ ("VALIDITY") const IS_IN_VALIDATION_MODE = PERFORMANCE_MODE === "VALIDITY" @@ -386,7 +386,7 @@ let substitutions_to_template = (ast, substitutions) => { } } -let node_to_explorable = (cursor) => { +export let node_to_explorable = (cursor) => { if ("cursor" in cursor) { cursor = cursor.cursor() } @@ -494,6 +494,7 @@ export let jl_dynamic = weak_memo((template, ...substitutions) => { /** @type {WeakMap, result: JuliaCodeObject }>} */ let template_cache = new WeakMap() + /** * @param {TemplateStringsArray} template * @param {Array} substitutions @@ -564,6 +565,9 @@ export let template = weak_memo1((julia_code_object) => { }) }) +/** + * + */ export let as_string = weak_memo1((/** @type {JuliaCodeObject} */ julia_code_object) => { let template_generator = to_template(julia_code_object) let julia_to_parse = intermediate_value(template_generator.next()) @@ -1059,6 +1063,8 @@ export function iterate_with_cursor({ tree, enter, leave, from = 0, to = tree.le /////////////////////////////////// /** + * Return `iterater_result.value` if `iterater_result.done` is `false`, otherwise throw an error. + * * Not sure why typescript doesn't infer the `Generator` when I ask !iterater_result.done... * Maybe it will bite me later 🤷‍♀️ * diff --git a/frontend/components/CellInput/tests/scopestate_helpers.js b/frontend/components/CellInput/tests/scopestate_helpers.js index 638e376f80..45054c083a 100644 --- a/frontend/components/CellInput/tests/scopestate_helpers.js +++ b/frontend/components/CellInput/tests/scopestate_helpers.js @@ -43,13 +43,17 @@ export let test_scopestate = (input, expected) => { } /** + * Compute the scopestate for the code. Variable names are used to create the expected scope: a variable called `global_something_2` is expected to be a global usage, etc. + * * @param {JuliaCodeObject} code */ export function test_implicit(code) { let expected_scopestate = { defined: [], local_used: [], global_used: [] } + console.log(as_string(code)) + for (let variable of as_string(code).matchAll(/(macro_)?(global|local|defined)(_[a-z0-9_]+)?/g)) { - let [variable_name, is_macro, usage_type] = variable + let [variable_name, is_macro, usage_type, number] = variable if (is_macro != null) { variable_name = `@${variable_name}` @@ -65,5 +69,7 @@ export function test_implicit(code) { } } + console.log(expected_scopestate) + return test_scopestate(code, expected_scopestate) } From e0b5b3fac9a09df44b2a8fb25b353e713ec9c9bc Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Thu, 9 Jan 2025 01:09:53 +0100 Subject: [PATCH 07/14] Update mixedParsers.js --- frontend/components/CellInput/mixedParsers.js | 39 +++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/frontend/components/CellInput/mixedParsers.js b/frontend/components/CellInput/mixedParsers.js index 62659fdec8..c191ff9933 100644 --- a/frontend/components/CellInput/mixedParsers.js +++ b/frontend/components/CellInput/mixedParsers.js @@ -83,28 +83,43 @@ const overlayHack = (overlay, input) => { export const STRING_NODE_NAMES = new Set(["StringLiteral", "CommandLiteral", "NsStringLiteral", "NsCommandLiteral"]) const juliaWrapper = parseMixed((node, input) => { - if (!STRING_NODE_NAMES.has(node.type.name)) { + // TODO: only get .node once + if (node.name !== "NsStringLiteral" && node.name !== "StringLiteral") { return null } - let is_tripple_string = node.name === "TripleString" || node.name === "TripleStringWithoutInterpolation" + const first_string_delim = node.node.getChild('"""') ?? node.node.getChild('"') + if (first_string_delim == null) return null + const last_string_delim = node.node.lastChild + if (last_string_delim == null) return null - const offset = is_tripple_string ? 3 : 1 - const string_content_from = node.from + offset - const string_content_to = Math.min(node.to - offset, input.length) + const offset = first_string_delim.to - first_string_delim.from + console.log({ first_string_delim, last_string_delim, offset }) + const string_content_from = first_string_delim.to + const string_content_to = Math.min(last_string_delim.from, input.length) if (string_content_from >= string_content_to) { return null } - // Looking for tag OR MacroIdentifier - const tagNode = node.node?.prevSibling || node.node?.parent?.prevSibling - if (tagNode == null || (tagNode.name !== "MacroIdentifier" && tagNode.name !== "Prefix")) { - // If you can't find a tag node, something is broken in the julia syntax, - // so parse it as Julia. Probably wrong interpolation! - return null + let tagNode + if (node.name === "NsStringLiteral") { + tagNode = node.node.firstChild + // if (tagNode) tag = input.read(tagNode.from, tagNode.to) + } else { + // must be a string, let's search for the parent `@htl`. + const start = node.node + const p1 = start.parent + if (p1 != null && p1.name === "Arguments") { + const p2 = p1.parent + if (p2 != null && p2.name === "MacrocallExpression") { + tagNode = p2.getChild("MacroIdentifier") + } + } } + if (tagNode == null) return null + const is_macro = tagNode.name === "MacroIdentifier" const tag = input.read(tagNode.from, tagNode.to) @@ -129,7 +144,7 @@ const juliaWrapper = parseMixed((node, input) => { let child = node.node.firstChild.cursor() do { - if (last_content_start !== child.from) { + if (last_content_start < child.from) { overlay.push({ from: last_content_start, to: child.from }) } last_content_start = child.to From 364df4faff97c34670b805e42db201e5be745106 Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Fri, 17 Jan 2025 13:27:15 +0100 Subject: [PATCH 08/14] Fix bracket matching --- .../CellInput/block_matcher_plugin.js | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/frontend/components/CellInput/block_matcher_plugin.js b/frontend/components/CellInput/block_matcher_plugin.js index 14e77e8f33..7b82fd9288 100644 --- a/frontend/components/CellInput/block_matcher_plugin.js +++ b/frontend/components/CellInput/block_matcher_plugin.js @@ -30,7 +30,6 @@ function match_try_node(node) { finally_node && { from: finally_node.from, to: finally_node.to }, { from: possibly_end.from, to: possibly_end.to }, ].filter((x) => x != null) - } function match_block(node) { @@ -207,11 +206,7 @@ function match_block(node) { return decorations } - if (node.name === "try" - || node.name === "catch" - || node.name === "finally" - || node.name === "else") { - + if (node.name === "try" || node.name === "catch" || node.name === "finally" || node.name === "else") { if (node.name === "catch") node = node.parent if (node.name === "finally") node = node.parent if (node.name === "else") node = node.parent @@ -299,9 +294,11 @@ export function matchBrackets(state, pos, dir, config = {}) { node = tree.resolveInner(pos, dir) let result = match_block(node) - return result || matchPlainBrackets(state, pos, dir, tree, node.type, maxScanDistance, brackets) + return result || matchPlainBrackets(state, pos, dir, tree, bracket_node_name_normalizer(node.name), maxScanDistance, brackets) } + function matchPlainBrackets(state, pos, dir, tree, tokenType, maxScanDistance, brackets) { + console.log("trying to match", pos, dir, tokenType, maxScanDistance, brackets) let startCh = dir < 0 ? state.sliceDoc(pos - 1, pos) : state.sliceDoc(pos, pos + 1) let bracket = brackets.indexOf(startCh) if (bracket < 0 || (bracket % 2 == 0) != dir > 0) return null @@ -309,16 +306,18 @@ function matchPlainBrackets(state, pos, dir, tree, tokenType, maxScanDistance, b let iter = state.doc.iterRange(pos, dir > 0 ? state.doc.length : 0), depth = 0 for (let distance = 0; !iter.next().done && distance <= maxScanDistance; ) { + console.log("matching", distance, pos, dir, tokenType, maxScanDistance, brackets) let text = iter.value if (dir < 0) distance += text.length let basePos = pos + distance * dir for (let pos = dir > 0 ? 0 : text.length - 1, end = dir > 0 ? text.length : -1; pos != end; pos += dir) { let found = brackets.indexOf(text[pos]) - if (found < 0 || tree.resolve(basePos + pos, 1).type != tokenType) continue + if (found < 0 || bracket_node_name_normalizer(tree.resolve(basePos + pos, 1).name) != tokenType) continue if ((found % 2 == 0) == dir > 0) { depth++ } else if (depth == 1) { // Closing + console.log("zomgg") if (found >> 1 == bracket >> 1) { return [startToken, { from: basePos + pos, to: basePos + pos + 1 }] } else { @@ -330,5 +329,25 @@ function matchPlainBrackets(state, pos, dir, tree, tokenType, maxScanDistance, b } if (dir > 0) distance += text.length } + console.log(iter.done, startToken) return iter.done ? [startToken] : null } + +/** + * Little modification to the original matchPlainBrackets function: in our Julia language, the node that opens a bracket is called "(". In e.g. markdown it's called LinkMark or something (the same name for opening and closing). We don't have this so we make them equal. + */ +const bracket_node_name_normalizer = (/** @type {String} */ node_name) => { + switch (node_name) { + case "(": + case ")": + return "()" + case "[": + case "]": + return "[]" + case "{": + case "}": + return "{}" + default: + return node_name + } +} From 459ebf26e851fa9b10fef6b8a8cf3d5c2bef382b Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Fri, 17 Jan 2025 13:30:23 +0100 Subject: [PATCH 09/14] Update block_matcher_plugin.js --- frontend/components/CellInput/block_matcher_plugin.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/frontend/components/CellInput/block_matcher_plugin.js b/frontend/components/CellInput/block_matcher_plugin.js index 7b82fd9288..81bc91b8f0 100644 --- a/frontend/components/CellInput/block_matcher_plugin.js +++ b/frontend/components/CellInput/block_matcher_plugin.js @@ -298,7 +298,6 @@ export function matchBrackets(state, pos, dir, config = {}) { } function matchPlainBrackets(state, pos, dir, tree, tokenType, maxScanDistance, brackets) { - console.log("trying to match", pos, dir, tokenType, maxScanDistance, brackets) let startCh = dir < 0 ? state.sliceDoc(pos - 1, pos) : state.sliceDoc(pos, pos + 1) let bracket = brackets.indexOf(startCh) if (bracket < 0 || (bracket % 2 == 0) != dir > 0) return null @@ -306,7 +305,6 @@ function matchPlainBrackets(state, pos, dir, tree, tokenType, maxScanDistance, b let iter = state.doc.iterRange(pos, dir > 0 ? state.doc.length : 0), depth = 0 for (let distance = 0; !iter.next().done && distance <= maxScanDistance; ) { - console.log("matching", distance, pos, dir, tokenType, maxScanDistance, brackets) let text = iter.value if (dir < 0) distance += text.length let basePos = pos + distance * dir @@ -317,7 +315,6 @@ function matchPlainBrackets(state, pos, dir, tree, tokenType, maxScanDistance, b depth++ } else if (depth == 1) { // Closing - console.log("zomgg") if (found >> 1 == bracket >> 1) { return [startToken, { from: basePos + pos, to: basePos + pos + 1 }] } else { @@ -329,7 +326,6 @@ function matchPlainBrackets(state, pos, dir, tree, tokenType, maxScanDistance, b } if (dir > 0) distance += text.length } - console.log(iter.done, startToken) return iter.done ? [startToken] : null } From 6a3acc4038789422ee9320328ad308d841154e8b Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Fri, 17 Jan 2025 13:45:14 +0100 Subject: [PATCH 10/14] Create cm6 crash test.jl --- sample/cm6 crash test.jl | 408 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 408 insertions(+) create mode 100644 sample/cm6 crash test.jl diff --git a/sample/cm6 crash test.jl b/sample/cm6 crash test.jl new file mode 100644 index 0000000000..3c3fecad28 --- /dev/null +++ b/sample/cm6 crash test.jl @@ -0,0 +1,408 @@ +### A Pluto.jl notebook ### +# v0.20.4 + +using Markdown +using InteractiveUtils + +# ╔═╡ e19bd550-c9dd-11ef-02bd-795cab4c96c6 +begin + + # false && Pkg.activate() + + + + import A + import B, C as eeeEE, FIFIFIFI + using XXD: asdf, E + using Dee: asdeeef, eeeE + + if false + :(import AA) + end +end + +# ╔═╡ d0ee4518-406a-4628-ac4a-30d5fa11f5a7 +md""" +# Difficult syntax +""" + +# ╔═╡ e746ef9c-47c1-4d5d-bce1-4616f25ff586 +( 1 + 2) + +# ╔═╡ 5666ac41-9e8d-40fc-89be-e2a6c1637da5 +# https://github.com/fonsp/Pluto.jl/issues/2382 +@a f(x-1 : g(x)) + +# ╔═╡ bf834c19-3d0d-4989-9ba3-ef7cb77f9a00 +# https://github.com/fonsp/Pluto.jl/issues/1921 +begin + # double click me + abc + ab! + aπc + @ab + ab😎 + a😎c + 🌟🌟🌟 +end + +# ╔═╡ 3da33f9d-1240-4522-9463-8772b0c2539a +# https://github.com/fonsp/Pluto.jl/issues/2063 +@amacro begin + x = a : b:c +end + +# ╔═╡ daba5486-8d5e-4fce-959b-251e821e5dea +# https://github.com/fonsp/Pluto.jl/issues/2639 +let x = 1 end + +# ╔═╡ 287dd3c7-33e6-482d-9639-d502fcff9234 +# https://github.com/fonsp/Pluto.jl/issues/2639 +let f() = 1 end + +# ╔═╡ 6db1e583-54f2-4d8f-9181-a7913345c7fd +# https://github.com/fonsp/Pluto.jl/issues/2095 +Bool <: Integer + +# ╔═╡ 3571549e-335b-4fbb-944f-d071db32b29f +(z for z in z) + +# ╔═╡ 7d8f150f-0625-463b-a964-34cd7cc8fda2 +# https://github.com/fonsp/Pluto.jl/issues/2731 +let + f() = let (x,y) = z + "$x" + end + # this doesn't look like a "comment" at all + # that's better +end + +# ╔═╡ 8dec1241-8bd9-4c60-935a-9230b39813b7 +# https://github.com/fonsp/Pluto.jl/issues/2875 +"const x = hey there! + +# ╔═╡ 466cd22c-bc46-4e35-b3c8-b6f884fd0858 +# https://github.com/fonsp/Pluto.jl/issues/2875 +md""" + +asdf + +sdf [23]() + +# asf _sdf_ + +sdf + +# ╔═╡ 259280cc-5468-4e7d-94c5-6679d7059685 +# https://github.com/fonsp/Pluto.jl/issues/3065 +x.var"abc" +x.var"# abc" +var"# abc" + +# ╔═╡ 908c565f-ea71-488c-8298-0c31757fb80c +# https://github.com/fonsp/Pluto.jl/issues/3080 +# https://github.com/JuliaPluto/lezer-julia/issues/24 +[@show (index) for index in eachindex(r_v)] + +# ╔═╡ 0402fff9-ea92-4302-861d-f40fba31ee61 +# https://github.com/fonsp/Pluto.jl/issues/3116 +quote + @huh begin + a(x,y) = a(x)y(y) + end +end + +# ╔═╡ e397f96e-8f91-4de6-9b12-730ae38ecc27 +# https://github.com/fonsp/Pluto.jl/issues/3131 +a = r"aa" => s"" +b = a"s" + +# ╔═╡ 1e5b8ba7-a0d4-458e-b6c3-10152e87b328 +md""" +# Scope stuff +""" + +# ╔═╡ 2a022568-47c9-4668-b91e-1bbc834e8d00 +quote +end + +# ╔═╡ d321e6ca-91a8-4e9b-9f54-be97735ab866 + + +# ╔═╡ 3a9fc24d-6767-4e48-b912-863c2cd6ddbc +begin + a, b = 123, 33 +end + +# ╔═╡ c93f0182-3e16-4ff5-a612-5cd2e70d249f +( + a + b + for a in 1:10 +) + +# ╔═╡ 2bdbece1-f111-40ee-b6f0-5cbd79fc4ff7 +for a = b, a=b + let + b + end +end + +# ╔═╡ 06f8bd08-a036-4534-863c-0f1470f62baa +a, b + +# ╔═╡ 0fb082a6-bc05-4dd7-9535-0f4fb3cdadb8 +try + a +catch a + a +end + +# ╔═╡ 62b805db-2028-40b0-b412-4c91d15ad338 +c = d = e = f= g =h=i=j=k=l=m=n=o=p=q=r=9 + +# ╔═╡ b57da300-58b5-498b-9798-c565dbb74026 +f(g((args...; kwarg1=dict[index], karg2=kwarg2) -> X)) + + +# ╔═╡ 63bd761a-1bc4-4859-a0c2-bbbeff613cba +for a in b:c, (b,c) in a:c + a + b + c + d +end + +# ╔═╡ a219d3da-3201-4a76-8d26-1df5251a2890 +for a = b:c + a + b + c + d +end + +# ╔═╡ 5daae1d8-a122-48e5-b6d4-918df4120e3d +fa(x, n) = g(a) + +# ╔═╡ 71690dcf-d3a9-49e6-9f59-adbb5b379571 +begin + ∘(x, n) = g(a + a) +end + +# ╔═╡ 43a2db54-a64f-49e8-9edc-2726becb7a38 +Fⁿ(x, n) = ∘(fill(F, n)...)(x); + +# ╔═╡ 74022839-85be-44b6-9dd0-7c9eaba70053 +macro yay(xs...) +xs[1] +end + +# ╔═╡ 10742d7d-81b5-436c-9ac8-04f920373c9c +@yay 123 a b = 1 + +# ╔═╡ 698876c5-8dec-49df-8309-63e69ddf0f1a +[ + a b + a a +] + +# ╔═╡ 3af8b76a-6534-44d6-8456-59a1b139efb2 +begin + a + b + + let a = 123 + + a + b + end + let b = 123 + + a + b + end + + let a = 123 + + let b = 123 + + a + b + end + end + + a + b +end + +# ╔═╡ b7d6a7fc-8286-4cf9-af62-4dedefc66c97 +x(a) = a + b + +# ╔═╡ 69acb688-3726-4252-b7e2-f496181d2aa6 +# https://github.com/fonsp/Pluto.jl/issues/2382 +f(x-1 : g(x)) + +# ╔═╡ c88fe37a-c2e6-46f1-b92b-98737437a741 +# https://github.com/fonsp/Pluto.jl/issues/2575 +[√p for p in x] + +# ╔═╡ 17c063da-5bdd-4ee1-b881-42acc6777610 +# https://github.com/fonsp/Pluto.jl/issues/2724 +let + x + [f(y[i]) for i in v] +end + +# ╔═╡ 8b6d03e1-2196-47e3-9a60-c1245c7b7849 +asdf() do a, b + x + a + b + c +end + +# ╔═╡ 57b3a2b8-ee9c-45dd-9819-fdb561135cb1 +ff2(a) = begin + a + b +end + +# ╔═╡ 45171038-97f5-4662-acd1-da7748443700 +z[a][2] = 22233 + +# ╔═╡ d4786341-13ab-4a79-8077-a66ffe09427a +z.a = 123 + +# ╔═╡ ac4d6ffd-4d5a-4932-ba35-1c5a56b42122 +let + + zzz.a.b + + a.zzz.b + a.zzz.b(b) + + a[b] + +end + +# ╔═╡ 3a506c19-2928-40ab-b6b9-e9288dd31eeb +let + a = b + + a + b +end + +# ╔═╡ 40079547-4cfc-4557-8e88-b2182903f9fb +yoo(x,y) = 123333 + +# ╔═╡ 9f9e083f-87c2-4c83-89bc-90a4e5f42c96 +function() + z + yef +end + +# ╔═╡ 85eaec36-5b60-4852-a0b0-d1b477d7b43e +a, b + +# ╔═╡ bf1140c5-e0b7-42a2-94fe-623fd4c8b8dc + + +# ╔═╡ 4a4e0029-899e-46e5-a101-90d8a64de073 +# @quickactivate + +# ╔═╡ b6ece33c-8f33-4219-b711-ef3ebb30b0bb + + +# ╔═╡ 67150120-fc85-448f-91c4-aaf69145900c +md""" +# Package imports +""" + +# ╔═╡ 20276c02-8e7c-41a6-ad3d-5ce84eadf025 +md""" +# Mixed mode +""" + +# ╔═╡ 5360ae7a-b43c-404b-b039-76485d12fa2f +md""" + +# asdfasdf + + + + + + + + +"""; # sdf + +# ╔═╡ e9019c45-4609-4640-8a6f-345129c54ac4 +@aaa """ + + +""" + +# ╔═╡ 422b8bb5-d910-4aaa-b8c2-a781c0b8b6f0 +@aaa(""" + + +""") + +# ╔═╡ a35574d7-c7eb-4ffa-b8ef-5e8aef7aa3e9 +begin + function ff(a, b=2; c=2, d) + a + b + c + d + e + f + end + xxx +end + +# ╔═╡ 99c58cfd-e400-458d-8304-8c68bce3a769 +begin + ff(a, b=2; c=2, d) = a + b + c + d + f + xxx +end + +# ╔═╡ Cell order: +# ╟─d0ee4518-406a-4628-ac4a-30d5fa11f5a7 +# ╠═e746ef9c-47c1-4d5d-bce1-4616f25ff586 +# ╠═b57da300-58b5-498b-9798-c565dbb74026 +# ╠═69acb688-3726-4252-b7e2-f496181d2aa6 +# ╠═5666ac41-9e8d-40fc-89be-e2a6c1637da5 +# ╠═bf834c19-3d0d-4989-9ba3-ef7cb77f9a00 +# ╠═3da33f9d-1240-4522-9463-8772b0c2539a +# ╠═c88fe37a-c2e6-46f1-b92b-98737437a741 +# ╠═daba5486-8d5e-4fce-959b-251e821e5dea +# ╠═287dd3c7-33e6-482d-9639-d502fcff9234 +# ╠═6db1e583-54f2-4d8f-9181-a7913345c7fd +# ╠═17c063da-5bdd-4ee1-b881-42acc6777610 +# ╠═3571549e-335b-4fbb-944f-d071db32b29f +# ╠═7d8f150f-0625-463b-a964-34cd7cc8fda2 +# ╠═8dec1241-8bd9-4c60-935a-9230b39813b7 +# ╠═466cd22c-bc46-4e35-b3c8-b6f884fd0858 +# ╠═259280cc-5468-4e7d-94c5-6679d7059685 +# ╠═908c565f-ea71-488c-8298-0c31757fb80c +# ╠═0402fff9-ea92-4302-861d-f40fba31ee61 +# ╠═e397f96e-8f91-4de6-9b12-730ae38ecc27 +# ╠═c93f0182-3e16-4ff5-a612-5cd2e70d249f +# ╟─1e5b8ba7-a0d4-458e-b6c3-10152e87b328 +# ╠═63bd761a-1bc4-4859-a0c2-bbbeff613cba +# ╠═a219d3da-3201-4a76-8d26-1df5251a2890 +# ╠═2bdbece1-f111-40ee-b6f0-5cbd79fc4ff7 +# ╠═06f8bd08-a036-4534-863c-0f1470f62baa +# ╠═8b6d03e1-2196-47e3-9a60-c1245c7b7849 +# ╠═2a022568-47c9-4668-b91e-1bbc834e8d00 +# ╠═0fb082a6-bc05-4dd7-9535-0f4fb3cdadb8 +# ╠═a35574d7-c7eb-4ffa-b8ef-5e8aef7aa3e9 +# ╠═99c58cfd-e400-458d-8304-8c68bce3a769 +# ╠═5daae1d8-a122-48e5-b6d4-918df4120e3d +# ╠═71690dcf-d3a9-49e6-9f59-adbb5b379571 +# ╠═43a2db54-a64f-49e8-9edc-2726becb7a38 +# ╠═d321e6ca-91a8-4e9b-9f54-be97735ab866 +# ╠═3a9fc24d-6767-4e48-b912-863c2cd6ddbc +# ╠═62b805db-2028-40b0-b412-4c91d15ad338 +# ╠═74022839-85be-44b6-9dd0-7c9eaba70053 +# ╠═10742d7d-81b5-436c-9ac8-04f920373c9c +# ╠═698876c5-8dec-49df-8309-63e69ddf0f1a +# ╠═3af8b76a-6534-44d6-8456-59a1b139efb2 +# ╠═b7d6a7fc-8286-4cf9-af62-4dedefc66c97 +# ╠═57b3a2b8-ee9c-45dd-9819-fdb561135cb1 +# ╠═45171038-97f5-4662-acd1-da7748443700 +# ╠═d4786341-13ab-4a79-8077-a66ffe09427a +# ╠═ac4d6ffd-4d5a-4932-ba35-1c5a56b42122 +# ╠═3a506c19-2928-40ab-b6b9-e9288dd31eeb +# ╠═40079547-4cfc-4557-8e88-b2182903f9fb +# ╠═9f9e083f-87c2-4c83-89bc-90a4e5f42c96 +# ╠═85eaec36-5b60-4852-a0b0-d1b477d7b43e +# ╠═bf1140c5-e0b7-42a2-94fe-623fd4c8b8dc +# ╠═4a4e0029-899e-46e5-a101-90d8a64de073 +# ╠═b6ece33c-8f33-4219-b711-ef3ebb30b0bb +# ╟─67150120-fc85-448f-91c4-aaf69145900c +# ╠═e19bd550-c9dd-11ef-02bd-795cab4c96c6 +# ╟─20276c02-8e7c-41a6-ad3d-5ce84eadf025 +# ╠═5360ae7a-b43c-404b-b039-76485d12fa2f +# ╠═e9019c45-4609-4640-8a6f-345129c54ac4 +# ╠═422b8bb5-d910-4aaa-b8c2-a781c0b8b6f0 From b7193cbceb5fea0bfa1048627145269fe95711da Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Fri, 17 Jan 2025 14:19:01 +0100 Subject: [PATCH 11/14] Remove frontend AST pattern matching (WIP) (#3132) * Remove frontend AST pattern matching (WIP) * pkg bubble plugin without lezer template * uhmmm * remove log * write new scope analysis based on TreeCursor and remove old code * more precise function stuff * support for loop, fix weird iteration * Support try catch, fix console group, comments * Support module, quote, :, do * New API: Pluto.is_single_expression (#3134) * Update scopestate_statefield.js * Fix #3136 * Fix #3136 * Update scopestate_statefield.js * Fix bracket matching * Update block_matcher_plugin.js * Update scopestate_statefield.js * fix ts errors * Support scope in generator expressions --- .../CellInput/debug_syntax_plugin.js | 4 +- .../components/CellInput/lezer_template.js | 1068 --------------- .../components/CellInput/pkg_bubble_plugin.js | 183 +-- .../CellInput/pluto_autocomplete.js | 2 + .../CellInput/scopestate_statefield.js | 1209 ++++------------- .../components/CellInput/tests/.gitignore | 1 - frontend/components/CellInput/tests/README.md | 14 - .../CellInput/tests/import_map.json | 5 - .../CellInput/tests/scopestate.test.js | 151 -- .../CellInput/tests/scopestate_helpers.js | 75 - frontend/imports/CodemirrorPlutoSetup.d.ts | 29 +- frontend/imports/CodemirrorPlutoSetup.js | 3 +- sample/cm6 crash test.jl | 4 + src/analysis/Parse.jl | 18 + src/webserver/Dynamic.jl | 1 - src/webserver/SessionActions.jl | 4 +- test/misc API.jl | 43 + test/runtests.jl | 1 + 18 files changed, 406 insertions(+), 2409 deletions(-) delete mode 100644 frontend/components/CellInput/tests/.gitignore delete mode 100644 frontend/components/CellInput/tests/README.md delete mode 100644 frontend/components/CellInput/tests/import_map.json delete mode 100644 frontend/components/CellInput/tests/scopestate.test.js delete mode 100644 frontend/components/CellInput/tests/scopestate_helpers.js create mode 100644 test/misc API.jl diff --git a/frontend/components/CellInput/debug_syntax_plugin.js b/frontend/components/CellInput/debug_syntax_plugin.js index 83d4d6ca5d..387676e8ba 100644 --- a/frontend/components/CellInput/debug_syntax_plugin.js +++ b/frontend/components/CellInput/debug_syntax_plugin.js @@ -1,8 +1,8 @@ -import { EditorView, syntaxTree, syntaxTreeAvailable } from "../../imports/CodemirrorPlutoSetup.js" +import { EditorView, syntaxTree, syntaxTreeAvailable, Text } from "../../imports/CodemirrorPlutoSetup.js" import { iterate_with_cursor } from "./lezer_template.js" /** - * @param {any} doc + * @param {Text} doc * @param {ReturnType} tree */ let find_error_nodes = (doc, tree) => { diff --git a/frontend/components/CellInput/lezer_template.js b/frontend/components/CellInput/lezer_template.js index 64f4723d76..beeb255a73 100644 --- a/frontend/components/CellInput/lezer_template.js +++ b/frontend/components/CellInput/lezer_template.js @@ -1,1033 +1,3 @@ -import { julia, NodeProp, syntaxTree, Text } from "../../imports/CodemirrorPlutoSetup.js" -import lodash from "../../imports/lodash.js" - -// @ts-ignore -import ManyKeysWeakMap from "https://esm.sh/many-keys-weakmap@1.0.0?pin=v113&target=es2020" - -/** - * @param {string} julia_code - * @returns {SyntaxNode} - */ -export let julia_to_ast = (julia_code) => { - return /** @type {any} */ (julia().language.parser.parse(julia_code).topNode.firstChild) -} - -// When you get errors while creating the templates (stuff related to substitutions), -// turn this on, and you will get a lot of info you can debug with! -const TEMPLATE_CREATION_VERBOSE = false - -/** - * Settings this to `"VALIDITY"` will enable some (currently, one) slow validations. - * Might be useful to run set this to `"VALIDITY"` every so often to make sure there are no bugs. - * In production this should always to `"SPEED"` - * - * @type {"SPEED" | "VALIDITY"} - */ -const PERFORMANCE_MODE = /** @type {any} */ ("VALIDITY") - -const IS_IN_VALIDATION_MODE = PERFORMANCE_MODE === "VALIDITY" - -/** - * @template {Array} P - * @template {(...args: P) => any} T - * @param {T} fn - * @param {(...args: P) => any} cachekey_resolver - * @param {WeakMap} cache - * @returns {T} - */ -let memo = (fn, cachekey_resolver = /** @type {any} */ ((x) => x), cache = new Map()) => { - return /** @type {any} */ ( - (/** @type {P} */ ...args) => { - let cachekey = cachekey_resolver(...args) - let result = cache.get(cachekey) - if (result != null) { - return result - } else { - let result = fn(...args) - - if (result == undefined) { - throw new Error("Memoized function returned undefined") - } - cache.set(cachekey, result) - return result - } - } - ) -} - -/** - * @template {(...args: any) => any} T - * @param {T} fn - * @param {(...x: Parameters) => Array} cachekey_resolver - * @returns {T} - */ -let weak_memo = (fn, cachekey_resolver = (...x) => x) => memo(fn, cachekey_resolver, new ManyKeysWeakMap()) - -/** - * @template {(arg: any) => any} T - * @param {T} fn - * @returns {T} - */ -let weak_memo1 = (fn) => memo(fn, (x) => x, new WeakMap()) - -// Good luck figuring anything out from these types 💕 - -/** - * @typedef TreeCursor - * @type {import("../../imports/CodemirrorPlutoSetup.js").TreeCursor} - * - * @typedef SyntaxNode - * @type {TreeCursor["node"]} - */ - -/** - * @typedef LezerOffsetNode - * @type {{ - * name: string, - * from: number, - * to: number, - * node: SyntaxNode, - * }} - */ - -/** - * @typedef TemplateGenerator - * @type {Generator} - */ - -/** - * @typedef Substitution - * @type {() => TemplateGenerator} - */ - -/** - * @typedef Templatable - * @type {JuliaCodeObject | Substitution} - */ - -/** - * @typedef MatchResult - * @type {any} - */ - -/** - * @typedef Matcher - * @type {{ - * match: (haystack_cursor: TreeCursor | SyntaxNode, verbose?: boolean) => void | { [key: string]: MatchResult } - * }} - */ - -/** @param {TreeCursor} cursor */ -export let child_cursors = function* (cursor) { - if (cursor.firstChild()) { - try { - do { - yield cursor - } while (cursor.nextSibling()) - } finally { - cursor.parent() - } - } -} - -/** @param {SyntaxNode} node */ -export let child_nodes = function* (node) { - if (node.firstChild) { - /** @type {SyntaxNode?} */ - let child = node.firstChild - do { - yield child - } while ((child = child.nextSibling)) - } -} - -/** - * @typedef AstTemplate - * @type {{ - * name?: string, - * from?: number, - * to?: number, - * node: SyntaxNode, - * children: Array, - * } | { - * pattern: ( - * haystack: (TreeCursor | null), - * matches: { [key: string]: any }, - * verbose?: boolean - * ) => boolean, - * }} - */ - -/** - * @param {TreeCursor | null} haystack_cursor - * @param {AstTemplate} template - * @param {{ [name: string]: any }} matches - * @param {boolean} verbose - */ -export let match_template = (haystack_cursor, template, matches, verbose = false) => { - if (verbose) { - console.group("Current haystack:", !haystack_cursor ? null : haystack_cursor.node.name) - - console.groupCollapsed("Details") - try { - console.log(`template:`, template) - console.log(`haystack_cursor:`, !haystack_cursor ? null : haystack_cursor.node.toString()) - - if ("node" in template) { - console.log(`template.node:`, template.node) - console.log(`template.node.toString():`, template.node.toString()) - } else if ("pattern" in template) { - console.log(`template.pattern:`, template.pattern) - } - } finally { - console.groupEnd() - } - } - - try { - if ("pattern" in template) { - let pattern = template.pattern - if (typeof pattern !== "function") { - throw new Error(`Unknown pattern "${pattern}"`) - } - - let matches_before_matching = {} - if (verbose) { - matches_before_matching = { ...matches } - console.groupCollapsed(`Matching against pattern: ${template.pattern.name}()`) - } - - let did_match = null - try { - did_match = pattern(haystack_cursor, matches, verbose) - } finally { - if (verbose) { - console.groupEnd() - } - } - if (verbose) { - if (did_match) { - console.log(`✅ because the pattern was happy! All hail the pattern!`) - if (!lodash.isEqual(matches, matches_before_matching)) { - let new_matches = lodash.omit(matches, Object.keys(matches_before_matching)) - console.log(` WE EVEN GOT NEW MATCHES SAY WHAAAAAAT:`, new_matches) - } - } else { - console.log(`❌ because... well, you should ask the pattern that!`) - } - } - - return did_match - } else if ("node" in template) { - let { node, children } = template - - verbose && console.log(`Matching against node: ${template.node.name}`) - - if (!haystack_cursor) { - if (node.name === "end") { - verbose && console.log(`✅ No node left to match, but it was the end anyway`) - return true - } - verbose && console.log(`❌ because no cursor left to match against`) - return false - } - - if (haystack_cursor.type.isError) { - // Not sure about this yet but ehhhh - verbose && console.log(`✅ because ⚠`) - return true - } - - if (haystack_cursor.name !== node.name) { - verbose && console.log(`❌ because name mismatch "${haystack_cursor.name}" !== "${node.name}"`) - return false - } - - if (haystack_cursor.firstChild()) { - try { - let is_last_from_haystack = false - for (let template_child of children) { - if (is_last_from_haystack) { - verbose && console.log(`Haystack is empty, but there are more children in template... lets see`) - let child_does_match = match_template(null, template_child, matches, verbose) - if (!child_does_match) { - verbose && console.log(`❌ template child did not accept null for an answer`, template_child, haystack_cursor.toString()) - return false - } - verbose && console.log(`👌🏽 This template child was fine with null`) - continue - } - - // Skip comments - // TODO This is, I think, one of the few julia-only things right now..... - // .... Any sane way to factor this out? - while (haystack_cursor.name === "LineComment" || haystack_cursor.name === "BlockComment") { - if (!haystack_cursor.nextSibling()) break - } - - let child_does_match = match_template(haystack_cursor, template_child, matches, verbose) - - if (!child_does_match) { - verbose && console.log(`❌ because a child mismatch`, template_child, haystack_cursor.toString()) - return false - } - - // This is where we actually move the haystack_cursor (in sync with the `template.children`) - // to give the nested match templates more freedom in move the cursor around between siblings. - is_last_from_haystack = !haystack_cursor.nextSibling() - } - - if (verbose && !is_last_from_haystack) { - let spare_children = [] - do { - spare_children.push(haystack_cursor.node) - } while (haystack_cursor.nextSibling()) - for (let child of spare_children) { - haystack_cursor.prevSibling() - } - - // prettier-ignore - console.warn("We did match all the children of the template, but there are more in the haystack... Might want to actually not match this?", spare_children, template) - } - - verbose && console.log(`✅ because all children match`) - return true - } finally { - haystack_cursor.parent() - } - } else { - if (template.children.length !== 0) { - verbose && console.log(`Haystack node is empty, but template has children... lets see`) - - for (let child of template.children) { - if (!match_template(null, child, matches, verbose)) { - verbose && console.log(`❌ because child template wasn't okay with having no children`, child) - return false - } - } - verbose && console.log(`✅ All template children we're fine with having no children to check on`) - return true - } else { - verbose && console.log(`✅ Template also has no children, yayyy`) - return true - } - } - } else { - console.log(`template:`, template) - throw new Error("waaaah") - } - } finally { - if (verbose) { - console.groupEnd() - } - } -} - -export class JuliaCodeObject { - /** - * @param {TemplateStringsArray} template - * @param {any[]} substitutions - * */ - constructor(template, substitutions) { - let flattened_template = [] - let flattened_substitutions = [] - - flattened_template.push(template[0]) - for (let [string_part, substitution] of lodash.zip(template.slice(1), substitutions)) { - if (substitution instanceof JuliaCodeObject) { - flattened_template[flattened_template.length - 1] += substitution.template[0] - for (let [sub_string_part, sub_substitution] of lodash.zip(substitution.template.slice(1), substitution.substitutions)) { - flattened_substitutions.push(sub_substitution) - flattened_template.push(sub_string_part) - } - flattened_template[flattened_template.length - 1] += string_part - } else { - flattened_substitutions.push(substitution) - flattened_template.push(string_part) - } - } - - this.template = flattened_template - this.substitutions = flattened_substitutions - } -} - -/** - * @param {SyntaxNode} ast - * @param {Array<{ generator: TemplateGenerator, from: number, to: number, used?: boolean }>} substitutions - * @returns {AstTemplate} - */ -let substitutions_to_template = (ast, substitutions) => { - for (let substitution of substitutions) { - if (ast.from === substitution.from && ast.to === substitution.to) { - // Hacky and weird, but it is only for validation - substitution.used = true - let result = substitution.generator.next({ - name: ast.name, - from: ast.from, - to: ast.to, - node: ast, - }) - - if (result.done) { - return result.value - } else { - throw new Error("Template generator not done providing ast node") - } - } - } - - return { - name: ast.name, - from: ast.from, - to: ast.to, - children: Array.from(child_nodes(ast)).map((node) => substitutions_to_template(node, substitutions)), - node: ast, - } -} - -export let node_to_explorable = (cursor) => { - if ("cursor" in cursor) { - cursor = cursor.cursor() - } - - let children = [] - if (cursor.firstChild()) { - try { - do { - children.push(node_to_explorable(cursor)) - } while (cursor.nextSibling()) - } finally { - cursor.parent() - } - } - return { - name: cursor.name, - from: cursor.from, - to: cursor.to, - children, - } -} - -/** - * @param {Templatable} julia_code_object - * @returns {TemplateGenerator} - */ -export let to_template = function* (julia_code_object) { - TEMPLATE_CREATION_VERBOSE && console.group(`to_template(`, typeof julia_code_object === "function" ? julia_code_object.name + "()" : julia_code_object, `)`) - try { - if (julia_code_object instanceof JuliaCodeObject) { - let julia_code_to_parse = "" - - let subsitions = [] - for (let [string_part, substitution] of lodash.zip(julia_code_object.template, julia_code_object.substitutions)) { - julia_code_to_parse += string_part - - if (substitution) { - let substitution_generator = to_template(substitution) - - let substitution_code = intermediate_value(substitution_generator.next()) - - subsitions.push({ - from: julia_code_to_parse.length, - to: julia_code_to_parse.length + substitution_code.length, - generator: substitution_generator, - }) - julia_code_to_parse += substitution_code - } - } - - let template_node = yield julia_code_to_parse - - let substitution_with_proper_position = subsitions.map((substitution) => { - return { - from: substitution.from + template_node.from, - to: substitution.to + template_node.from, - generator: substitution.generator, - used: false, - } - }) - - if (TEMPLATE_CREATION_VERBOSE) { - console.log(`julia_code_to_parse:`, julia_code_to_parse) - console.log(`template_node:`, node_to_explorable(template_node.node.cursor())) - console.log(`subsitions:`, subsitions) - console.log(`substitution_with_proper_position:`, substitution_with_proper_position) - } - - let result = substitutions_to_template(template_node.node, substitution_with_proper_position) - let unused_substitutions = substitution_with_proper_position - .filter((substitution) => !substitution.used) - .map((x) => { - return { - text: julia_code_to_parse.slice(x.from, x.to), - from: x.from, - to: x.to, - } - }) - if (unused_substitutions.length > 0) { - throw new Error( - `Some substitutions not applied, this means it couldn't be matched to a AST position.\n\nUnused substitutions: ${JSON.stringify( - unused_substitutions - )}\n` - ) - } - return result - } else if (typeof julia_code_object === "function") { - return yield* julia_code_object() - } else { - console.log(`julia_code_object:`, julia_code_object) - throw new Error("Unknown substition type") - } - } finally { - TEMPLATE_CREATION_VERBOSE && console.groupEnd() - } -} - -/** - * @param {TemplateStringsArray} template - * @param {any[]} substitutions - * */ -export let jl_dynamic = weak_memo((template, ...substitutions) => { - return new JuliaCodeObject(template, substitutions) -}) - -/** @type {WeakMap, result: JuliaCodeObject }>} */ -let template_cache = new WeakMap() - -/** - * @param {TemplateStringsArray} template - * @param {Array} substitutions - */ -export let jl = (template, ...substitutions) => { - let cached = template_cache.get(template) - if (cached != null) { - let { input, result } = cached - if (IS_IN_VALIDATION_MODE) { - if (!lodash.isEqual(substitutions, input)) { - console.trace("Substitutions changed on `jl` template string.. change to `jl_dynamic` if you need this.") - } - } - return result - } else { - // Uncomment this if you want to check if the cache is working - // console.log("Creating template for", template, substitutions) - let result = new JuliaCodeObject(template, substitutions) - template_cache.set(template, { - input: substitutions, - result: result, - }) - return result - } -} - -/** - * Turns a ``` jl`` ``` (or substitution) into a template with a `.match(cursor)` method. - * - * @type {(code: Templatable) => Matcher} - */ -export let template = weak_memo1((julia_code_object) => { - let template_generator = to_template(julia_code_object) - let julia_to_parse = intermediate_value(template_generator.next()) - let template_ast = julia_to_ast(julia_to_parse) - - let template_description = return_value( - template_generator.next({ - from: 0, - to: julia_to_parse.length, - name: template_ast.name, - node: /** @type {any} */ (template_ast), - }) - ) - - return /** @type {Matcher} */ ({ - /** - * @param {TreeCursor | SyntaxNode} haystack_cursor - * @param {boolean} verbose? - **/ - template_description, - match(haystack_cursor, verbose = false) { - // Performance gain for not converting to `TreeCursor` possibly 🤷‍♀️ - if ("node" in template_description && template_description.node.name !== haystack_cursor.name) return - if (haystack_cursor.type.isError) return null - - if ("cursor" in haystack_cursor) haystack_cursor = haystack_cursor.cursor() - - let matches = /** @type {{ [key: string]: MatchResult }} */ ({}) - - verbose && console.groupCollapsed(`Starting a match at ${haystack_cursor.name}`) - try { - return match_template(haystack_cursor, template_description, matches, verbose) ? matches : null - } finally { - verbose && console.groupEnd() - } - }, - }) -}) - -/** - * - */ -export let as_string = weak_memo1((/** @type {JuliaCodeObject} */ julia_code_object) => { - let template_generator = to_template(julia_code_object) - let julia_to_parse = intermediate_value(template_generator.next()) - // @ts-ignore - template_generator.return() - return julia_to_parse -}) - -export let as_node = weak_memo1((/** @type {JuliaCodeObject} */ julia_code_object) => { - return julia_to_ast(as_string(julia_code_object)) -}) - -export let as_doc = weak_memo1((/** @type {JuliaCodeObject} */ julia_code_object) => { - return Text.of([as_string(julia_code_object)]) -}) - -/** - * @template {(name: string, other_arg: Object) => any} T - * @param {T} func - * @returns {T} - **/ -let memo_first_argument_weakmemo_second = (func) => { - let fake_weakmap_no_arg = {} - let per_name_memo = memo((name) => { - return weak_memo1((arg) => { - if (arg === fake_weakmap_no_arg) return func(name, undefined) - return func(name, arg) - }) - }) - - return /** @type {T} */ ( - (name, arg = fake_weakmap_no_arg) => { - let sub_memo = per_name_memo(name) - return sub_memo(arg) - } - ) -} - -/** @type {Substitution} */ -function* any() { - yield "expression" - return { - pattern: function expression(cursor, matches, verbose = false) { - if (!cursor) { - verbose && console.log("❌ I want anything!! YOU GIVE ME NULL???") - return false - } - if (cursor.type.is("keyword")) { - verbose && console.log("❌ Keywords are not allowed!") - return false - } - return true - }, - } -} - -/** - * @param {TemplateGenerator} template_generator - * @return {TemplateGenerator} - * */ -function* narrow_template(template_generator) { - let ast = yield intermediate_value(template_generator.next()) - - if (ast.node.firstChild && ast.node.from === ast.node.firstChild.from && ast.node.to === ast.node.firstChild.to) { - console.log("Narrowing!!!!", ast.node, ast.node.firstChild) - return { - node: ast.node, - from: ast.from, - to: ast.to, - children: [ - return_value( - template_generator.next({ - ...ast, - node: ast.node.firstChild, - }) - ), - ], - } - } else { - return return_value(template_generator.next(ast)) - } -} - -export const t = /** @type {const} */ ({ - any: any, - /** - * Match no, one, or multiple! Like `*` in regex. - * It stores it's matches as `{ [name]: Array<{ node: SyntaxNode, matches: MatchResult }> }` - * - * If name isn't provided it will not store any of the matches.. useful if you really really don't care about something. - * - * @type {(name?: string, what?: Templatable) => Substitution} - */ - many: memo_first_argument_weakmemo_second((name, of_what = any) => { - return function* many() { - let template_generator = to_template(of_what) - let ast = yield intermediate_value(template_generator.next()) - - // Ugly but it works - let narrowed_node = null - let sub_template = null - if (ast.node.firstChild && ast.node.from === ast.node.firstChild.from && ast.node.to === ast.node.firstChild.to) { - narrowed_node = ast.node - sub_template = return_value( - template_generator.next({ - ...ast, - node: ast.node.firstChild, - }) - ) - } else { - sub_template = return_value(template_generator.next(ast)) - } - - // let sub_template = yield* narrow_template(to_template(of_what)) - - return { - narrowed_node, - sub_template, - pattern: function many(cursor, matches, verbose = false) { - if (!cursor) { - verbose && console.log("✅ Nothing to see here... I'm fine with that - many") - return true - } - - if (narrowed_node) { - if (cursor.name !== narrowed_node.name) { - verbose && console.log("❌ Tried to go in, but she wasn't my type - many") - cursor.prevSibling() - return true - } - cursor.firstChild() - } - - try { - let matches_nodes = [] - while (true) { - // So, big oof here, but I think we shouldn't match error nodes in many - if (cursor.type.isError) { - cursor.prevSibling() - verbose && console.log("✋ I don't do errors - many") - return true // Still we did finish, lets just hope someone else cares about the error - } - - let local_match = {} - let did_match = match_template(cursor, sub_template, local_match, verbose) - if (!did_match) { - // Move back on child, as that is the child that DIDN'T match - // And we want to give the next template a change to maybe match it - cursor.prevSibling() - break - } - matches_nodes.push({ node: cursor.node, match: local_match }) - - if (!cursor.nextSibling()) break - } - - if (name != null) { - matches[name] = matches_nodes - } - return true - } finally { - if (narrowed_node) { - cursor.parent() - } - } - }, - } - } - }), - - /** - * Match either a single node or none. Like `?` in regex. - * @type {(what: Templatable) => Substitution} - */ - maybe: weak_memo1((what) => { - return function* maybe() { - let sub_template = yield* to_template(what) - return { - sub_template, - pattern: function maybe(cursor, matches, verbose = false) { - if (!cursor) return true - if (cursor.type.isError) return true - - let did_match = match_template(cursor, sub_template, matches, verbose) - - if (did_match === false) { - // Roll back position because we didn't match - cursor.prevSibling() - } - return true - }, - } - } - }), - - /** - * This is an escape hatch. - * Ideally I'd want to ask for multiple different templates to match, but I can't easily get that to work yet. - * So for now, you can ask templates to just match "Anything kinda like this", - * and then do further matching on the result manually. - * - * More technically, this says "match anything that will appear in my position in the AST". - * It does not care about the type. Don't use this recklessly! - * - * @type {(what: Templatable) => Substitution} - * */ - anything_that_fits: weak_memo1((what) => { - return function* anything_that_fits() { - // We send the template code upwards, but we fully ignore the output - yield* to_template(what) - return { - pattern: function anything_that_fits(cursor, matches, verbose = false) { - return true - }, - } - } - }), - /** - * This is an escape hatch, like {@linkcode anything_that_fits}, - * but it does also check for node type at least. - * - * @type {(what: Templatable) => Substitution} - * */ - something_with_the_same_type_as: weak_memo1((what) => { - return function* something_with_the_same_type_as() { - let template_generator = to_template(what) - let julia_to_parse = intermediate_value(template_generator.next()) - - let ast = yield julia_to_parse - - // @ts-ignore - template_generator.return() - - return { - pattern: function something_with_the_same_type_as(haystack, matches, verbose = false) { - return haystack != null && ast.name === haystack.name - }, - } - } - }), - - /** - * This "higher-order" template pattern is for adding their nodes to the matches. - * Without a pattern (e.g. `t.as("foo")`) it will default to `t.any` - * - * @type {(name: string, what?: Templatable) => Substitution} - */ - as: memo_first_argument_weakmemo_second((name, what = any) => { - return function* as() { - let sub_template = yield* to_template(what) - return { - sub_template, - pattern: function as(haystack, matches, verbose = false) { - let did_match = match_template(haystack, sub_template, matches, verbose) - if (did_match === true) { - matches[name] = haystack?.["node"] - } - return haystack != null && did_match - }, - } - } - }), - - /** @type {Substitution} */ - Identifier: function* Identifier() { - yield "identifier" - return { - pattern: function Identifier(haystack, matches, verbose = false) { - return haystack != null && narrow_name(haystack) === "Identifier" - }, - } - }, - /** @type {Substitution} */ - Number: function* Number() { - yield "69" - return { - pattern: function Number(haystack, matches, verbose = false) { - return haystack != null && (narrow_name(haystack) === "IntegerLiteral" || narrow_name(haystack) === "FloatLiteral") - }, - } - }, - /** @type {Substitution} */ - String: function* String() { - yield `"A113"` - return { - pattern: function String(haystack, matches, verbose = false) { - return haystack != null && (narrow_name(haystack) === "StringLiteral" || narrow_name(haystack) === "NsStringLiteral") - }, - } - }, -}) - -/** - * Basically exists for {@linkcode create_specific_template_maker} - * - * @type {(template: Templatable, meta_template: Matcher) => Matcher} - */ -export let take_little_piece_of_template = weak_memo((template, meta_template) => { - let template_generator = to_template(template) - let julia_to_parse = intermediate_value(template_generator.next()) - - // Parse the AST from the template, but we don't send it back to the template_generator yet! - let template_ast = julia_to_ast(julia_to_parse) - - let match = null - // Match our created template code to the meta-template, which will yield us the part of the - // AST that falls inside the "content" in the meta-template. - if ((match = meta_template.match(template_ast))) { - let { content } = /** @type {{ content: SyntaxNode }} */ (match) - - let possible_parents = [] - while (content.firstChild && content.firstChild.from == content.from && content.firstChild.to == content.to) { - possible_parents.push(content.type) - content = content.firstChild - } - - if (content == null) { - console.log(`match:`, match) - throw new Error("No content match?") - } - - // Now we send just the `content` back to the template generator, which will happily accept it... - // (We do send the original from:to though, as these are the from:to's that are also in the template AST still) - let template_description = return_value( - template_generator.next({ - name: content.name, - node: content, - // Need to provide the original from:to range - from: template_ast.from, - to: template_ast.to, - }) - ) - - // And for some reason this works? - // Still feels like it shouldn't... it feels like I conjured some dark magic and I will be swiming in tartarus soon... - - return /** @type {Matcher} */ ({ - possible_parents, - template_description, - /** - * @param {TreeCursor | SyntaxNode} haystack_cursor - * @param {boolean} verbose? - * */ - match(haystack_cursor, verbose = false) { - if (haystack_cursor.type.isError) { - verbose && console.log(`❌ Short circuiting because haystack(${haystack_cursor.name}) is an error`) - return false - } - if ("cursor" in haystack_cursor) haystack_cursor = haystack_cursor.cursor() - - // Should possible parents be all-or-nothing? - // So either it matches all the possible parents, or it matches none? - let depth = 0 - for (let possible_parent of possible_parents) { - if (haystack_cursor.type === possible_parent) { - let parent_from = haystack_cursor.from - let parent_to = haystack_cursor.to - // Going in - if (haystack_cursor.firstChild()) { - if (haystack_cursor.from === parent_from && haystack_cursor.to === parent_to) { - verbose && console.log(`✅ Matched parent, going one level deeper (${possible_parent})`) - depth++ - } else { - haystack_cursor.parent() - verbose && - console.log( - `❌ Was matching possible parent (${possible_parent}), but it wasn't filling?! That's weird.... ${haystack_cursor.toString()}` - ) - for (let i = 0; i < depth; i++) { - haystack_cursor.parent() - } - return false - } - } - } else { - break - } - } - - // prettier-ignore - verbose && console.groupCollapsed(`Starting a specific at match haystack(${haystack_cursor.name}) vs. template(${"node" in template_description ? template_description.name : template_description.pattern.name})`) - - try { - let matches = {} - return match_template(haystack_cursor, template_description, matches, verbose) ? matches : null - } finally { - // ARE FOR LOOPS REALLY THE BEST I CAN DO HERE?? - for (let i = 0; i < depth; i++) { - haystack_cursor.parent() - } - - verbose && console.groupEnd() - } - }, - }) - } else { - console.log(`meta_template:`, meta_template) - console.log(`template:`, template) - throw new Error("Template passed into `take_little_piece_of_template` doesn't match meta_template") - } -}) - -/** - * Sometimes nodes are nested at the exact same position: - * `struct X end`, here `X` could be both a `Definition(Identifier)` or just the `Identifier`. - * This function will get you the deepest node, so in the above example, it would be `Identifier`. - * If the node has multiple children, or the child is offset, it will return the original node. - * - * @param {SyntaxNode} node - * @returns {SyntaxNode} - **/ -export let narrow = (node) => { - if (node.firstChild && node.firstChild.from === node.from && node.firstChild.to === node.to) { - return narrow(node.firstChild) - } else { - return node - } -} - -/** - * Effecient, cursor-based, version of `narrow(node)`, - * for if all you care about is the name. - * - * Which will be most of the time.. - * - * @param {TreeCursor} cursor - * @return {string} - */ -export let narrow_name = (cursor) => { - let from = cursor.from - let to = cursor.to - if (cursor.firstChild()) { - try { - if (cursor.from === from && cursor.to === to) { - return narrow_name(cursor) - } - } finally { - cursor.parent() - } - } - return cursor.name -} - -/** - * This allows for selecting the unselectable! - * By default templates need to match the topnode of their AST, but sometimes we want to match something on a special position. - * - * ```create_specific_template_maker(x => jl_dynamic`import X: ${x}`)``` will match specifiers that could occur specifically on the `${x}` position. - * - * NOTE: Inside `create_specific_template_maker` you'll have to use `jl_dynamic` not going to explain why. - * - * @param {(subtemplate: Templatable) => Templatable} fn - */ -export let create_specific_template_maker = (fn) => { - return (argument) => { - let meta_template = template(fn(t.as("content", argument))) - return take_little_piece_of_template(fn(argument), meta_template) - } -} - /** * Like Lezers `iterate`, but instead of `{ from, to, getNode() }` * this will give `enter()` and `leave()` the `cursor` (which can be effeciently matches with lezer template) @@ -1057,41 +27,3 @@ export function iterate_with_cursor({ tree, enter, leave, from = 0, to = tree.le } } } - -/////////////////////////////////// -// FULL ON UTILITY FUNCTIONS -/////////////////////////////////// - -/** - * Return `iterater_result.value` if `iterater_result.done` is `false`, otherwise throw an error. - * - * Not sure why typescript doesn't infer the `Generator` when I ask !iterater_result.done... - * Maybe it will bite me later 🤷‍♀️ - * - * @template T - * @param {IteratorResult} iterater_result - * @returns {T} iterater_result - */ -let intermediate_value = (iterater_result) => { - if (iterater_result.done) { - throw new Error("Expected `yield`-d value, but got `return`") - } else { - return /** @type {any} */ (iterater_result.value) - } -} - -/** - * Not sure why typescript doesn't infer the `Generator<_, T>` when I ask !iterater_result.done... - * Maybe it will bite me later 🤷‍♀️ - * - * @template T - * @param {IteratorResult} iterater_result - * @returns {T} iterater_result - */ -let return_value = (iterater_result) => { - if (iterater_result.done) { - return /** @type {any} */ (iterater_result.value) - } else { - throw new Error("Expected `yield`-d value, but got `return`") - } -} diff --git a/frontend/components/CellInput/pkg_bubble_plugin.js b/frontend/components/CellInput/pkg_bubble_plugin.js index f38a5e9dee..63d48087ee 100644 --- a/frontend/components/CellInput/pkg_bubble_plugin.js +++ b/frontend/components/CellInput/pkg_bubble_plugin.js @@ -1,9 +1,9 @@ import _ from "../../imports/lodash.js" -import { EditorView, syntaxTree, Decoration, ViewUpdate, ViewPlugin, Facet } from "../../imports/CodemirrorPlutoSetup.js" +import { EditorView, syntaxTree, Decoration, ViewUpdate, ViewPlugin, Facet, EditorState } from "../../imports/CodemirrorPlutoSetup.js" import { PkgStatusMark, PkgActivateMark } from "../PkgStatusMark.js" import { html } from "../../imports/Preact.js" import { ReactWidget } from "./ReactWidget.js" -import { create_specific_template_maker, iterate_with_cursor, jl, jl_dynamic, narrow, t, template } from "./lezer_template.js" +import { iterate_with_cursor } from "./lezer_template.js" /** * @typedef PkgstatusmarkWidgetProps @@ -23,145 +23,78 @@ export const pkg_disablers = [ "@quickactivate", ] -function find_import_statements({ doc, tree, from, to }) { - // This quotelevel stuff is waaaay overengineered and precise... - // but I love making stuff like this SO LET ME OKAY - let quotelevel = 0 +/** + * @param {object} a + * @param {EditorState} a.state + * @param {Number} a.from + * @param {Number} a.to + */ +function find_import_statements({ state, from, to }) { + const doc = state.doc + const tree = syntaxTree(state) + let things_to_return = [] + let currently_using_or_import = "import" + let currently_selected_import = false + iterate_with_cursor({ tree, from, to, - enter: (cursor) => { - // `quote ... end` or `:(...)` - if (cursor.name === "QuoteExpression" || cursor.name === "QuoteStatement") { - quotelevel++ - } - // `$(...)` when inside quote - if (cursor.name === "InterpExpression") { - quotelevel-- - } - if (quotelevel !== 0) return + enter: (node) => { + let go_to_parent_afterwards = null - // Check for Pkg.activate() and friends - if (cursor.name === "CallExpression" || cursor.name === "MacroExpression") { - let node = cursor.node - let callee = node.firstChild - let callee_name = doc.sliceString(callee.from, callee.to) + if (node.name === "QuoteExpression" || node.name === "FunctionDefinition") return false - if (pkg_disablers.includes(callee_name)) { - things_to_return.push({ - type: "package_disabler", - name: callee_name, - from: cursor.to, - to: cursor.to, - }) - } + if (node.name === "import") currently_using_or_import = "import" + if (node.name === "using") currently_using_or_import = "using" - return - } + // console.group("exploring", node.name, doc.sliceString(node.from, node.to), node) - let import_specifier_template = create_specific_template_maker((x) => jl_dynamic`import A, ${x}`) - // Because the templates can't really do recursive stuff, we need JavaScript™️! - let unwrap_scoped_import = (specifier) => { - let match = null - if ((match = import_specifier_template(jl`${t.as("package")}.${t.any}`).match(specifier))) { - return unwrap_scoped_import(match.package) - } else if ((match = import_specifier_template(jl`.${t.maybe(t.any)}`).match(specifier))) { - // Still trash! - return null - } else if ((match = import_specifier_template(jl`${t.Identifier}`).match(specifier))) { - return specifier - } else { - console.warn("Unknown nested import specifier: " + specifier.toString()) + if (node.name === "CallExpression" || node.name === "MacrocallExpression") { + let callee = node.node.firstChild + if (callee) { + let callee_name = doc.sliceString(callee.from, callee.to) + + if (pkg_disablers.includes(callee_name)) { + things_to_return.push({ + type: "package_disabler", + name: callee_name, + from: node.from, + to: node.to, + }) + } } + return false } - let match = null - if ( - // These templates might look funky... and they are! - // But they are necessary to force the matching to match as specific as possible. - // With just `import ${t.many("specifiers")}` it will match `import A, B, C`, but - // it will do so by giving back [`A, B, C`] as one big specifier! - (match = template(jl`import ${t.as("specifier")}: ${t.many()}`).match(cursor)) ?? - (match = template(jl`import ${t.as("specifier")}, ${t.many("specifiers")}`).match(cursor)) ?? - (match = template(jl`using ${t.as("specifier")}: ${t.many()}`).match(cursor)) ?? - (match = template(jl`using ${t.as("specifier")}, ${t.many("specifiers")}`).match(cursor)) - ) { - let { specifier, specifiers = [] } = match + if (node.name === "ImportStatement") { + currently_selected_import = false + } + if (node.name === "SelectedImport") { + currently_selected_import = true + node.firstChild() + go_to_parent_afterwards = true + } - if (specifier) { - specifiers = [{ node: specifier }, ...specifiers] + if (node.name === "ImportPath") { + const item = { + type: "package", + name: doc.sliceString(node.from, node.to), + from: node.from, + to: node.to, } - for (let { node: specifier } of specifiers) { - specifier = narrow(specifier) - - let match = null - if ((match = import_specifier_template(jl`${t.as("package")} as ${t.maybe(t.any)}`).match(specifier))) { - let node = unwrap_scoped_import(match.package) - if (node) { - things_to_return.push({ - type: "package", - name: doc.sliceString(node.from, node.to), - from: node.to, - to: node.to, - }) - } - } else if ((match = import_specifier_template(jl`${t.as("package")}.${t.any}`).match(specifier))) { - let node = unwrap_scoped_import(match.package) - if (node) { - things_to_return.push({ - type: "package", - name: doc.sliceString(node.from, node.to), - from: node.to, - to: node.to, - }) - } - } else if ((match = import_specifier_template(jl`.${t.as("scoped")}`).match(specifier))) { - // Trash! - } else if ((match = import_specifier_template(jl`${t.as("package")}`).match(specifier))) { - let node = unwrap_scoped_import(match.package) - if (node) { - things_to_return.push({ - type: "package", - name: doc.sliceString(node.from, node.to), - from: node.to, - to: node.to, - }) - } - } else { - console.warn("Unknown import specifier: " + specifier.toString()) - } - } + things_to_return.push(item) - match = null - if ((match = template(jl`using ${t.as("specifier")}, ${t.many("specifiers")}`).match(cursor))) { - let { specifier } = match - if (specifier) { - if (doc.sliceString(specifier.to, specifier.to + 1) === "\n" || doc.sliceString(specifier.to, specifier.to + 1) === "") { - things_to_return.push({ - type: "implicit_using", - name: doc.sliceString(specifier.from, specifier.to), - from: specifier.to, - to: specifier.to, - }) - } - } - } + // This is just for show... might delete it later + if (currently_using_or_import === "using" && !currently_selected_import) things_to_return.push({ ...item, type: "implicit_using" }) + } + if (go_to_parent_afterwards) { + node.parent() return false - } else if (cursor.name === "ImportStatement") { - throw new Error("What") - } - }, - leave: (cursor) => { - if (cursor.name === "QuoteExpression" || cursor.name === "QuoteStatement") { - quotelevel-- - } - if (cursor.name === "InterpExpression") { - quotelevel++ } }, }) @@ -179,8 +112,7 @@ function pkg_decorations(view, { pluto_actions, notebook_id, nbpkg }) { let widgets = view.visibleRanges .flatMap(({ from, to }) => { let things_to_mark = find_import_statements({ - doc: view.state.doc, - tree: syntaxTree(view.state), + state: view.state, from: from, to: to, }) @@ -240,7 +172,7 @@ function pkg_decorations(view, { pluto_actions, notebook_id, nbpkg }) { } /** - * @type {Facet} + * @type {Facet} */ export const NotebookpackagesFacet = Facet.define({ combine: (values) => values[0], @@ -277,6 +209,7 @@ export const pkgBubblePlugin = ({ pluto_actions, notebook_id_ref }) => } }, { + // @ts-ignore decorations: (v) => v.decorations, } ) diff --git a/frontend/components/CellInput/pluto_autocomplete.js b/frontend/components/CellInput/pluto_autocomplete.js index f44239d00e..12718ff4c9 100644 --- a/frontend/components/CellInput/pluto_autocomplete.js +++ b/frontend/components/CellInput/pluto_autocomplete.js @@ -312,7 +312,9 @@ const writing_variable_name_or_keyword = (/** @type {autocomplete.CompletionCont let inside_do_argument_expression = ctx.matchBefore(/do [\(\), \p{L}\p{Nl}\p{Sc}\d_!]*$/u) let node = syntaxTree(ctx.state).resolve(ctx.pos, -1) + // TODO: BareTupleExpression let node2 = node?.parent?.name === "BareTupleExpression" ? node?.parent : node + // TODO: AssignmentExpression let inside_assigment_lhs = node?.name === "Identifier" && node2?.parent?.name === "AssignmentExpression" && node2?.nextSibling != null return just_finished_a_keyword || after_keyword || inside_do_argument_expression || inside_assigment_lhs diff --git a/frontend/components/CellInput/scopestate_statefield.js b/frontend/components/CellInput/scopestate_statefield.js index 027d9035b7..19572bc53e 100644 --- a/frontend/components/CellInput/scopestate_statefield.js +++ b/frontend/components/CellInput/scopestate_statefield.js @@ -1,6 +1,7 @@ -import { syntaxTree, StateField } from "../../imports/CodemirrorPlutoSetup.js" +import { syntaxTree, StateField, NodeWeakMap, Text } from "../../imports/CodemirrorPlutoSetup.js" import _ from "../../imports/lodash.js" -import { child_cursors, child_nodes, create_specific_template_maker, jl, jl_dynamic, narrow, t, template } from "./lezer_template.js" + +const VERBOSE = false /** * @typedef TreeCursor @@ -29,1051 +30,331 @@ import { child_cursors, child_nodes, create_specific_template_maker, jl, jl_dyna * @property {Array<{ definition: Range, validity: Range, name: string }>} locals */ -/** - * @param {ScopeState} a - * @param {ScopeState} b - * @returns {ScopeState} - */ -let merge_scope_state = (a, b) => { - if (a === b) return a +const r = (cursor) => ({ from: cursor.from, to: cursor.to }) - let usages = [...a.usages, ...b.usages] - let definitions = new Map(a.definitions) - for (let [key, value] of b.definitions) { - definitions.set(key, value) +const find_local_definition = (locals, name, cursor) => { + for (let lo of locals) { + if (lo.name === name && cursor.from >= lo.validity.from && cursor.to <= lo.validity.to) { + return lo + } } - let locals = [...a.locals, ...b.locals] - return { usages, definitions, locals } } -/** @param {TreeCursor} cursor */ -let search_for_interpolations = function* (cursor) { - for (let child of child_cursors(cursor)) { - if (child.name === "InterpExpression") { - yield cursor - } else if (child.name === "QuoteExpression" || child.name === "QuoteStatement") { - for (let child_child of search_for_interpolations(child)) { - yield* search_for_interpolations(child_child) - } - } else { - yield* search_for_interpolations(child) +const HardScopeNames = new Set([ + "WhileStatement", + "ForStatement", + "TryStatement", + "LetStatement", + "FunctionDefinition", + "MacroDefinition", + "DoClause", + "Generator", +]) + +const does_this_create_scope = (/** @type {TreeCursor} */ cursor) => { + if (HardScopeNames.has(cursor.name)) return true + + if (cursor.name === "Assignment") { + const reset = cursor.firstChild() + try { + // f(x) = x + // @ts-ignore + if (cursor.name === "CallExpression") return true + } finally { + if (reset) cursor.parent() } } -} -/** @param {TreeCursor} cursor */ -let go_through_quoted_expression_looking_for_interpolations = function* (cursor) { - if (cursor.name !== "QuoteExpression" && cursor.name !== "QuoteStatement") throw new Error("Expected QuotedExpression or QuoteStatement") - yield* search_for_interpolations(cursor) + + return false } /** - * So this was a late addition, and it creates a bit crazy syntax... - * but I love it for that syntax! It really makes the patterns pop out, - * which it really needs, because the patterns are the most important part of this code.. - * @param {(subsitution: import("./lezer_template.js").Templatable) => import("./lezer_template.js").Matcher} template_fn + * Look into the left-hand side of an Assigment expression and find all ranges where variables are defined. + * E.g. `a, (b,c) = something` will return ranges for a, b, c. + * @param {TreeCursor} root_cursor + * @returns {Range[]} */ -let make_beautiful_matcher = (template_fn) => { - return function match(cursor, verbose = false) { - if (cursor == null) { - /** @type {(...args: Parameters) => any} */ - return (x, ...args) => { - return template_fn(jl(x, ...args)) - } +const explore_assignment_lhs = (root_cursor) => { + const a = cursor_not_moved_checker(root_cursor) + let found = [] + root_cursor.iterate((cursor) => { + if (cursor.name === "Identifier" || cursor.name === "MacroIdentifier" || cursor.name === "Operator") { + found.push(r(cursor)) } - - /** @type {(...args: Parameters) => any} */ - return function jl_and_match(x, ...args) { - return template_fn(jl(x, ...args)).match(cursor, verbose) + if (cursor.name === "IndexExpression" || cursor.name === "FieldExpression") { + // not defining a variable but modifying an object + return false } - } + }) + a() + return found } /** - * @param {Parameters[0]} template_creator + * Remember the position where this is called, and return a function that will move into parents until we are are back at that position. + * + * You can use this before exploring children of a cursor, and then go back when you are done. */ -let make_beautiful_specific_matcher = (template_creator) => { - let template_fn = create_specific_template_maker(template_creator) - return function match(cursor, verbose = false) { - if (cursor == null) { - /** @type {(...args: Parameters) => any} */ - return (x, ...args) => { - return template_fn(jl(x, ...args)) - } - } - - /** @type {(...args: Parameters) => any} */ - return function jl_and_match(x, ...args) { - return template_fn(jl(x, ...args)).match(cursor, verbose) +const back_to_parent_resetter = (/** @type {TreeCursor} */ cursor) => { + const map = new NodeWeakMap() + map.cursorSet(cursor, "here") + return () => { + while (map.cursorGet(cursor) !== "here") { + if (!cursor.parent()) throw new Error("Could not find my back to the original parent!") } } } -let match_for_binding = make_beautiful_specific_matcher((x) => jl_dynamic`[i for i in i ${x}]`) -let match_assignee = make_beautiful_specific_matcher((x) => jl_dynamic`${x} = nothing`) -let match_function_definition_argument = make_beautiful_specific_matcher((x) => jl_dynamic`function f(${x}) end`) -let match_function_call_argument = make_beautiful_specific_matcher((x) => jl_dynamic`f(${x})`) -let match_function_call_named_argument = make_beautiful_specific_matcher((x) => jl_dynamic`f(; ${x})`) +const cursor_not_moved_checker = (cursor) => { + const map = new NodeWeakMap() + map.cursorSet(cursor, "yay") -/** - * @param {TreeCursor | SyntaxNode} cursor - * @param {any} doc - * @param {ScopeState} scopestate - * @param {boolean} [verbose] - * @returns {ScopeState} - */ -let explorer_function_definition_argument = (cursor, doc, scopestate, verbose = false) => { - let match = null + const debug = (cursor) => `${cursor.name}(${cursor.from},${cursor.to})` - if ((match = match_function_call_argument(cursor)`; ${t.many("named_args")}`)) { - // "Parameters", the `y, z` in `function f(x; y, z) end` - let { named_args = [] } = match - for (let { node: named_arg } of named_args) { - scopestate = explorer_function_definition_argument(named_arg, doc, scopestate, verbose) - } - return scopestate - } else if ((match = match_function_definition_argument(cursor)`${t.Identifier}`)) { - return scopestate_add_definition(scopestate, doc, cursor) - } else if ((match = match_function_definition_argument(cursor)`${t.as("subject")}...`)) { - // `function f(x...)` => ["x"] - return explore_pattern(match.subject, doc, scopestate, null, verbose) - } else if ((match = match_function_definition_argument(cursor)`${t.as("name")} = ${t.as("value")}`)) { - // `function f(x = 10)` => ["x"] - let { name, value } = match - scopestate = explore_pattern(name, doc, scopestate, value.to, verbose) - scopestate = explore_variable_usage(value.cursor(), doc, scopestate, verbose) - return scopestate - } else if ( - (match = match_function_definition_argument(cursor)`${t.as("name")}::${t.as("type")}`) ?? - (match = match_function_definition_argument(cursor)`${t.as("name")}:`) ?? - // (match = match_function_definition_argument(cursor)`${t.as("name")}::`) ?? - (match = match_function_definition_argument(cursor)`::${t.as("type")}`) - ) { - let { name, type } = match - if (name) scopestate = explore_pattern(name, doc, scopestate, type.to, verbose) - if (type) scopestate = explore_variable_usage(type.cursor(), doc, scopestate, verbose) - return scopestate - } else { - // Fall back to "just explore pattern"... - // There is more overlap between function arguments and patterns than I use now, I think - scopestate = explore_pattern(cursor, doc, scopestate) + const debug_before = debug(cursor) - verbose && console.warn("UNKNOWN FUNCTION DEFINITION ARGUMENT:", cursor.toString()) - return scopestate + return () => { + if (map.cursorGet(cursor) !== "yay") { + throw new Error(`Cursor changed position when forbidden! Before: ${debug_before}, after: ${debug(cursor)}`) + } } } -/** - * @param {TreeCursor | SyntaxNode} node - * @param {any} doc - * @param {ScopeState} scopestate - * @param {number?} valid_from - * @param {boolean} [verbose] - * @returns {ScopeState} - */ -let explore_pattern = (node, doc, scopestate, valid_from = null, verbose = false) => { - let match = null - - verbose && console.group("Explorering pattern:", node.toString()) - try { - if ((match = match_assignee(node)`${t.Identifier}`)) { - verbose && console.log("It's an identifier, adding it to the scope") - return scopestate_add_definition(scopestate, doc, node, valid_from) - } else if ((match = match_assignee(node)`${t.as("object")}::${t.as("type")}`)) { - let { object, type } = match - scopestate = explore_variable_usage(type.cursor(), doc, scopestate, verbose) - scopestate = scopestate_add_definition(scopestate, doc, object) - return scopestate - } else if ((match = match_assignee(node)`${t.as("subject")}...`)) { - // `x... = [1,2,3]` => ["x"] - return explore_pattern(match.subject, doc, scopestate, valid_from, verbose) - } else if ((match = match_function_definition_argument(node)`${t.as("name")} = ${t.as("value")}`)) { - let { name, value } = match - scopestate = explore_pattern(name, doc, scopestate, value.from, verbose) - scopestate = explore_variable_usage(value.cursor(), doc, scopestate, verbose) - return scopestate - } else if ((match = match_assignee(node)`(; ${t.many("named_tuples")})`)) { - // `(; x, y) = z` => ["x", "y"] - let { named_tuples } = match - for (let name of named_tuples) { - scopestate = explore_pattern(name.node.cursor(), doc, scopestate, valid_from, verbose) - } - return scopestate - } else if ( - (match = match_assignee(node)`${t.as("first")}, ${t.many("rest")}`) ?? - (match = match_assignee(node)`(${t.as("first")}, ${t.many("rest")})`) - ) { - // console.warn("Tuple assignment... but the bad one") - for (let { node: name } of [{ node: match.first }, ...(match.rest ?? [])]) { - scopestate = explore_pattern(name.cursor(), doc, scopestate, valid_from, verbose) - } - return scopestate - } else if ((match = match_julia(node)`${t.as("prefix")}${t.as("string", t.String)}`)) { - // This one is also a bit enigmatic, but `t.String` renders in the template as `"..."`, - // so the template with match things that look like `prefix"..."` - let { prefix, string } = match - let prefix_string = doc.sliceString(prefix.from, prefix.to) - - if (prefix_string === "var") { - let name = doc.sliceString(string.from + 1, string.to - 1) - if (name.length !== 0) { - scopestate.definitions.set(name, { - from: node.from, - to: node.to, - valid_from: node.to, - }) - } - } else { - scopestate = explore_variable_usage("cursor" in node ? node.cursor() : node, doc, scopestate, verbose) - } - return scopestate - } else if ((match = match_assignee(node)`${t.as("object")}[${t.as("property")}]`)) { - let { object, property } = match - scopestate = explore_variable_usage(object.cursor(), doc, scopestate, verbose) - if (property) scopestate = explore_variable_usage(property.cursor(), doc, scopestate, verbose) - return scopestate - } else if ((match = match_assignee(node)`${t.as("object")}.${t.as("property")}`)) { - let { object, property } = match - scopestate = explore_variable_usage(object.cursor(), doc, scopestate, verbose) - return scopestate - } else { - verbose && console.warn("UNKNOWN PATTERN:", node.toString(), doc.sliceString(node.from, node.to)) - return scopestate +const i_am_nth_child = (cursor) => { + const map = new NodeWeakMap() + map.cursorSet(cursor, "here") + if (!cursor.parent()) throw new Error("Cannot be toplevel") + cursor.firstChild() + let i = 0 + while (map.cursorGet(cursor) !== "here") { + i++ + if (!cursor.nextSibling()) { + throw new Error("Could not find my way back") } - } finally { - verbose && console.groupEnd() } + return i } /** - * Explores the definition part of a struct or such. - * Takes care of defining that actual name, defining type parameters, - * and using all the types used. - * - * It returns an inner and an outer scope state. - * The inner scope state is for code "inside" the struct, which has access to the implicitly created types. - * Outer scope is only the actual defined name, so will always exist of just one definition and no usages. - * This distinction is so the created types don't escape outside the struct. - * Usages all go in the inner scope. - * * @param {TreeCursor} cursor - * @param {any} doc - * @param {ScopeState} scopestate - * @param {boolean} [verbose] - * @returns {{ inner: ScopeState, outer: ScopeState }} + * @returns {Range[]} */ -let explore_definition = function (cursor, doc, scopestate, verbose = false) { - let match = null +const explore_funcdef_arguments = (cursor, { enter, leave }) => { + let found = [] - if (cursor.name === "Definition" && cursor.firstChild()) { - try { - return explore_definition(cursor, doc, scopestate) - } finally { - cursor.parent() - } - } else if (cursor.name === "Identifier") { - return { - inner: scopestate_add_definition(scopestate, doc, cursor), - outer: scopestate_add_definition(fresh_scope(), doc, cursor), - } - } else if ((match = match_julia(cursor)`${t.as("subject")}{ ${t.many("parameters")} }`)) { - // A{B} - let { subject, parameters } = match - let outer = fresh_scope() - if (subject) { - let subject_explored = explore_definition(subject.cursor(), doc, scopestate) - outer = subject_explored.outer - scopestate = subject_explored.inner - } - for (let { node: parameter } of parameters) { - // Yes, when there is a type parameter in the definition itself (so not after `::`), - // it implies a new type parameter being implicitly instanciated. - let { inner: parameter_inner } = explore_definition(parameter.cursor(), doc, scopestate) - scopestate = parameter_inner + const position_validation = cursor_not_moved_checker(cursor) + const position_resetter = back_to_parent_resetter(cursor) + + if (!cursor.firstChild()) throw new Error(`Expected to go into function definition argument expression, stuck at ${cursor.name}`) + // should be in the TupleExpression now + + // @ts-ignore + VERBOSE && console.assert(cursor.name === "TupleExpression" || cursor, name === "Arguments", cursor.name) + + cursor.firstChild() + do { + if (cursor.name === "KeywordArguments") { + cursor.firstChild() // go into kwarg arguments } - return { inner: scopestate, outer: outer } - } else if ((match = match_julia(cursor)`${t.as("subject")} <: ${t.maybe(t.as("type"))}`)) { - let { subject, type } = match - let outer = fresh_scope() - if (subject) ({ outer, inner: scopestate } = explore_definition(subject.cursor(), doc, scopestate)) - if (type) scopestate = explore_variable_usage(type.cursor(), doc, scopestate) - return { inner: scopestate, outer: outer } - } else { - verbose && console.warn(`Unknown thing in definition: "${doc.sliceString(cursor.from, cursor.to)}", "${cursor.toString()}"`) - return { inner: scopestate, outer: fresh_scope() } - } -} -let match_julia = make_beautiful_matcher(template) + if (cursor.name === "Identifier" || cursor.name === "Operator") { + found.push(r(cursor)) + } else if (cursor.name === "KwArg") { + let went_in = cursor.firstChild() + found.push(r(cursor)) + // cursor.nextSibling() + // find stuff used here + // cursor.iterate(enter, leave) -/** - * @param {TreeCursor} cursor - * @param {any} doc - * @param {ScopeState} scopestate - * @param {boolean} [verbose] - * @returns {ScopeState} - */ -let explore_macro_identifier = (cursor, doc, scopestate, verbose = false) => { - let match = null + if (went_in) cursor.parent() + } + } while (cursor.nextSibling()) - let match_macro_identifier = make_beautiful_specific_matcher((x) => jl_dynamic`${x} x y z`) + position_resetter() + position_validation() - if ((match = match_macro_identifier(cursor)`${t.as("macro", jl`@${t.any}`)}`)) { - let { macro } = match - let name = doc.sliceString(macro.from, macro.to) - scopestate.usages.push({ - usage: macro, - definition: scopestate.definitions.get(name) ?? null, - name: name, - }) - return scopestate - } else if ((match = match_macro_identifier(cursor)`${t.as("object")}.@${t.as("macro")}`)) { - let { object } = match - let name = doc.sliceString(object.from, object.to) - scopestate.usages.push({ - usage: object, - definition: scopestate.definitions.get(name) ?? null, - name: name, - }) - return scopestate - } else if ((match = match_macro_identifier(cursor)`@${t.as("object")}.${t.as("macro")}`)) { - let { object } = match - let name = doc.sliceString(object.from, object.to) - scopestate.usages.push({ - usage: object, - definition: scopestate.definitions.get(name) ?? null, - name: name, - }) - return scopestate - } else { - verbose && console.warn("Mwep mweeeep", cursor.toString()) - return scopestate - } + VERBOSE && console.log({ found }) + return found } /** + * @param {TreeCursor | SyntaxNode} tree + * @param {Text} doc + * @param {any} _scopestate + * @param {boolean} [verbose] * @returns {ScopeState} */ -let fresh_scope = () => { - return { - usages: [], - definitions: new Map(), - locals: [], +export let explore_variable_usage = (tree, doc, _scopestate, verbose = VERBOSE) => { + if ("cursor" in tree) { + console.trace("`explore_variable_usage()` called with a SyntaxNode, not a TreeCursor") + tree = tree.cursor() } -} -/** - * Currently this clones a scope state, except for the usages. - * The reason is skips the usages is a premature optimisation. - * We don't need them in the inner scope, but we just as well could leave them on - * (as they won't do any harm either way) - * - * @param {ScopeState} scopestate - * @returns {ScopeState} - */ -let lower_scope = (scopestate) => { - return { + const scopestate = { usages: [], - definitions: new Map(scopestate.definitions), + definitions: new Map(), locals: [], } -} -/** - * For use in combination with `lower_scope`. - * This will take an inner scope and merge only the usages into the outer scope. - * So we see the usages of the inner scope, but the definitions don't exist in the outer scope. - * - * @param {ScopeState} nested_scope - * @param {ScopeState} scopestate - * @param {number} [nested_scope_validity] - * @returns {ScopeState} - */ -let raise_scope = (nested_scope, scopestate, nested_scope_validity = undefined) => { - return { - usages: [...scopestate.usages, ...nested_scope.usages], - definitions: scopestate.definitions, - locals: [ - // TODO: Disabled because of performance problems, see https://github.com/fonsp/Pluto.jl/pull/1925 - // ...(nested_scope_validity === null - // ? [] - // : Array.from(nested_scope.definitions) - // .filter(([name, _]) => !scopestate.definitions.has(name)) - // .map(([name, definition]) => ({ - // name, - // definition, - // validity: { - // from: definition.valid_from, - // to: nested_scope_validity, - // }, - // }))), - // ...nested_scope.locals, - // ...scopestate.locals, - ], - } -} + let local_scope_stack = /** @type {Range[]} */ ([]) -/** - * @param {ScopeState} scopestate - * @param {any} doc - * @param {SyntaxNode | TreeCursor} node - * @param {number?} valid_from - */ -let scopestate_add_definition = (scopestate, doc, node, valid_from = null) => { - valid_from = valid_from === null ? node.to : valid_from - scopestate.definitions.set(doc.sliceString(node.from, node.to), { - from: node.from, - to: node.to, - valid_from, - }) - return scopestate -} + const definitions = /** @type {Map} */ new Map() + const locals = /** @type {Array<{ definition: Range, validity: Range, name: string }>} */ ([]) + const usages = /** @type {Array<{ usage: Range, definition: Range | null, name: string }>} */ ([]) -/** - * @param {TreeCursor | SyntaxNode} cursor - * @param {any} doc - * @param {ScopeState} scopestate - * @param {boolean} [verbose] - * @returns {ScopeState} - */ -export let explore_variable_usage = ( - cursor, - doc, - scopestate = { - usages: [], - definitions: new Map(), - locals: [], - }, - verbose = false -) => { - if ("cursor" in cursor) { - // console.trace("`explore_variable_usage()` called with a SyntaxNode, not a TreeCursor") - cursor = cursor.cursor() - } + const return_false_immediately = new NodeWeakMap() - let start_node = null - if (verbose) { - console.group(`Explorer: ${cursor.name}`) + let enter, leave - console.groupCollapsed("Details") - try { - console.log(`Full tree: ${cursor.toString()}`) - console.log("Full text:", doc.sliceString(cursor.from, cursor.to)) - console.log(`scopestate:`, scopestate) - } finally { - console.groupEnd() + enter = (/** @type {TreeCursor} */ cursor) => { + if (verbose) { + console.group(`Explorer: ${cursor.name}`) + + console.groupCollapsed("Details") + try { + console.log(`Full tree: ${cursor.toString()}`) + console.log("Full text:", doc.sliceString(cursor.from, cursor.to)) + console.log(`scopestate:`, scopestate) + } finally { + console.groupEnd() + } } - start_node = cursor.node - } - try { - let match = null - // Doing these checks in front seems to speed things up a bit. if ( - cursor.type.is("keyword") || - cursor.name === "Program" || - cursor.name === "BoolLiteral" || - cursor.name === "CharLiteral" || - cursor.name === "String" || - cursor.name === "IntegerLiteral" || - cursor.name === "FloatLiteral" || - cursor.name === "Comment" || - cursor.name === "BinaryExpression" || - cursor.name === "Operator" || - cursor.name === "MacroArgumentList" || - cursor.name === "ReturnStatement" || - cursor.name === "OpenTuple" || - cursor.name === "ParenExpression" || - cursor.name === "Type" || - cursor.name === "InterpExpression" || - cursor.name === "SpreadExpression" || - cursor.name === "CompoundExpression" || - cursor.name === "ParameterizedIdentifier" || - cursor.name === "BraceExpression" || - cursor.name === "TernaryExpression" || - cursor.name === "Coefficient" || - cursor.name === "TripleString" || - cursor.name === "RangeExpression" || - cursor.name === "IndexExpression" || - cursor.name === "UnaryExpression" || - cursor.name === "ConstStatement" || - cursor.name === "GlobalStatement" || - cursor.name === "ContinueStatement" || - cursor.name === "MatrixExpression" || - cursor.name === "MatrixRow" || - cursor.name === "VectorExpression" + return_false_immediately.cursorGet(cursor) || + cursor.name === "ModuleDefinition" || + cursor.name === "QuoteStatement" || + cursor.name === "QuoteExpression" || + cursor.name === "MacroIdentifier" || + cursor.name === "ImportStatement" ) { - for (let subcursor of child_cursors(cursor)) { - scopestate = explore_variable_usage(subcursor, doc, scopestate, verbose) - } - return scopestate + if (verbose) console.groupEnd() + return false + } + + const register_variable = (range) => { + const name = doc.sliceString(range.from, range.to) + + if (local_scope_stack.length === 0) + definitions.set(name, { + ...range, + valid_from: range.from, + }) + else locals.push({ name, validity: _.last(local_scope_stack), definition: range }) + } + + if (does_this_create_scope(cursor)) { + local_scope_stack.push(r(cursor)) } - if (cursor.name === "Identifier" || cursor.name === "MacroIdentifier") { - let name = doc.sliceString(cursor.from, cursor.to) - scopestate.usages.push({ + if (cursor.name === "Identifier" || cursor.name === "MacroIdentifier" || cursor.name === "Operator") { + const name = doc.sliceString(cursor.from, cursor.to) + usages.push({ name: name, usage: { from: cursor.from, to: cursor.to, }, - definition: scopestate.definitions.get(name) ?? null, - }) - return scopestate - } else if ((match = match_julia(cursor)`:${t.any}`)) { - // Nothing, ha! - return scopestate - } else if ((match = match_julia(cursor)`${t.Number}`)) { - // Nothing, ha! - return scopestate - } else if ((match = match_julia(cursor)`${t.String}`)) { - // Nothing, ha! - return scopestate - } else if ((match = match_julia(cursor)`${t.as("object")}.${t.as("property")}`)) { - let { object, property } = match - if (object) scopestate = explore_variable_usage(object.cursor(), doc, scopestate, verbose) - return scopestate - } else if ((match = match_julia(cursor)`${t.as("assignee")} = ${t.maybe(t.as("value"))}`)) { - let { assignee, value } = match - if (value) scopestate = explore_variable_usage(value.cursor(), doc, scopestate, verbose) - if (assignee) scopestate = explore_pattern(assignee.cursor(), doc, scopestate, value?.to ?? null, verbose) - return scopestate - } else if ( - (match = match_julia(cursor)` - ${t.as("macro", t.anything_that_fits(jl`@macro`))}(${t.many("args")}) ${t.maybe(jl`do ${t.maybe(t.many("do_args"))} - ${t.many("do_expressions")} - end`)}} - `) - ) { - let { macro, args = [], do_args, do_expressions } = match - if (macro) explore_macro_identifier(macro.cursor(), doc, scopestate, verbose) - - for (let { node: arg } of args) { - if ((match = match_function_call_argument(arg)`${t.as("name")} = ${t.as("value")}`)) { - let { name, value } = match - if (value) scopestate = explore_variable_usage(value.cursor(), doc, scopestate, verbose) - } else { - scopestate = explore_variable_usage(arg.cursor(), doc, scopestate, verbose) - } - } - - if (do_args && do_expressions) { - // Cheating because lezer-julia isn't up to this task yet - // TODO julia-lezer is up to the task now!! - let inner_scope = lower_scope(scopestate) - - // Don't ask me why, but currently `do (x, y)` is parsed as `DoClauseArguments(ArgumentList(x, y))` - // while an actual argumentslist, `do x, y` is parsed as `DoClauseArguments(BareTupleExpression(x, y))` - let do_args_actually = do_args.firstChild - if (do_args_actually?.name === "Identifier") { - inner_scope = scopestate_add_definition(inner_scope, doc, do_args_actually) - } else if (do_args_actually?.name === "ArgumentList") { - for (let child of child_nodes(do_args_actually)) { - inner_scope = explorer_function_definition_argument(child, doc, inner_scope) - } - } else if (do_args_actually?.name === "BareTupleExpression") { - for (let child of child_nodes(do_args_actually)) { - inner_scope = explorer_function_definition_argument(child, doc, inner_scope) - } - } else { - verbose && console.warn("Unrecognized do args", do_args_actually.toString()) - } - - for (let { node: expression } of do_expressions) { - inner_scope = explore_variable_usage(expression.cursor(), doc, inner_scope, verbose) - } - return raise_scope(inner_scope, scopestate, cursor.to) - } - - return scopestate - } else if ((match = match_julia(cursor)`${t.as("macro", t.anything_that_fits(jl`@macro`))} ${t.many("args")}`)) { - let { macro, args = [] } = match - if (macro) explore_macro_identifier(macro.cursor(), doc, scopestate, verbose) - - for (let { node: arg } of args) { - scopestate = explore_variable_usage(arg.cursor(), doc, scopestate, verbose) - } - return scopestate - } else if ( - (match = match_julia(cursor)` - struct ${t.as("defined_as")} - ${t.many("expressions")} - end - `) ?? - (match = match_julia(cursor)` - mutable struct ${t.as("defined_as")} - ${t.many("expressions")} - end - `) - ) { - let { defined_as, expressions = [] } = match - defined_as = narrow(defined_as) - - let inner_scope = lower_scope(scopestate) - let outer_scope = fresh_scope() - - if (defined_as) ({ inner: inner_scope, outer: outer_scope } = explore_definition(defined_as.cursor(), doc, inner_scope)) - - // Struct body - for (let { node: expression } of expressions) { - if (cursor.name === "Identifier") { - // Nothing, this is just the name inside the struct blegh get it out of here - } else if ((match = match_julia(expression)`${t.as("subject")}::${t.as("type")}`)) { - // We're in X::Y, and Y is a reference - let { subject, type } = match - inner_scope = explore_variable_usage(type.cursor(), doc, inner_scope, verbose) - } else if ((match = match_julia(expression)`${t.as("assignee")} = ${t.as("value")}`)) { - let { assignee, value } = match - - // Yeah... we do the same `a::G` check again - if ((match = match_julia(assignee)`${t.as("subject")}::${t.as("type")}`)) { - let { subject, type } = match - inner_scope = explore_variable_usage(type.cursor(), doc, inner_scope, verbose) - } - inner_scope = explore_variable_usage(value.cursor(), doc, inner_scope, verbose) - } - } - - scopestate = raise_scope(inner_scope, scopestate, cursor.to) - scopestate = merge_scope_state(scopestate, outer_scope) - return scopestate - } else if ((match = match_julia(cursor)`abstract type ${t.as("name")} end`)) { - let { name } = match - if (name) { - let { outer } = explore_definition(name.cursor(), doc, scopestate) - scopestate = merge_scope_state(scopestate, outer) - } - return scopestate - } else if ((match = match_julia(cursor)`quote ${t.many("body")} end`) ?? (match = match_julia(cursor)`:(${t.many("body")})`)) { - // We don't use the match because I made `go_through_quoted_expression_looking_for_interpolations` - // to take a cursor at the quoted expression itself - for (let interpolation_cursor of go_through_quoted_expression_looking_for_interpolations(cursor)) { - scopestate = explore_variable_usage(interpolation_cursor, doc, scopestate, verbose) - } - return scopestate - } else if ( - (match = match_julia(cursor)` - module ${t.as("name")} - ${t.many("expressions")} - end - `) - ) { - let { name, expressions = [] } = match - - if (name) scopestate = scopestate_add_definition(scopestate, doc, name) - - let module_scope = fresh_scope() - for (let { node: expression } of expressions) { - module_scope = explore_variable_usage(expression.cursor(), doc, module_scope) - } - // We still merge the module scopestate with the global scopestate, but only the usages that don't escape. - // (Later we can have also shadowed definitions for the dimming of unused variables) - scopestate = merge_scope_state(scopestate, { - usages: Array.from(module_scope.usages).filter((x) => x.definition != null), - definitions: new Map(), - locals: [], + definition: find_local_definition(locals, name, cursor) ?? null, }) - - for (let { node: expression } of expressions) { - scopestate = explore_variable_usage(expression.cursor(), doc, scopestate) - } - return scopestate - } else if ((match = match_julia(cursor)`${t.as("prefix")}${t.as("string", t.String)}`)) { - // This one is also a bit enigmatic, but `t.String` renders in the template as `"..."`, - // so the template with match things that look like `prefix"..."` - let { prefix, string } = match - let prefix_string = doc.sliceString(prefix.from, prefix.to) - - if (prefix_string === "var") { - let name = doc.sliceString(string.from + 1, string.to - 1) - if (name.length !== 0) { - scopestate.usages.push({ - name: name, - usage: { - from: cursor.from, - to: cursor.to, - }, - definition: scopestate.definitions.get(name) ?? null, - }) - } - return scopestate - } else { - let name = `@${prefix_string}_str` - scopestate.usages.push({ - name: name, - usage: { - from: prefix.from, - to: prefix.to, - }, - definition: scopestate.definitions.get(name) ?? null, - }) - } - return scopestate - } else if ((match = match_julia(cursor)`${t.Number}${t.as("unit")}`)) { - // This isn't that useful, just wanted to test (and show off) the template - return explore_variable_usage(match.unit.cursor(), doc, scopestate, verbose) - } else if ( - (match = match_julia(cursor)`import ${t.any}: ${t.many("specifiers")}`) ?? - (match = match_julia(cursor)`using ${t.any}: ${t.many("specifiers")}`) - ) { - let { specifiers = [] } = match - let match_selected_import_specifier = make_beautiful_specific_matcher((x) => jl_dynamic`import X: ${x}`) - - for (let { node: specifier } of specifiers) { - if ((match = match_selected_import_specifier(specifier)`${t.as("name")} as ${t.as("alias")}`)) { - let { alias } = match - scopestate = scopestate_add_definition(scopestate, doc, alias) - } else if ((match = match_selected_import_specifier(specifier)`${t.as("name", t.Identifier)}`)) { - let { name } = match - scopestate = scopestate_add_definition(scopestate, doc, name) - } else if ((match = match_selected_import_specifier(specifier)`@${t.any}`)) { - scopestate = scopestate_add_definition(scopestate, doc, specifier) - } else { - verbose && console.warn("Hmmmm, I don't know what to do with this selected import specifier:", specifier.toString()) - } - } - return scopestate - } else if ((match = match_julia(cursor)`import ${t.many("specifiers")}`)) { - let { specifiers = [] } = match - - let match_import_specifier = make_beautiful_specific_matcher((x) => jl_dynamic`import ${x}`) - - for (let { node: specifier } of specifiers) { - if ((match = match_import_specifier(specifier)`${t.any} as ${t.as("alias")}`)) { - let { alias } = match - scopestate = scopestate_add_definition(scopestate, doc, alias) - } else if ((match = match_import_specifier(specifier)`${t.as("package")}.${t.as("name", t.Identifier)}`)) { - scopestate = scopestate_add_definition(scopestate, doc, match.name) - } else if ((match = match_import_specifier(specifier)`.${t.as("scoped")}`)) { - let scopedmatch = null - while ((scopedmatch = match_import_specifier(match.scoped)`.${t.as("scoped")}`)) { - match = scopedmatch - } - scopestate = scopestate_add_definition(scopestate, doc, match.scoped) - } else if ((match = match_import_specifier(specifier)`${t.as("name", t.Identifier)}`)) { - scopestate = scopestate_add_definition(scopestate, doc, match.name) - } else { - verbose && console.warn("Hmmm, I don't know what to do with this import specifier:", specifier) - } - } - return scopestate - } else if ((match = match_julia(cursor)`using ${t.many()}`)) { - // Can't care less - return scopestate - } else if ( - (match = match_julia(cursor)` - for ${t.many("bindings", t.something_with_the_same_type_as(jl`x in y`))}; - ${t.many("expressions")} - end - `) - ) { - let for_loop_binding_template = create_specific_template_maker((arg) => jl_dynamic`for ${arg}; x end`) - let for_loop_binding_match_julia = - (cursor) => - (...args) => { - // @ts-ignore - return for_loop_binding_template(jl(...args)).match(cursor) + } else if (cursor.name === "Assignment" || cursor.name === "KwArg" || cursor.name === "ForBinding" || cursor.name === "CatchClause") { + if (cursor.firstChild()) { + // @ts-ignore + if (cursor.name === "catch") cursor.nextSibling() + // CallExpression means function definition `f(x) = x`, this is handled elsewhere + // @ts-ignore + if (cursor.name !== "CallExpression") { + explore_assignment_lhs(cursor).forEach(register_variable) + // mark this one as finished + return_false_immediately.cursorSet(cursor, true) } - - let { bindings, expressions } = match - let inner_scope = lower_scope(scopestate) - - for (let { node: binding } of bindings) { - let match = null - if ( - (match = for_loop_binding_match_julia(binding)`${t.as("name")} in ${t.as("range")}`) ?? - (match = for_loop_binding_match_julia(binding)`${t.as("name")} ∈ ${t.as("range")}`) ?? - (match = for_loop_binding_match_julia(binding)`${t.as("name")} = ${t.as("range")}`) - ) { - let { name, range } = match - if (range) inner_scope = explore_variable_usage(range.cursor(), doc, inner_scope, verbose) - if (name) inner_scope = explore_pattern(name, doc, inner_scope, range?.to ?? null, verbose) - } else { - verbose && console.warn("Unrecognized for loop binding", binding.toString()) + cursor.parent() + } + } else if (cursor.name === "Parameters") { + explore_assignment_lhs(cursor).forEach(register_variable) + if (verbose) console.groupEnd() + return false + } else if (cursor.name === "Field") { + if (verbose) console.groupEnd() + return false + } else if (cursor.name === "CallExpression") { + if (cursor.matchContext(["FunctionDefinition", "Signature"]) || (cursor.matchContext(["Assignment"]) && i_am_nth_child(cursor) === 0)) { + const pos_resetter = back_to_parent_resetter(cursor) + + cursor.firstChild() // CallExpression now + cursor.firstChild() + // @ts-ignore + if (cursor.name === "Identifier" || cursor.name === "Operator") { + if (verbose) console.log("found function name", doc.sliceString(cursor.from, cursor.to)) + + const last_scoper = local_scope_stack.pop() + register_variable(r(cursor)) + if (last_scoper) local_scope_stack.push(last_scoper) + + cursor.nextSibling() } - } - - for (let { node: expression } of expressions) { - inner_scope = explore_variable_usage(expression.cursor(), doc, inner_scope, verbose) - } - - return raise_scope(inner_scope, scopestate, cursor.to) - } else if ( - (match = match_julia(cursor)` - ${t.as("callee")}(${t.many("args")}) ${t.maybe(jl`do ${t.maybe(t.many("do_args"))} - ${t.many("do_expressions")} - end`)} - `) ?? - (match = match_julia(cursor)` - ${t.as("callee")}.(${t.many("args")}) - `) - ) { - let { callee, args = [], do_args = [], do_expressions = [] } = match - - scopestate = explore_variable_usage(callee.cursor(), doc, scopestate, verbose) - - for (let { node: arg } of args) { - let match = null - if ((match = match_function_call_argument(arg)`; ${t.many("named_args")}`)) { - // "Parameters", the part in `f(x; y, z)` after the `;` - let { named_args = [] } = match - for (let { node: named_arg } of named_args) { - let match = null - if ((match = match_function_call_named_argument(named_arg)`${t.as("name")} = ${t.as("value")}`)) { - let { name, value } = match - scopestate = explore_variable_usage(value.cursor(), doc, scopestate, verbose) - } else { - scopestate = explore_variable_usage(named_arg.cursor(), doc, scopestate, verbose) - } - } - } else if ((match = match_function_call_argument(arg)`${t.as("name")} = ${t.as("value")}`)) { - let { name, value } = match - if (value) scopestate = explore_variable_usage(value.cursor(), doc, scopestate, verbose) - } else if ((match = match_function_call_argument(arg)`${t.as("result")} ${t.many("clauses", t.anything_that_fits(jl`for x = y`))}`)) { - let { result, clauses } = match - let nested_scope = lower_scope(scopestate) - // Because of the `t.anything_that_fits`, we can now match different `for x ? y`'s and `if x`s manually. - // There might be a way to express this in the template, but this keeps templates a lot simpler yet powerful. - for (let { node: for_binding } of clauses) { - let match = null - if ( - (match = match_for_binding(for_binding)`for ${t.as("variable")} = ${t.maybe(t.as("value"))}`) ?? - (match = match_for_binding(for_binding)`for ${t.as("variable")} in ${t.maybe(t.as("value"))}`) ?? - (match = match_for_binding(for_binding)`for ${t.as("variable")} ∈ ${t.maybe(t.as("value"))}`) ?? - (match = match_for_binding(for_binding)`for ${t.as("variable")}`) - ) { - let { variable, value } = match - - if (value) nested_scope = explore_variable_usage(value.cursor(), doc, nested_scope, verbose) - if (variable) nested_scope = explore_pattern(variable, doc, nested_scope) - } else if ((match = match_for_binding(for_binding)`if ${t.maybe(t.as("if"))}`)) { - let { if: node } = match - if (node) nested_scope = explore_variable_usage(node.cursor(), doc, nested_scope, verbose) - } else { - verbose && console.log("Hmmm, can't parse for binding", for_binding) - } - } + if (verbose) console.log("expl funcdef ", doc.sliceString(cursor.from, cursor.to)) + explore_funcdef_arguments(cursor, { enter, leave }).forEach(register_variable) + if (verbose) console.log("expl funcdef ", doc.sliceString(cursor.from, cursor.to)) - nested_scope = explore_variable_usage(result.cursor(), doc, nested_scope, verbose) + pos_resetter() - return raise_scope(nested_scope, scopestate, cursor.to) - } else { - scopestate = explore_variable_usage(arg.cursor(), doc, scopestate, verbose) - } - } + if (verbose) console.log("end of FunctionDefinition, currently at ", cursor.node) - let inner_scope = lower_scope(scopestate) - - for (let { node: arg } of do_args) { - inner_scope = explorer_function_definition_argument(arg, doc, inner_scope) - } - for (let { node: expression } of do_expressions) { - inner_scope = explore_variable_usage(expression.cursor(), doc, inner_scope, verbose) + if (verbose) console.groupEnd() + return false } - return raise_scope(inner_scope, scopestate, cursor.to) - } else if ((match = match_julia(cursor)`(${t.many("tuple_elements")},)`)) { - // TODO.. maybe? `(x, g = y)` is a "ParenthesizedExpression", but lezer parses it as a tuple... - // For now I fix it here hackily by checking if there is only NamedFields + } else if (cursor.name === "Generator") { + // This is: (f(x) for x in xs) or [f(x) for x in xs] + const savior = back_to_parent_resetter(cursor) - let { tuple_elements = [] } = match + // We do a Generator in two steps: + // First we explore all the ForBindings (where locals get defined), and then we go into the first child (where those locals are used). - let match_tuple_element = make_beautiful_specific_matcher((arg) => jl_dynamic`(${arg},)`) - - let is_named_field = tuple_elements.map(({ node }) => match_tuple_element(cursor)`${t.Identifier} = ${t.any}` != null) - - if (is_named_field.every((x) => x === true) || is_named_field.every((x) => x === false)) { - // Valid tuple, either named or unnamed - for (let { node: element } of tuple_elements) { - let match = null - if ((match = match_tuple_element(cursor)`${t.as("name")} = ${t.as("value")}`)) { - let { name, value } = match - if (value) scopestate = explore_variable_usage(value.cursor(), doc, scopestate, verbose) - } else { - scopestate = explore_variable_usage(element.cursor(), doc, scopestate, verbose) - } - } - } else { - // Sneaky! Actually an expression list 😏 - for (let { node: element } of tuple_elements) { - let match = null - if ((match = match_tuple_element(cursor)`${t.as("name")} = ${t.as("value")}`)) { - // 🚨 actually an assignment 🚨 - let { name, value } = match - if (value) scopestate = explore_variable_usage(value.cursor(), doc, scopestate, verbose) - if (name) scopestate = scopestate_add_definition(scopestate, doc, name, value?.to ?? null) - } else { - scopestate = explore_variable_usage(element.cursor(), doc, scopestate, verbose) - } + // 1. The for bindings `x in xs` + if (cursor.firstChild()) { + // Note that we skip the first child here, which is what we want! That's the iterated expression that we leave for the end. + while (cursor.nextSibling()) { + cursor.iterate(enter, leave) } + savior() } - return scopestate - } else if ( - (match = match_julia(cursor)`(${t.many("args")}) -> ${t.many("body")}`) ?? - (match = match_julia(cursor)`${t.as("arg")} -> ${t.many("body")}`) ?? - (match = match_julia(cursor)`${t.as("name")}(${t.many("args")})::${t.as("return_type")} = ${t.many("body")}`) ?? - (match = match_julia(cursor)`${t.as("name")}(${t.many("args")}) = ${t.many("body")}`) ?? - (match = match_julia(cursor)`${t.as("name")}(${t.many("args")}) = ${t.many("body", t.anything_that_fits(jl`x, y`))}`) ?? - (match = match_julia(cursor)` - function ${t.as("name")}(${t.many("args")})::${t.as("return_type")} where ${t.as("type_param")} - ${t.many("body")} - end - `) ?? - (match = match_julia(cursor)` - function ${t.as("name")}(${t.many("args")}) where ${t.as("type_param")} - ${t.many("body")} - end - `) ?? - (match = match_julia(cursor)` - function ${t.as("name")}(${t.many("args")})::${t.as("return_type")} - ${t.many("body")} - end - `) ?? - (match = match_julia(cursor)` - function ${t.as("name")}(${t.many("args")}) - ${t.many("body")} - end - `) ?? - (match = match_julia(cursor)` - function ${t.as("name", t.Identifier)} end - `) ?? - // Putting macro definitions here too because they are very similar - (match = match_julia(cursor)`macro ${t.as("macro_name")} end`) ?? - (match = match_julia(cursor)` - macro ${t.as("macro_name")}(${t.many("args")}) - ${t.many("body")} - end - `) - ) { - let { name, macro_name, arg, args = [], return_type, type_param, body = [] } = match - - if (arg) { - args.push({ node: arg }) - } - - if (name) { - scopestate = scopestate_add_definition(scopestate, doc, name) - } else if (macro_name) { - scopestate.definitions.set(`@${doc.sliceString(macro_name.from, macro_name.to)}`, { - from: macro_name.from, - to: macro_name.to, - valid_from: macro_name.to, - }) - } - - let inner_scope = lower_scope(scopestate) - if (type_param) { - let match_where_types = make_beautiful_specific_matcher((x) => jl_dynamic`function X() where ${x} end`) - let match_where_type = make_beautiful_specific_matcher((x) => jl_dynamic`function X() where {${x}} end`) - - let type_params = [{ node: type_param }] - let multiple_types_match = match_where_types(type_param)`{${t.many("type_params")}}` - if (multiple_types_match) { - type_params = multiple_types_match.type_params - } - - for (let { node: type_param } of type_params) { - let match = null - if ((match = match_where_type(type_param)`${t.as("defined", t.Identifier)} <: ${t.as("parent_type")}`)) { - let { defined, parent_type } = match - inner_scope = explore_variable_usage(parent_type, doc, inner_scope, verbose) - inner_scope = scopestate_add_definition(inner_scope, doc, defined) - } else if ((match = match_where_type(type_param)`${t.as("defined", t.Identifier)}`)) { - let { defined } = match - inner_scope = scopestate_add_definition(inner_scope, doc, defined) - } else { - verbose && console.warn(`Can't handle type param:`, type_param) - } - } + // 2. The iterated expression `f(x)` + if (cursor.firstChild()) { + cursor.iterate(enter, leave) + savior() } - if (return_type) { - inner_scope = explore_variable_usage(narrow(return_type).cursor(), doc, inner_scope, verbose) - } - for (let { node: arg } of args) { - inner_scope = explorer_function_definition_argument(arg.cursor(), doc, inner_scope, verbose) - } - for (let { node: expression } of body) { - inner_scope = explore_variable_usage(expression.cursor(), doc, inner_scope, verbose) - } - return raise_scope(inner_scope, scopestate, cursor.to) - } else if ( - (match = match_julia(cursor)` - let ${t.many("assignments", jl`${t.as("assignee")} = ${t.as("value")}`)} - ${t.many("body", t.any)} - end - `) - ) { - let { assignments = [], body = [] } = match - let innerscope = lower_scope(scopestate) - for (let { - match: { assignee, value }, - } of assignments) { - // Explorer lefthandside in inner scope - if (assignee) innerscope = explore_pattern(assignee, doc, innerscope, value?.to ?? null, verbose) - // Explorer righthandside in the outer scope - if (value) scopestate = explore_variable_usage(value.cursor(), doc, scopestate, verbose) - } - // Explorer body in innerscope - for (let { node: line } of body) { - innerscope = explore_variable_usage(line.cursor(), doc, innerscope, verbose) - } - return raise_scope(innerscope, scopestate, cursor.to) - } else if ( - // A bit hard to see from the template, but these are array (and generator) comprehensions - // e.g. [x for x in y] - (match = match_julia(cursor)`[ - ${t.as("result")} - ${t.many("clauses", t.anything_that_fits(jl`for x = y`))} - ]`) ?? - // Are there syntax differences between Array or Generator expressions? - // For now I treat them the same... - // (Also this is one line because lezer doesn't parse multiline generator expressions yet) - (match = match_julia(cursor)`(${t.as("result")} ${t.many("clauses", t.anything_that_fits(jl`for x = y`))})`) - ) { - let { result, clauses } = match + // k thx byeee + leave(cursor) + return false + } + } - let nested_scope = lower_scope(scopestate) + leave = (/** @type {TreeCursor} */ cursor) => { + if (verbose) { + console.groupEnd() + } - // Because of the `t.anything_that_fits`, we can now match different `for x in/∈/= y`-s and `if x`-s manually. - // There might be a way to express this in the template, but this keeps templates a lot simpler yet powerful. - for (let { node: for_binding } of clauses) { - let match = null - if ( - (match = match_for_binding(for_binding)`for ${t.as("variable")} = ${t.maybe(t.as("value"))}`) ?? - (match = match_for_binding(for_binding)`for ${t.as("variable")} in ${t.maybe(t.as("value"))}`) ?? - (match = match_for_binding(for_binding)`for ${t.as("variable")} ∈ ${t.maybe(t.as("value"))}`) ?? - (match = match_for_binding(for_binding)`for ${t.as("variable")}`) - ) { - let { variable, value } = match + if (does_this_create_scope(cursor)) { + local_scope_stack.pop() + } + } - if (value) nested_scope = explore_variable_usage(value.cursor(), doc, nested_scope, verbose) - if (variable) nested_scope = explore_pattern(variable, doc, nested_scope) - } else if ((match = match_for_binding(for_binding)`if ${t.maybe(t.as("if"))}`)) { - let { if: node } = match - if (node) nested_scope = explore_variable_usage(node.cursor(), doc, nested_scope, verbose) - } else { - verbose && console.warn("Hmmm, can't parse for binding", for_binding) - } - } + const debugged_enter = (cursor) => { + const a = cursor_not_moved_checker(cursor) + const result = enter(cursor) + a() + return result + } - nested_scope = explore_variable_usage(result.cursor(), doc, nested_scope, verbose) + tree.iterate(verbose ? debugged_enter : enter, leave) - return raise_scope(nested_scope, scopestate, cursor.to) - } else { - if (verbose) { - console.groupCollapsed(`Cycling through all children of`, cursor.name) - console.log(`text:`, doc.sliceString(cursor.from, cursor.to)) - console.groupEnd() - } + if (local_scope_stack.length > 0) throw new Error(`Some scopes were not leaved... ${JSON.stringify(local_scope_stack)}`) - // In most cases we "just" go through all the children separately - for (let subcursor of child_cursors(cursor)) { - scopestate = explore_variable_usage(subcursor, doc, scopestate, verbose) - } - return scopestate - } - } finally { - verbose && console.groupEnd() - } + const output = { usages, definitions, locals } + if (verbose) console.log(output) + return output } /** @@ -1099,7 +380,7 @@ export let ScopeStateField = StateField.define({ try { if (syntaxTree(tr.state) != syntaxTree(tr.startState)) { let cursor = syntaxTree(tr.state).cursor() - let scopestate = explore_variable_usage(cursor, tr.state.doc) + let scopestate = explore_variable_usage(cursor, tr.state.doc, null) return scopestate } else { return value diff --git a/frontend/components/CellInput/tests/.gitignore b/frontend/components/CellInput/tests/.gitignore deleted file mode 100644 index ed9f9cc128..0000000000 --- a/frontend/components/CellInput/tests/.gitignore +++ /dev/null @@ -1 +0,0 @@ -coverage \ No newline at end of file diff --git a/frontend/components/CellInput/tests/README.md b/frontend/components/CellInput/tests/README.md deleted file mode 100644 index c8dd93eef5..0000000000 --- a/frontend/components/CellInput/tests/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# Deno unit tests - -Unit tests for the scopestate explorer (and maybe later more who knows) - -```shell -deno test --import-map=./import_map.json --allow-env -``` - -You can also do coverage, which looks fun but not sure how to interpret it: - -```shell -deno test --import-map=./import_map.json --allow-env --coverage=coverage -deno coverage coverage --include=../scopestate_statefield.js -``` \ No newline at end of file diff --git a/frontend/components/CellInput/tests/import_map.json b/frontend/components/CellInput/tests/import_map.json deleted file mode 100644 index 57a87cf9b4..0000000000 --- a/frontend/components/CellInput/tests/import_map.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "imports": { - "../../../imports/lodash.js": "https://deno.land/x/lodash@4.17.15-es/lodash.js" - } -} diff --git a/frontend/components/CellInput/tests/scopestate.test.js b/frontend/components/CellInput/tests/scopestate.test.js deleted file mode 100644 index 5dc05e668c..0000000000 --- a/frontend/components/CellInput/tests/scopestate.test.js +++ /dev/null @@ -1,151 +0,0 @@ -import { jl } from "../lezer_template.js" -import { test_implicit } from "./scopestate_helpers.js" -// @ts-ignore -import chalk from "https://deno.land/x/chalk_deno@v4.1.1-deno/source/index.js" - -let broken = (fn) => { - try { - console.log() - fn() - console.log(chalk.green("BROKEN TEST PASSED???")) - } catch (error) { - console.log(chalk.blue("Broken test failed as expected")) - } -} - -Deno.test("Function call", () => { - test_implicit(jl`global_call(global_argument1, global_argument2...)`) -}) - -Deno.test("Simple definition", () => { - test_implicit(jl`defined = 10`) -}) - -Deno.test("Tuple destructuring", () => { - test_implicit(jl`(defined_1, defined_2, defined_3...) = global_call()`) -}) - -// Need to fix this later -Deno.test("BROKEN Named destructuring", () => { - broken(() => { - test_implicit(jl`(; irrelevant_1=defined_1, irrelevant_2=defined_2) = global_call()`) - }) -}) - -Deno.test("Array comprehension", () => { - test_implicit(jl`(local_x + global_const for local_x in global_xs)`) - test_implicit(jl`( - local_x + local_y + global_const - for local_x in global_xs - for local_y in [local_x, local_x * 2] - )`) - test_implicit(jl`(local_x for local_x in global_xs if local_x > global_const)`) -}) - -Deno.test("BROKEN Array comprehension with comma but VERY WEIRD", () => { - // So... `for x in xs, y in ys` does resolve the `_ in _`'s from right to left... - // Except..... it then specifically "unbinds" the defined variables..... - // So in this case, even though `irrelevant` might be globally defined, - // this will ALWAYS say `irrelevant is not defined`, because it is "overshadowed" by the `for irrelevant in global_ys`. - broken(() => { - test_implicit(jl`(local_x + global_const for local_x in irrelevant, for irrelevant in global_ys)`) - }) -}) - -Deno.test("BROKEN Array comprehension with comma", () => { - broken(() => { - test_implicit(jl`(local_x for global_array in global_array_array, for local_x in global_array)`) - }) -}) - -Deno.test("Function definition", () => { - test_implicit(jl` - function defined_function(local_argument1, local_argument2...; local_argument3, local_argument4...) - local_argument1, local_argument2, local_argument3, local_argument4 - global_var1, global_var2 - end - `) -}) - -Deno.test("Function definition", () => { - test_implicit(jl` - function defined_function(local_argument1 = global_default) - local_argument1 - end - `) -}) - -Deno.test("Function definition where", () => { - test_implicit(jl` - function defined_function(local_argument1) where {local_type} - local_argument1, local_type - end - `) -}) - -Deno.test("Function definition returntype", () => { - test_implicit(jl`begin - function defined_function(local_argument1)::global_type - local_argument1 - end - end`) -}) - -Deno.test("Function expression", () => { - test_implicit(jl`defined_fn = (local_argument1, local_argument2) -> (local_argument1, local_argument2, global_var1, global_var2)`) -}) - -Deno.test("Let block", () => { - test_implicit(jl`defined_outside = let local_let = global_let - local_inside = local_let - local_inside * global_inside - end`) -}) - -Deno.test("Imports", () => { - test_implicit(jl`import defined_module`) - test_implicit(jl`import X: defined_specific1, defined_specific2`) - test_implicit(jl`import X: defined_specific3, irrelevant as defined_alias`) - test_implicit(jl`import X.defined_specific4`) - test_implicit(jl`import X.irrelevant as defined_alias2`) -}) - -Deno.test("Typed struct", () => { - test_implicit(jl`begin - struct defined_struct{local_type} <: global_type{local_type, global_type2} - g - y::global_type3 - z = global_var - x::local_type = global_var2 - end - end`) -}) - -Deno.test("Quotes", () => { - test_implicit(jl`quote - irrelevant_1 = irrelevant_2 - irrelevant_3 = $(global_var) - end`) -}) - -Deno.test("Nested Quotes", () => { - test_implicit(jl`quote - :($irrelevant) - :($$global_var) - end - end`) -}) - -Deno.test("Macros", () => { - test_implicit(jl`global_used.@irrelevant`) - test_implicit(jl`@global_but_not_macro.irrelevant`) - test_implicit(jl`@macro_global`) -}) - -Deno.test("Lonely bare tuple", () => { - test_implicit(jl`defined, = (global_var,)`) -}) - -Deno.test("Very, very lonely arguments", () => { - test_implicit(jl`global_var(;)`) -}) diff --git a/frontend/components/CellInput/tests/scopestate_helpers.js b/frontend/components/CellInput/tests/scopestate_helpers.js deleted file mode 100644 index 45054c083a..0000000000 --- a/frontend/components/CellInput/tests/scopestate_helpers.js +++ /dev/null @@ -1,75 +0,0 @@ -import { assertEquals as untyped_assertEquals } from "https://deno.land/std@0.123.0/testing/asserts.ts" -import { jl, as_node, as_doc, JuliaCodeObject, as_string } from "../lezer_template.js" -import { explore_variable_usage } from "../scopestate_statefield.js" - -/** - * @template T - * @param {T} a - * @param {T} b - **/ -export let assertEquals = (a, b) => untyped_assertEquals(a, b) - -/** - * @param {import("../scopestate_statefield.js").ScopeState} scopestate - */ -let simplify_scopestate = (scopestate) => { - let { definitions, usages } = scopestate - return { - defined: new Set(Array.from(definitions.keys())), - local_used: new Set(usages.filter((x) => x.definition != null).map((x) => x.name)), - global_used: new Set(usages.filter((x) => x.definition == null).map((x) => x.name)), - } -} - -/** - * @param {import("../lezer_template.js").JuliaCodeObject} input - * @param {{ - * defined?: Array, - * local_used?: Array, - * global_used?: Array, - * }} expected - */ -export let test_scopestate = (input, expected) => { - let scopestate = { - defined: [], - local_used: [], - global_used: [], - } - assertEquals(simplify_scopestate(explore_variable_usage(as_node(input).cursor(), as_doc(input), undefined, false)), { - defined: new Set(expected.defined), - local_used: new Set(expected.local_used), - global_used: new Set(expected.global_used), - }) -} - -/** - * Compute the scopestate for the code. Variable names are used to create the expected scope: a variable called `global_something_2` is expected to be a global usage, etc. - * - * @param {JuliaCodeObject} code - */ -export function test_implicit(code) { - let expected_scopestate = { defined: [], local_used: [], global_used: [] } - - console.log(as_string(code)) - - for (let variable of as_string(code).matchAll(/(macro_)?(global|local|defined)(_[a-z0-9_]+)?/g)) { - let [variable_name, is_macro, usage_type, number] = variable - - if (is_macro != null) { - variable_name = `@${variable_name}` - } - - let index = variable.index - if (usage_type === "global") { - expected_scopestate.global_used.push(variable_name) - } else if (usage_type === "local") { - expected_scopestate.local_used.push(variable_name) - } else if (usage_type === "defined") { - expected_scopestate.defined.push(variable_name) - } - } - - console.log(expected_scopestate) - - return test_scopestate(code, expected_scopestate) -} diff --git a/frontend/imports/CodemirrorPlutoSetup.d.ts b/frontend/imports/CodemirrorPlutoSetup.d.ts index 4de86cb4d3..d86a63b1cb 100644 --- a/frontend/imports/CodemirrorPlutoSetup.d.ts +++ b/frontend/imports/CodemirrorPlutoSetup.d.ts @@ -4533,6 +4533,33 @@ declare class TreeCursor implements SyntaxNodeRef { */ matchContext(context: readonly string[]): boolean; } +/** +Provides a way to associate values with pieces of trees. As long +as that part of the tree is reused, the associated values can be +retrieved from an updated tree. +*/ +declare class NodeWeakMap { + private map; + private setBuffer; + private getBuffer; + /** + Set the value for this syntax node. + */ + set(node: SyntaxNode, value: T): void; + /** + Retrieve value for this syntax node, if it exists in the map. + */ + get(node: SyntaxNode): T | undefined; + /** + Set the value for the node that a cursor currently points to. + */ + cursorSet(cursor: TreeCursor, value: T): void; + /** + Retrieve the value for the node that a cursor currently points + to. + */ + cursorGet(cursor: TreeCursor): T | undefined; +} /** Objects returned by the function passed to @@ -7208,4 +7235,4 @@ Python language support. */ declare function python(): LanguageSupport; -export { Annotation, ChangeSet, Compartment, Decoration, Diagnostic, EditorSelection, EditorState, EditorView, Facet, HighlightStyle, MatchDecorator, NodeProp, PostgreSQL, SelectionRange, StateEffect, StateField, Text, Tooltip, Transaction, TreeCursor, ViewPlugin, ViewUpdate, WidgetType, index_d as autocomplete, bracketMatching, closeBrackets, closeBracketsKeymap, collab, combineConfig, completionKeymap, css, cssLanguage, defaultHighlightStyle, defaultKeymap, drawSelection, foldGutter, foldKeymap, getClientID, getSyncedVersion, highlightActiveLine, highlightSelectionMatches, highlightSpecialChars, history, historyKeymap, html, htmlLanguage, indentLess, indentMore, indentOnInput, indentUnit, javascript, javascriptLanguage, julia, keymap, lineNumbers, linter, markdown, markdownLanguage, moveLineDown, moveLineUp, parseCode, parseMixed, placeholder, python, pythonLanguage, receiveUpdates, rectangularSelection, searchKeymap, selectNextOccurrence, sendableUpdates, setDiagnostics, showTooltip, sql, syntaxHighlighting, syntaxTree, syntaxTreeAvailable, tags, tooltips }; +export { Annotation, ChangeSet, Compartment, Decoration, Diagnostic, EditorSelection, EditorState, EditorView, Facet, HighlightStyle, MatchDecorator, NodeProp, NodeWeakMap, PostgreSQL, SelectionRange, StateEffect, StateField, Text, Tooltip, Transaction, Tree, TreeCursor, ViewPlugin, ViewUpdate, WidgetType, index_d as autocomplete, bracketMatching, closeBrackets, closeBracketsKeymap, collab, combineConfig, completionKeymap, css, cssLanguage, defaultHighlightStyle, defaultKeymap, drawSelection, foldGutter, foldKeymap, getClientID, getSyncedVersion, highlightActiveLine, highlightSelectionMatches, highlightSpecialChars, history, historyKeymap, html, htmlLanguage, indentLess, indentMore, indentOnInput, indentUnit, javascript, javascriptLanguage, julia, keymap, lineNumbers, linter, markdown, markdownLanguage, moveLineDown, moveLineUp, parseCode, parseMixed, placeholder, python, pythonLanguage, receiveUpdates, rectangularSelection, searchKeymap, selectNextOccurrence, sendableUpdates, setDiagnostics, showTooltip, sql, syntaxHighlighting, syntaxTree, syntaxTreeAvailable, tags, tooltips }; diff --git a/frontend/imports/CodemirrorPlutoSetup.js b/frontend/imports/CodemirrorPlutoSetup.js index 55e25a5766..f1c5bba63c 100644 --- a/frontend/imports/CodemirrorPlutoSetup.js +++ b/frontend/imports/CodemirrorPlutoSetup.js @@ -44,6 +44,7 @@ export { indentUnit, combineConfig, NodeProp, + NodeWeakMap, autocomplete, html, htmlLanguage, @@ -64,4 +65,4 @@ export { linter, setDiagnostics, //@ts-ignore -} from "https://cdn.jsdelivr.net/gh/JuliaPluto/codemirror-pluto-setup@f01836f/dist/index.es.min.js" +} from "https://cdn.jsdelivr.net/gh/JuliaPluto/codemirror-pluto-setup@d17bc01/dist/index.es.min.js" diff --git a/sample/cm6 crash test.jl b/sample/cm6 crash test.jl index 3c3fecad28..e6f36782d6 100644 --- a/sample/cm6 crash test.jl +++ b/sample/cm6 crash test.jl @@ -52,6 +52,9 @@ end x = a : b:c end +# ╔═╡ f248e96a-4050-4888-940b-f38158c102fe + + # ╔═╡ daba5486-8d5e-4fce-959b-251e821e5dea # https://github.com/fonsp/Pluto.jl/issues/2639 let x = 1 end @@ -355,6 +358,7 @@ end # ╠═bf834c19-3d0d-4989-9ba3-ef7cb77f9a00 # ╠═3da33f9d-1240-4522-9463-8772b0c2539a # ╠═c88fe37a-c2e6-46f1-b92b-98737437a741 +# ╠═f248e96a-4050-4888-940b-f38158c102fe # ╠═daba5486-8d5e-4fce-959b-251e821e5dea # ╠═287dd3c7-33e6-482d-9639-d502fcff9234 # ╠═6db1e583-54f2-4d8f-9181-a7913345c7fd diff --git a/src/analysis/Parse.jl b/src/analysis/Parse.jl index 5385b0cecf..11dbb3a085 100644 --- a/src/analysis/Parse.jl +++ b/src/analysis/Parse.jl @@ -119,6 +119,24 @@ end preprocess_expr(val::Any) = val +""" +Does this `String` contain a single expression? If this function returns `false`, then Pluto will show a "multiple expressions in one cell" error in the editor. + +!!! compat "Pluto 0.20.5" + This function is new in Pluto 0.20.5. + +""" +function is_single_expression(s::String) + n = Pluto.Notebook([Pluto.Cell(s)]) + e = parse_custom(n, n.cells[1]) + bad = Meta.isexpr(e, :toplevel, 2) && Meta.isexpr(e.args[2], :call, 2) && e.args[2].args[1] == :(PlutoRunner.throw_syntax_error) && e.args[2].args[2] isa String && startswith(e.args[2].args[2], "extra token after end of expression") + + + return !bad +end + + + function updated_topology(old_topology::NotebookTopology{Cell}, notebook::Notebook, updated_cells) get_code_str(cell::Cell) = cell.code get_code_expr(cell::Cell) = parse_custom(notebook, cell) diff --git a/src/webserver/Dynamic.jl b/src/webserver/Dynamic.jl index 5960c0ff74..b347e1629a 100644 --- a/src/webserver/Dynamic.jl +++ b/src/webserver/Dynamic.jl @@ -427,7 +427,6 @@ function _set_cells_to_queued_in_local_state(client, notebook, cells) if haskey(results, cell.cell_id) old = results[cell.cell_id]["queued"] results[cell.cell_id]["queued"] = true - @debug "Setting val!" cell.cell_id old end end end diff --git a/src/webserver/SessionActions.jl b/src/webserver/SessionActions.jl index 0135c9dbcb..52733f50e6 100644 --- a/src/webserver/SessionActions.jl +++ b/src/webserver/SessionActions.jl @@ -258,7 +258,9 @@ function move(session::ServerSession, notebook::Notebook, newpath::String) else move_notebook!(notebook, newpath; disable_writing_notebook_files=session.options.server.disable_writing_notebook_files) putplutoupdates!(session, clientupdate_notebook_list(session.notebooks)) - WorkspaceManager.cd_workspace((session, notebook), newpath) + let workspace = WorkspaceManager.get_workspace((session, notebook); allow_creation=false) + isnothing(workspace) || WorkspaceManager.cd_workspace(workspace, newpath) + end end end diff --git a/test/misc API.jl b/test/misc API.jl new file mode 100644 index 0000000000..5d9bc9ccb1 --- /dev/null +++ b/test/misc API.jl @@ -0,0 +1,43 @@ +@testset "Misc API" begin + + + @testset "is_single_expression" begin + + @test Pluto.is_single_expression("") + @test Pluto.is_single_expression("a") + @test Pluto.is_single_expression("a + 1") + @test Pluto.is_single_expression("a; a + 1") + @test !Pluto.is_single_expression(""" + a = 1 + a + 1 + """) + + @test Pluto.is_single_expression(""" + "yooo" + function f(x) + X + C \\ c + end + """) + + + @test Pluto.is_single_expression(""" + # asdf + + "yooo" + function f(x) + X + C \\ c + end; # aasasdf + """) + + + + @test Pluto.is_single_expression(""" + a a a a a // / // / 123 1 21 1313 + """) + + + + end +end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 4efab46c88..c088d38e33 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -46,6 +46,7 @@ verify_no_running_processes() @timeit_include("DependencyCache.jl") @timeit_include("Throttled.jl") @timeit_include("cell_disabling.jl") +@timeit_include("misc API.jl") verify_no_running_processes() From 97e27533011b22cfa6979e8f3dff1c0a309155ce Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Fri, 17 Jan 2025 23:32:55 +0100 Subject: [PATCH 12/14] update lezer to 0.12.4 --- frontend/imports/CodemirrorPlutoSetup.d.ts | 4 ++++ frontend/imports/CodemirrorPlutoSetup.js | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/imports/CodemirrorPlutoSetup.d.ts b/frontend/imports/CodemirrorPlutoSetup.d.ts index d86a63b1cb..34f4e31c2c 100644 --- a/frontend/imports/CodemirrorPlutoSetup.d.ts +++ b/frontend/imports/CodemirrorPlutoSetup.d.ts @@ -99,6 +99,9 @@ declare abstract class Text implements Iterable { objects that it is made up of. For leaf nodes, this holds null. */ abstract readonly children: readonly Text[] | null; + /** + @hide + */ [Symbol.iterator]: () => Iterator; /** Create a `Text` instance for the given array of lines. @@ -6806,6 +6809,7 @@ declare class BlockContext implements PartialParse { get depth(): number; parentType(depth?: number): NodeType; nextLine(): boolean; + peekLine(): string; private moveRangeI; private lineChunkAt; prevLineEnd(): number; diff --git a/frontend/imports/CodemirrorPlutoSetup.js b/frontend/imports/CodemirrorPlutoSetup.js index f1c5bba63c..eb28ce165a 100644 --- a/frontend/imports/CodemirrorPlutoSetup.js +++ b/frontend/imports/CodemirrorPlutoSetup.js @@ -65,4 +65,4 @@ export { linter, setDiagnostics, //@ts-ignore -} from "https://cdn.jsdelivr.net/gh/JuliaPluto/codemirror-pluto-setup@d17bc01/dist/index.es.min.js" +} from "https://cdn.jsdelivr.net/gh/JuliaPluto/codemirror-pluto-setup@35aa48a/dist/index.es.min.js" From 61679f96ec7efa7c465c16018c7abd25b9e67d13 Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Fri, 17 Jan 2025 23:53:46 +0100 Subject: [PATCH 13/14] remove some todos --- frontend/components/CellInput.js | 6 ++--- frontend/components/CellInput/mixedParsers.js | 24 +++++++++---------- .../CellInput/pluto_autocomplete.js | 16 +++++++++---- sample/cm6 crash test.jl | 16 ++++++++++--- 4 files changed, 39 insertions(+), 23 deletions(-) diff --git a/frontend/components/CellInput.js b/frontend/components/CellInput.js index 3f3afaa3d3..84e8769add 100644 --- a/frontend/components/CellInput.js +++ b/frontend/components/CellInput.js @@ -365,12 +365,12 @@ export const CellInput = ({ return true } - const anySelect = cm.state.selection.ranges.some(r => !r.empty) + const anySelect = cm.state.selection.ranges.some((r) => !r.empty) if (anySelect) { return indentMore(cm) } else { - cm.dispatch( - cm.state.changeByRange(selection => ({ + cm.dispatch( + cm.state.changeByRange((selection) => ({ range: EditorSelection.cursor(selection.from + 1), changes: { from: selection.from, to: selection.to, insert: "\t" }, })) diff --git a/frontend/components/CellInput/mixedParsers.js b/frontend/components/CellInput/mixedParsers.js index c191ff9933..763bac75db 100644 --- a/frontend/components/CellInput/mixedParsers.js +++ b/frontend/components/CellInput/mixedParsers.js @@ -82,19 +82,19 @@ const overlayHack = (overlay, input) => { export const STRING_NODE_NAMES = new Set(["StringLiteral", "CommandLiteral", "NsStringLiteral", "NsCommandLiteral"]) -const juliaWrapper = parseMixed((node, input) => { - // TODO: only get .node once - if (node.name !== "NsStringLiteral" && node.name !== "StringLiteral") { +const juliaWrapper = parseMixed((cursor, input) => { + if (cursor.name !== "NsStringLiteral" && cursor.name !== "StringLiteral") { return null } - const first_string_delim = node.node.getChild('"""') ?? node.node.getChild('"') + const node = cursor.node + const first_string_delim = node.getChild('"""') ?? node.getChild('"') if (first_string_delim == null) return null - const last_string_delim = node.node.lastChild + const last_string_delim = node.lastChild if (last_string_delim == null) return null - const offset = first_string_delim.to - first_string_delim.from - console.log({ first_string_delim, last_string_delim, offset }) + // const offset = first_string_delim.to - first_string_delim.from + // console.log({ first_string_delim, last_string_delim, offset }) const string_content_from = first_string_delim.to const string_content_to = Math.min(last_string_delim.from, input.length) @@ -103,12 +103,12 @@ const juliaWrapper = parseMixed((node, input) => { } let tagNode - if (node.name === "NsStringLiteral") { - tagNode = node.node.firstChild + if (cursor.name === "NsStringLiteral") { + tagNode = node.firstChild // if (tagNode) tag = input.read(tagNode.from, tagNode.to) } else { // must be a string, let's search for the parent `@htl`. - const start = node.node + const start = node const p1 = start.parent if (p1 != null && p1.name === "Arguments") { const p2 = p1.parent @@ -139,9 +139,9 @@ const juliaWrapper = parseMixed((node, input) => { } let overlay = [] - if (node.node.firstChild != null) { + if (node.firstChild != null) { let last_content_start = string_content_from - let child = node.node.firstChild.cursor() + let child = node.firstChild.cursor() do { if (last_content_start < child.from) { diff --git a/frontend/components/CellInput/pluto_autocomplete.js b/frontend/components/CellInput/pluto_autocomplete.js index 12718ff4c9..d749720c49 100644 --- a/frontend/components/CellInput/pluto_autocomplete.js +++ b/frontend/components/CellInput/pluto_autocomplete.js @@ -299,6 +299,7 @@ const from_notebook_type = "c_from_notebook completion_module c_Any" */ const writing_variable_name_or_keyword = (/** @type {autocomplete.CompletionContext} */ ctx) => { let just_finished_a_keyword = ctx.matchBefore(endswith_keyword_regex) + if (just_finished_a_keyword) return true // Regex explaination: // 1. a keyword that could be followed by a variable name like `catch ex` where `ex` is a variable name that should not get completed @@ -308,16 +309,21 @@ const writing_variable_name_or_keyword = (/** @type {autocomplete.CompletionCont // 3b. a `, ` comma-space, to treat `const a, b` but not `for a in // 4. a `$` to match the end of the line let after_keyword = ctx.matchBefore(/(catch|local|module|abstract type|struct|macro|const|for|function|let|do) ([@\p{L}\p{Nl}\p{Sc}\d_!,\(\)]|, )*$/u) + if (after_keyword) return true let inside_do_argument_expression = ctx.matchBefore(/do [\(\), \p{L}\p{Nl}\p{Sc}\d_!]*$/u) + if (inside_do_argument_expression) return true let node = syntaxTree(ctx.state).resolve(ctx.pos, -1) - // TODO: BareTupleExpression - let node2 = node?.parent?.name === "BareTupleExpression" ? node?.parent : node - // TODO: AssignmentExpression - let inside_assigment_lhs = node?.name === "Identifier" && node2?.parent?.name === "AssignmentExpression" && node2?.nextSibling != null + let npn = node?.parent?.name + if (node?.name === "Identifier" && npn === "KeywordArguments") return true - return just_finished_a_keyword || after_keyword || inside_do_argument_expression || inside_assigment_lhs + let node2 = npn === "OpenTuple" || npn === "TupleExpression" ? node?.parent : node + let n2pn = node2?.parent?.name + let inside_assigment_lhs = node?.name === "Identifier" && (n2pn === "Assignment" || n2pn === "KwArg") && node2?.nextSibling != null + + if (inside_assigment_lhs) return true + return false } const global_variables_completion = diff --git a/sample/cm6 crash test.jl b/sample/cm6 crash test.jl index e6f36782d6..3bdcee7875 100644 --- a/sample/cm6 crash test.jl +++ b/sample/cm6 crash test.jl @@ -137,6 +137,9 @@ begin a, b = 123, 33 end +# ╔═╡ e45548dc-cf3c-4ddd-be78-e2776380f172 +(zzz,zzz2) = [a,a] + # ╔═╡ c93f0182-3e16-4ff5-a612-5cd2e70d249f ( a + b @@ -160,9 +163,6 @@ catch a a end -# ╔═╡ 62b805db-2028-40b0-b412-4c91d15ad338 -c = d = e = f= g =h=i=j=k=l=m=n=o=p=q=r=9 - # ╔═╡ b57da300-58b5-498b-9798-c565dbb74026 f(g((args...; kwarg1=dict[index], karg2=kwarg2) -> X)) @@ -335,6 +335,14 @@ md""" """) +# ╔═╡ 62b805db-2028-40b0-b412-4c91d15ad338 +c = d = e = f= g =h=i=j=k=l=m=n=o=p=q=r=9 + +# ╔═╡ 5b62306f-c730-42f0-85dd-8692ca8b50fa +function f(by, y; by=213, beeyt) + sdf +end + # ╔═╡ a35574d7-c7eb-4ffa-b8ef-5e8aef7aa3e9 begin function ff(a, b=2; c=2, d) @@ -352,6 +360,8 @@ end # ╔═╡ Cell order: # ╟─d0ee4518-406a-4628-ac4a-30d5fa11f5a7 # ╠═e746ef9c-47c1-4d5d-bce1-4616f25ff586 +# ╠═e45548dc-cf3c-4ddd-be78-e2776380f172 +# ╠═5b62306f-c730-42f0-85dd-8692ca8b50fa # ╠═b57da300-58b5-498b-9798-c565dbb74026 # ╠═69acb688-3726-4252-b7e2-f496181d2aa6 # ╠═5666ac41-9e8d-40fc-89be-e2a6c1637da5 From a8fb3f3eaabafde4214a51b18c9057a4d5df37f9 Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Fri, 17 Jan 2025 23:56:22 +0100 Subject: [PATCH 14/14] version pin --- frontend/imports/CodemirrorPlutoSetup.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/imports/CodemirrorPlutoSetup.js b/frontend/imports/CodemirrorPlutoSetup.js index eb28ce165a..090fea9db0 100644 --- a/frontend/imports/CodemirrorPlutoSetup.js +++ b/frontend/imports/CodemirrorPlutoSetup.js @@ -65,4 +65,4 @@ export { linter, setDiagnostics, //@ts-ignore -} from "https://cdn.jsdelivr.net/gh/JuliaPluto/codemirror-pluto-setup@35aa48a/dist/index.es.min.js" +} from "https://cdn.jsdelivr.net/gh/JuliaPluto/codemirror-pluto-setup@2000.0.0/dist/index.es.min.js"