Forest: Structural Code Editing with Multiple Cursors

44
ETH Library Forest: Structural Code Editing with Multiple Cursors Master Thesis Author(s): Voinov, Philippe Publication date: 2022-01-12 Permanent link: https://doi.org/10.3929/ethz-b-000526812 Rights / license: In Copyright - Non-Commercial Use Permitted This page was generated automatically upon download from the ETH Zurich Research Collection . For more information, please consult the Terms of use .

Transcript of Forest: Structural Code Editing with Multiple Cursors

ETH Library

Forest: Structural Code Editingwith Multiple Cursors

Master Thesis

Author(s):Voinov, Philippe

Publication date:2022-01-12

Permanent link:https://doi.org/10.3929/ethz-b-000526812

Rights / license:In Copyright - Non-Commercial Use Permitted

This page was generated automatically upon download from the ETH Zurich Research Collection.For more information, please consult the Terms of use.

Forest: Structural Code Editing withMultiple Cursors

Master Thesis

Philippe Voinov

January 12, 2022

Advisors: Dr. Manuel Rigger, Prof. Dr. Zhendong Su

Department of Computer Science, ETH Zurich

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

AbstractSoftware developers sometimes have to repeat an editin multiple parts of their codebase in order to main-tain or extend their software. To let the user perform arepetitive edit, text editors provide multi-cursor editing,which applies editing commands in multiple locationssimultaneously. However, multi-cursor text editing islimited, since each executed command must make thedesired change regardless of which cursor executes it.To perform a wider range of repeated edits, some de-velopers write refactoring scripts that work structurally.However, unlike multi-cursor editing, the process ofdeveloping a refactoring script is not interactive. Wepropose Forest, the first editor which specially inte-grates structural editing commands with multi-cursorediting. Compared to a text editor, Forest offers differ-ent editing commands, and therefore supports differentmulti-cursor edits. Forest allows performing edits simi-lar to those from refactoring scripts, while still beinginteractive. We attempted to perform edits from 48real-world refactoring scripts using Forest and foundthat 11 were possible, while another 17 would be possi-ble with added features. Additionally, we investigateda real-world codebase, finding that ∼15% of commitscontained edits with potential for multi-cursor editing.

1 IntroductionWhen maintaining and extending software, developersare sometimes forced to make repetitive edits. Figure 1shows such an edit. The goal of the developer was towrap every property value (the expressions to the rightof : within the object literal { ... }) in a function call(warnOnError) directly, rather than using a loop to wrapthe values at runtime. This change was made in orderto later add a new property which, unlike the others,must be wrapped in ignoreError.

Wrapping every property value in Figure 1 cannotbe performed effectively with regular expressions or amulti-cursor text editor [10] (where each text editingoperation is performed in multiple locations simultane-ously), because the task is not textual in nature. Select-ing every property is especially difficult, because theyspan varying numbers of lines and the property separa-tor (,) appears deep inside the values of some proper-ties. The developer could write a refactoring script [2]that transforms the Abstract Syntax Tree (AST) of theprogram. This approach avoids text-related issues likeidentifying separators since it fits the structural natureof the task. However, writing a refactoring script is un-reasonably heavyweight for an edit that is required inso few locations. This edit is also too program-specificto be available as a built-in refactor in an Integrated

Development Environment (IDE). Programming-by-demonstration tools [18] [26] [18], which infer a struc-tural edit based on an example in one location, are amuch more lightweight solution than writing a cus-tom refactoring script. However, such tools are not yetwidely used and may not always perform the desirededit.

We propose to combine multi-cursor editing with thecommands commonly found in structural code editors.We believe that this combination is particularly wellsuited for edits like the one described above. Structuralediting commands are designed for edits that are struc-tural in nature — the kind that are difficult to capturewith regular expressions and text editors. Using suchcommands the developer directly and exactly describesedits — unlike giving examples which programming-by-demonstration tools may misinterpret. Simultane-ously performing the edit with multiple cursors pro-vides a quick and interactive prototyping experience —a much more lightweight process than creating refac-toring scripts.

Our prototype editor Forest (Figure 2) is a multi-cursor structural editor for TypeScript. In Forest, cur-sors point to AST nodes. The user navigates within theAST by using operations like “move to parent”. Inser-tions are performed by parsing typed text. A cursorcan be split (e.g. for each child of the selected ASTnode) to create multiple cursors that handle editingcommands simultaneously. A novel hierarchy of cursorsconcept makes it practical to work with cursors thatwere split multiple times.

Both multi-cursor editing and structural editing arenot new concepts. Multi-cursor editing became wide-spread in text editors around a decade ago, and manyearly structural editors were designed in the 1980s.However, as far as we are aware, there is only one struc-tural editor with multi-cursor support [16]. That edi-tor has no special handling beyond broadcasting com-mands independently to each cursor. Forest has specialintegration between structural editing commands andmultiple cursors.

To understand the strengths and weaknesses of multi-cursor structural editing, we performed two distinctevaluations. First, we collected real-world AST-basedrefactoring scripts and attempted to perform the corre-sponding edits interactively in Forest. Of the 48 edits, 11could be performed without any significant issues, anda further 17 would likely become practical with someimprovements to the editor. This shows that develop-ers could avoid writing some (but not all) refactoringscripts and instead use a multi-cursor structural editorto perform their edit interactively. Second, we analyzedreal-world commits to understand how often multi-cursor editing could be especially effective. Of the 913

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

export const handlers = {

"ctrl-v": node.actions.setVariant

? tryAction("setVariant", n => n.id, true)

: tryAction("setFromString"),

"ctrl-d": tryDeleteChild,

"ctrl-c": () => copyNode(node),

"ctrl-p": tryAction("replace", n => n.id),

"ctrl-f": editFlags,

"ctrl-4": () =>

setMarks({ ...marks, TODO: f(parentIndexEntry) }),

};

for (const [k, v] of handlers) {

handlers[k] = warnOnError(v);

}

export const handlers = {

"ctrl-v": warnOnError(

node.actions.setVariant

? tryAction("setVariant", n => n.id, true)

: tryAction("setFromString"),

),

"ctrl-d": warnOnError(tryDeleteChild),

"ctrl-c": warnOnError(() => copyNode(node)),

"ctrl-p": warnOnError(tryAction("replace", n => n.id)),

"ctrl-f": warnOnError(editFlags),

"ctrl-4": warnOnError(() =>

setMarks({ ...marks, TODO: f(parentIndexEntry) }),

),

"ctrl-x": ignoreError(cut),

};

Figure 1. The diff of an edit which is structural in nature, specific to a given program, and repeated in a smallnumber of locations. Each property is directly wrapped in a call to “warnOnError”, instead of using a loop. Then anew non-wrapped property is added. See Example A.9 for a step-by-step demonstration of this edit in Forest.

commits that we inspected, 142 (∼15%) contained editswhere multi-cursor support could be useful. We furtherclassified these edits and found that around 30% wereprogram-specific structural transformations, which webelieve multi-cursor structural editors are particularlywell suited to.

Figure 2. A screenshot of Forest, our multi-cursor struc-tural editor prototype, editing one of its own sourcefiles. For a description of the basic features of this pro-totype, see Section 3. This screenshot shows two activecursors. Multi-cursor editing is described in Section 4.

In Section 2, we give an overview of various ex-isting editing tools. In Section 3, we describe Forest(our structural code editor prototype), but without anymulti-cursor related features. The design described inthat section combines elements that already exist invarious other structural editors. This section is mainlyintended to give enough context to understand themulti-cursor examples. Section 4 introduces Forest’smulti-cursor features, motivating most features with ex-amples. In Section 5, we evaluate Forest by comparingit to AST-based refactoring scripts. Section 6 containsan alternative evaluation by analyzing real-world edits.

We make three main contributions:• We built Forest — a structural editor prototype

for TypeScript. Forest is one of very few struc-tural editors for modern languages with complexsyntax.

• We extended Forest to support multiple cursors,with special integration between multi-cursorediting and structural editing. This is the onlymulti-cursor structural editor with such integra-tion.

• We evaluated how Forest compares to refactoringscripts and how it handles real-world editingtasks. This is the first evaluation of a multi-cursorstructural editor.

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

2 BackgroundThis section gives an overview of existing structuralcode editors, as well as existing approaches for per-forming repetitive code edits.

2.1 Existing Structural Code EditorsStructural code editors (also called structure editors or pro-jectional editors) are editors in which the navigation andediting commands operate on a tree representation ofa program, rather than allowing the user to arbitrarilymodify the text of a source file.

Early Structural Editors. Structural editors have along history, with some created as early as the 1970s(Emily [17]), and many more created around the 1980s(Mentor [11], Cornell program synthesizer [31], GAN-DALF [12], Syned [14], Lispedit [22], Poe [13]). Theseprojects made some assumptions that no longer holdin today’s environment. Many older structural editorswere designed for Pascal, which has a simpler syntaxcompared to TypeScript or other modern languages.We believe that complex syntax makes some designsimpractical, for example having a menu of AST nodesthat can be inserted, or showing placeholders for all op-tional children. Designing for acceptable performancewas a major constraint for old editors, but today’s morepowerful computers require much less focus on perfor-mance and make new designs possible. Additionally,many features that motivated old structural editorsare now commonplace in our structure-aware text edi-tors. Some examples are scope-aware auto-completion,jump-to-definition, refactors for extracting and movingcode, and continuous type checking.

Newer Structural Editors. Since the 1980s there hasbeen less active development of structural editors. Somenotable newer projects are MPS [1] (usually Java as baselanguage; language workbench), Envision [4] (a sub-set of Java; focus on visualization), Hazel [27] (customfunctional language), Lamdu [8] (custom functional lan-guage), and GopCaml [16] (OCaml; plugin for Emacs).MPS is by far the most widely used structural editor,yet it is almost completely unknown compared to texteditors. The fact that MPS is a language workbench canadd complexity for users, for example when workingwith variable references [5]. GopCaml is the closest toour work in that it fully supports OCaml, which is awidely known language with complex syntax.

2.2 Multi-cursor EditingMulti-cursor editing (also called multiple selection orsimultaneous editing) is a feature in text editors whichlets the user create more than one cursor/selection ina document. In response to a user’s command (e.g.,

pressing backspace) every cursor moves and edits si-multaneously. It was first described in [24] togetherwith a feature for inferring cursors, which we ignorehere. Sublime Text (2008) seems to be the first widelyused editor with a multi-cursor feature. Since its release,this has become a standard feature in most commoncode editors (e.g., Ace, VScode, emacs with a plugin,IntelliJ, and Notepad++).

Combination with Structural Editing. Importantly,multi-cursor editing became widespread much laterthan the peak of interest in structural editors. This islikely why these two ideas have not been investigated incombination. Note that although GopCaml [16] can becombined with multiple-cursors for Emacs [30], thereis almost no special handling or discussion for thiscombination.

2.3 KakouneKakoune [10] is a unique editor which combines multi-cursor as a central primitive with a large set of textmanipulation commands. It has some similarities tostructural editors while still being a text editor. In Kak-oune the user always has at least one selection andsuch selections are always a text range, not a text loca-tion. The user can widen, narrow or split the selection,often using regular expressions. This can be used tonavigate in a similar way to moving up and down atree in a structural editor. Although, since Kakouneitself has no understanding of programming languagesyntax, it is not a structural editor. The rich set of com-mands in Kakoune allows much more complex editsthan in other multi-cursor text editors. By issuing mul-tiple commands that include regular expressions, aKakoune user can achieve similar edits to structuralregular expressions [28].

2.4 MacrosMacros can often be used to achieve similar results tomulti-cursor editing. Although multi-cursor editing isrelatively new, macros are a feature even in older texteditors like Emacs and Vi. Some structural editors havemacro support (e.g., Mentor procedures [20]). Macrosare either recordings of keypresses that trigger editorcommands or programs in a scripting language thatcan control the editor. The main advantage of multi-cursor editing compared to recorded macros is thatmulti-cursor editing shows the effect of every edit inevery location simultaneously, but a recorded macroonly shows edits in one location while recording. Script-like macros are discussed in Section 2.7.

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

2.5 AST Transformation ScriptsTo perform precise large scale code changes it is pos-sible to write scripts that operate on abstract syntaxtrees. This can either be done using specialized trans-formation languages such as TXL [9], or using general-purpose languages with compiler libraries for parsingand printing. JavaScript developers generally use js-codeshift [2] or the TypeScript compiler API for creat-ing such scripts. An example of such transformationscripts are the React codemod scripts [3]. Such ASTtransformation scripts are a heavyweight approach —they require specialized knowledge and are not createdin an interactive editor. We believe that writing suchscripts is tedious, and may take longer than performingthe edits manually if the number of edited locations issmall.

2.6 Programming by DemonstrationProgramming by demonstration tools are another op-tion for performing repetitive code changes. The userperforms the desired change in a few locations andthe system finds more similar changes and recom-mends equivalent edits. Examples of such systems areSydit [21] and reCode [26]. Programming by demon-stration systems abstract edits from concrete examples,while in multi-cursor systems the user directly per-forms edits with sufficiently abstract commands.

2.7 Scripts vs EditorsTo make large scale repetitive changes to text files, aprogrammer may choose to write a small program toperform the changes. Such text editing scripts are oftenwritten in scripting languages (e.g., Bash or Python)and contain operations like regular expression searches,string splitting, or loops. However, for most editingtasks, programmers will perform these edits in a nor-mal text editor instead of writing scripts. Some texteditors (e.g., vim) can perform many of the operationsused in text editing scripts, especially when combinedwith macros (Section 2.4). Such text editors could beseen as the interactive equivalent to text editing scripts.Analogously, we believe that multi-cursor structuraleditors are the interactive equivalent of AST transfor-mation scripts (Section 2.5). This idea is investigated inSection 5.

3 ForestThis section describes Forest, but without any of itsfeatures related to multi-cursor editing. Our main con-ceptual contributions are concerned with multi-cursorediting, which is discussed separately in Section 4. Sec-tion 3.1 is intended to give sufficient background tounderstand Section 4. Section 3.2 discusses details of

Forest’s design, which are not closely related to multi-cursor editing.

3.1 Overview of DesignSource files in Forest are shown as pretty-printed text.This text is synchronized with a Forest-specific AST(referred to as “tree” from now on) which the usercan interact with. Forest is a modal editor, with sep-arate normal and insert modes. A full list of availablecommands is given in Section A.1 of the appendix.

Navigation. The user controls a cursor that alwayshas part of the source file selected. This selection mustcorrespond to a contiguous selection of siblings in thetree. Forest provides structural navigation commands(e.g., “go to parent” or “select next leaf node”) forchanging the selection. The navigation commands treatevery node in the tree as a list of children in text order.This makes it possible to navigate through any nodeusing the same commands.

Insertion. To add to the source file, the user entersinsert mode at the text location at the start (or end) oftheir selection, types normal source code, and exitsinsert mode. It is only possible to exit insert modewhen the source file has valid syntax. Insertions cannot change existing code.

Modification. Existing code can be modified usingthe delete, copy and paste commands. These commandsare structural: they modify the tree (not the text) di-rectly, after which the modified tree is pretty-printedand shown to the user. Deleting a node may result ina tree that does not correspond to a valid TypeScriptAST (e.g., deleting b in a + b). In this case, Forest addsa placeholder (effectively a hole) instead of the deleteditem.

AST. Most nodes in the Forest tree correspond ex-actly to the equivalent TypeScript AST node. However,for nodes where the TypeScript AST is very inconve-nient to edit, Forest uses a different structure. For exam-ple chains of property accesses and calls(this.data.filter(...).map(...)) are a left-associativetree in TypeScript, but a single flat list in Forest (this,data, filter, (...), map, (...)).

3.2 Reasoning Behind Design3.2.1 Text Display. Forest displays code as text, likeevery text editor and most other structural editors. Webelieve that text is an intuitive and compact way todisplay programs. Although it would be possible to vi-sualize the AST more graphically, as we did in an earlyprototype of Forest, we feel that too much navigationis required to read code in such a format, because it isless compact than text.

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

3.2.2 Text Insertion. In Forest, the user inserts codeby typing text that is then parsed. We believe that thistext-based insertion is the most practical approach, es-pecially for inserting expressions. Our early prototypeused a command-based insertion design, where theuser had to explicitly build the AST with menus andshortcuts. This led to numerous issues that we dis-cuss in this section, in order to argue in favor of atext-insertion approach.

Insertion Order Issue. A major issue with command-based insertion is that it encourages performing inser-tions “inside-out”, which is quite unnatural and diffi-cult [23] [19]. Although we tried to compensate for thisissue by adding convenient shortcuts and navigationfeatures, we could not make a satisfactory prototypewith this insertion design.

Too Many AST Nodes. A further major issue withcommand-based insertion is that TypeScript has manytypes of AST nodes (over 100, excluding keywords andtokens). This especially caused problems for noviceusers who tried our prototypes. They knew which syn-tax they wanted, but could not find the correspondingAST nodes in the menus, because they did not knowtheir names. Even after developing and extensively test-ing this early prototype, the authors still occasionallyhad to pause to think about the AST node they neededand remember its shortcut. We believe that command-based insertion may have been less problematic in earlystructural editors [17] [15], as their target languageshad fewer AST nodes.

Syntax as a Command Language. An argument infavor of text insertion is that a language’s syntax nat-urally corresponds to an efficient set of insertion com-mands. For example, typing console.log(x); could beinterpreted as:

• console — create the identifier console

• . — wrap the preceding expression in a propertyaccess and focus the right-hand side

• log — create the identifier log

• ( — wrap the preceding expression in a call ex-pression and focus the argument list

• x — create the identifier x

• ) — focus the parent expression or statement• ; — append a new empty statement to the parent

and focus it

With this point of view, it is not clear that there is acustom set of shortcuts for command-based insertionthat would be more efficient than text insertion.

3.2.3 Selections. The behavior of selections in Forestis heavily influenced by the ABC editor [19]. A selection(the focus) is one or more nodes in the tree, which must

be contiguous siblings. Any selections with the sametext range are considered equivalent.

Equivalent Selections. We will use the TypeReferenceNode

from the TypeScript AST to demonstrate equivalent se-lections. This node is used to refer to a previouslydeclared type. It contains an Identifier (the referencedtype) and an optional list of type parameters. Depend-ing on whether the type parameter list is used, aTypeReferenceNode looks like MyGenericType<T> orMyNonGenericType. Note that when the type parameterlist is not used, selecting the TypeReferenceNode wouldhave the same text range as selecting its inner Identifier.Forest treats selections with equal text range as one,meaning it is ambiguous which AST node is selected.However, any command that is applied will effectivelydisambiguate the selection. In our example, if “move toparent” is performed, then the selection will move tothe parent of the TypeReferenceNode, but if “rename” isperformed, then the command will affect the Identifier.By not allowing the user to make two different selec-tions with the same text range, we hope to minimizeconfusion about what is selected. This also eases navi-gation, since the tree (as perceived by the user) is lessdeep.

Selections and Text Insertion. The design of equiv-alent selections in Forest is very closely tied to textinsertion, since text insertion effectively disambiguatesequivalent selections. Consider the function call f(x.y)with the argument x.y focused. It is ambiguous whetherthe argument x.y itself or the list of arguments to f (con-taining the single argument x.y) is focused. If we hadused a non-text-based command for inserting list items(like “append item to list” followed by a menu selec-tion), we would need a way to disambiguate which listthe user wishes to append to. However, a text insertionnaturally disambiguates this: If the user chooses to ap-pend .z (beginning with a period), it is clear that theyare appending a property access to the argument itself.If they choose to append ,z (beginning with a comma),it is clear that they are appending a new argument tothe argument list of f.

3.2.4 AST Adjustments. Most types of syntax are rep-resented in the Forest tree the same way as in theTypeScript AST. However, some nodes are handleddifferently to make interactive editing more practical.

Loose and Tight Expressions. We convert typical ex-pressions to two levels of flat lists, which we call looseexpressions (whose elements are generally separated byspaces) and tight expressions (generally printed withoutwhitespace). For example, a.x() + f.g * c is a looseexpression containing a.x(), +, f.g, *, and c. Similarly,a.x() is tight expression containing a, x, and (). Having

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

most expressions be flat lists (instead of deeply nestedbinary trees) eases navigation. The separation of looseand tight expressions seems intuitive and predictableto us, but we have not investigated this design choicewith users.

Operator Precedence. Forest’s adjusted tree with flat-tened lists loses precedence information. This is doneintentionally to simplify navigation. It also allows re-placing operators in a way that ignores precedence,like in a text editor. Flattening expressions around bi-nary operators and ignoring precedence is the sameapproach taken by the ABC editor.

Typical TypeScript Edits. Having expressions flat-tened at the tight expression level is helpful for editsthat commonly appear when writing TypeScript. Forexample, consider the following expression:

myArray.map(x => x + 1). filter(x => x < 0)

Since Forest interprets this expression with a flat struc-ture (myArray, map, (x => ...), filter, (x => ...)) theuser to easily perform the following operations:

• Navigate through the chained calls (without imag-ining the binary tree)

• Remove calls (even from the middle of the chain,e.g., map(...))

• Add new identifiers to form property accesses(e.g., prepend this. to the whole chain)

• Add or remove the function calls themselves (e.g.,add join to the end of the expression, resultingin an unevaluated function, then later add themissing (), resulting in a string)

• Add inline operators like ?. (optional chaining)(e.g., add ?. after myArray in case it is undefined)

3.2.5 Navigation. Forest has a relatively small set ofnavigation commands, most of which are directional ina tree sense (e.g., going up towards the parent, or goingto the next sibling). None of the navigation commandsdepend on the way the code is formatted.

Dependency on Formatting. The decision to avoidcommands which depend on formatting was early inthe development process of Forest. It was motivated bythe annoyance of having to use different commands toperform edits in vim depending on how the code waspretty-printed. For example, to delete a function argu-ment one would use “delete up to including comma”(not last argument), “delete to closing parenthesis” (lastargument), or “delete line” (formatting with one argu-ment per line). Having navigation be completely inde-pendent of pretty-printing introduces limitations (forexample, moving down by a few lines is not possible ingeneral), but it also allows for special integration withthe pretty-printer.

Continuous Pretty-Printing. The text code displayedin Forest is pretty-printed after every single edit. How-ever, since navigation does not depend on this print,it would be possible to run the pretty-printer asyn-chronously. For example, the user could perform aninsertion and then start navigating, while the editorperforms a pretty-print in the background (withoutblocking the user’s navigation), and eventually showsthe new print. This would not be possible if Forest hadcommands like “delete line”, because if the user issuesthis command right before the editor switches to a newprint, the meaning of the command would change andmight not do what the user intended.

3.2.6 Empty Selections. Forest does not allow emptyselections, except in the following case: Consider thefunction call f(x, y) and a selection inside the argu-ment list (covering x, y). The user can append a func-tion argument using the “insert text after cursor” com-mand. However, if the function call had no arguments,then the user could not focus the argument list, since itwould be an empty selection. To make the situation thesame with no arguments as with some arguments, weallow selecting the content of the empty parenthesizedlists.

3.2.7 Deletion and Placeholders. Because the treethat the user edits in Forest is slightly different fromthe TypeScript AST (Section 3.2.4), deleting a node maymake Forest’s tree no longer correspond to a validTypeScript AST. For example, f() is a tight expressionconsisting of f and () in Forest. Deleting f is reason-able (e.g., to replace it with another identifier), butthe remainder () does not correspond to a valid Type-Script AST for an expression. In this case, Forest insertsthe identifier placeholder in place of the deleted iden-tifier. This allows all existing tooling (the TypeScriptparser and any pretty-printers) to work normally. For-est tracks the fact that this node is a placeholder acrossedits and pretty-printing, so that special highlightingand behavior can be provided for placeholders. Forest’splaceholders are effectively holes.

3.2.8 Copy and Paste. The paste command in Forestreplaces the current selection by the copied tree nodes.This was the only simple option, because of problematicinteractions with equivalent selections (Section 3.2.3).Since it is sometimes ambiguous which list a user hasselected, commands like “paste before/after cursor”would need additional hints about which list to pasteinto. Although we believe that paste before/after couldbe slightly more user-friendly, we decided that it was ac-ceptable for our prototype to only allow replacement.

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

4 Multi-Cursor EditingThis Section describes how multi-cursor editing is per-formed in Forest. We describe the features that Forestprovides for multi-cursor editing and motivate themwith step-by-step examples. A full list of multi-cursorediting commands is given in Section A.1 of the appen-dix.

4.1 BasicsMulti-cursor editing is a feature of most text editors.However, as far as we are aware, there is only oneexisting structural editor with any multi-cursor support[16]. The basic usage of multi-cursor editing is similaracross all editors (both textual and structural). Insteadof having a single cursor (with a position or range in thetext or tree), the user can create multiple cursors. Oncethese cursors are created, any editing command issuedby the user is applied to every cursor simultaneously.This section describes the same cursor-creation andcommand-issuing process as it applies to Forest.

4.1.1 Creating Multiple Cursors.

Manually. The most direct way to get multiple cur-sors is to manually mark where they should be created.In most multi-cursor text editors this is done by click-ing the desired locations with a modifier key. In Forest,the user moves the cursor to the desired selection and“queues” it with q . After all desired locations havebeen queued, the user can switch to the queued set ofcursors using + q .

With Search. Another widely supported way of cre-ating multiple cursors is to create a cursor at eachoccurrence of a search result. Forest supports this, butit is discussed separately in Section 4.3.3.

Splitting. Some editors (notably Kakoune) supportsplitting the current selection into multiple selections.In Kakoune, the user supplies a regular expressionthat describes the separator text. In Forest, a selectioncontaining multiple nodes can be split using s . Theuser does not need to supply any further information,because the tree naturally defines how nodes shouldbe separated. This is especially powerful for splittingcomplex nested expressions, since separators in theinner expressions are perfectly ignored by the structuralsplit operation, but would be difficult or impossible toignore using regular expressions.

4.1.2 Relaxed Mode. In multi-cursor editors, eachmovement or editing command performed by the useris executed by every cursor simultaneously. Note thatthe clipboard (and sometimes other editor state) is gen-erally stored per cursor.

Failing Cursors. If some cursor can not handle acommand (e.g., move one character to the right, butthe cursor is already at the end of the document), nospecial handling is performed. The single failing cursorwill simply do nothing, while the other cursors willstill process the command. This “broadcast commandand ignore failures” approach is called relaxed mode inForest. Unlike other editors, Forest has a few alterna-tive multi-cursor modes which give different behavior(Section 4.3.4).

4.2 Relationship to Textual Multi-cursorAs described in Section 2.2 most text editors have multi-cursor support. Multi-cursor in a structural editor is notfundamentally different, but it is arguably more power-ful, because the power of multi-cursor depends on thesingle cursor editing commands that are available.

Example. Consider a minimal text editor where theonly operations are move cursor by one character, deletecharacter, and insert character. It would generally only bepossible to edit two code snippets using multi-cursor insuch an editor if the code had the same structure, thecode was formatted identically, and matching literalshad the same length. Since most text editors extendthis minimal model with commands like move cursorby one word and move cursor to end of line, matchingliterals generally don’t need to have the same lengthand minor code formatting differences can be tolerated.Since structural editors add structural movement andediting commands, they can tolerate most formattingdifferences. Generally, the only requirement to edit twocode snippets using a multi-cursor structural editor isthat they have a similar structure.

4.3 Features and ChallengesIn this section, we describe the features in Forest whichare either specific to multi-cursor editing or are espe-cially useful together with multi-cursor. Most of thesewill be motivated by an example editing task. Forest in-troduces multiple novel features including a hierarchyof cursors and marks which track AST nodes.

4.3.1 Hierarchy of Cursors. After creating multiplecursors, it is eventually necessary to switch back to asingle cursor. In most text editors one of the cursors isdesignated as the primary cursor and there is a com-mand to delete all other cursors. Forest instead hasa command to reduce to the first/last cursor by locationin the document (remove all cursors except this one;for keeping inner/outer cursors see Section 4.3.6). Thiscommand interacts in a special way with Forest’s novelconcept of a hierarchy of cursors.

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

Motivation. In Example A.2 the user wants to addtype annotations to each function argument and thenadd a return type annotation to the function itself. Ifthey only wanted to do this for one function, they couldsplit the cursor to have one cursor per function param-eter, annotate the parameter types, reduce to the firstcursor, and finally annotate the function return type. Anatural way to perform this task for both functions isto first split the cursor to have one cursor per function,then perform the rest of the steps as for a single func-tion. However, this does not work if reduce to first cursorwould truly leave only the first cursor, since then onlythe first function would get a return type annotation.

Principle. Forest’s hierarchy of cursors solves the issuepresented above. As far as we know, this concept iscompletely novel. When a user splits a cursor intonew cursors, the lineage of these cursors is tracked,effectively organizing them into a tree. When the userperforms an operation like reduce to first cursor, theoperation is performed per group of cursors, where allcursors with the same parent (cursor which was splitto create these cursors) are part of the same group. InExample A.2, reduce to first cursor (Step 5) leaves thecursors on the first function argument of each function.

Multiple Levels. The hierarchy of cursors can be ar-bitrarily deep. The above example could be extendedto split at three levels (classes, methods, and parame-ters). Then the first invocation of reduce to first cursorwould leave the first cursor per method and class andthe second invocation would leave the first cursor perclass.

4.3.2 Marks. Marks are a feature that is available insome text editors (e.g., Vim). The user can create a markat the current cursor position and later jump back tothis mark. In Forest marks store a selection, as cur-sors have tree selections and not character positions.Example A.3 shows how marks are typically used inForest.

Motivation. Marks are useful but not necessary witha single cursor, since it is always possible to manuallymove the cursor back to its old location. With multiplecursors, it is not always possible to manually moveall cursors back to their old locations, since each cur-sor might need slightly different commands to movethere, but commands are broadcast to all cursors. How-ever, using marks all cursors can be moved back atonce, since they all need the same jump back to markcommand.

Persistence. Marks in Forest persist across edits andpretty-printing, always remaining on the same ASTnodes. This allows marks to be used extensively during

editing, which is very common in practice. No existingtext editors or structural editors that we know of havemarks that track AST nodes.

Separating Cursors. Each cursor has its own set ofmarks. The fact that marks are part of a cursor’s stateis what allows cursors to have equal selections (seeSection 4.3.5), but still be separated later.

4.3.3 Structural Search. Structural search is a fea-ture in many structure editors. It is also part of somestandalone structural find-replace tools [6]. Almost nowidely used text-based editing environments (exceptIntelliJ) have integrated structural search.

Support in Forest. Forest has very basic structuralsearch support. The user writes their query in a top-level source file (so that it can be unambiguously parsed)and selects a tree node to search for (e.g., an object lit-eral property). By default, all results must deeply matchthe query node. To widen their search, the user canforce shallow matching on certain subtrees of the querynode and use regular expressions to match identifiers.

With Multiple Cursors. Structural search is performedindependently for each cursor and always searcheswithin the current selection. If there are multiple re-sults within a selection, that cursor is split into multiplecursors. The split created by searching maintains thehierarchy of cursors (Section 4.3.1) the same way thatthe normal split command would.

Uses. The most common use for structural search isto define the initial set of cursors, for example by find-ing every call to a specific function. Search results maybe nested, the consequences of which are describedin Section 4.3.6. Structural search can also be to filtercursors, as described in Section 4.3.4.

4.3.4 Filtering Cursors. For some tasks exactly select-ing the locations to edit is quite complicated. In Ex-ample A.8 the user must find all calls to Object.assign

where the first argument is an object literal and none ofthe other arguments are spread elements (...x). Manystructural search systems will not be able to capturethis in a single query, especially not in an easy-to-understand way.

Overview. In Forest, the user performs queries likethis using multiple search and navigation steps in se-quence. First, a structural search (or direct cursor splitas in Step 2) gives all calls to Object.assign. The userthen navigates to the first argument and drops any cur-sors where this argument is not an object literal (Steps3 and 4). Finally, the user selects all arguments exceptthe first and drops any calls where these argumentscontain spread elements (Steps 5 and 6). The concrete

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

mechanisms which are available to the user to performsuch filtering are explained in the rest of this section.

Drop Mode. An implicit way to filter cursors in Forestis the multi-cursor drop mode. After issuing a commandin this mode, any failed cursors (those which can notexecute the command) are deleted. Examples of failingcommands are a structural search with no results, jumpto surrounding parentheses when none exist, and reduceto first list item when the selection is an empty list. Inthe Object.assign example navigating to the first argu-ment in multi-cursor drop mode (not done in ExampleA.8) would be enough to remove any cursors whereObject.assign is called with no arguments, since reduceto first list item would fail.

Shallow Search. Multi-cursor drop mode can also beused to handle the condition “where the first argumentis an object literal” by using structural search (Step 3).However, selecting the first argument, searching for anobject literal, and dropping failed cursors does not pre-cisely capture this condition. This method would keepcursors that contain an object literal somewhere deep in-side the first argument, for example, Object.assign(f({})).Forest has an option to use structural search to checkwhether the top level of the selection matches a query,without searching deeply within the selection. With thisoption the check in this example is precise. If the selec-tion matches, the command succeeds without movingthe cursor. If the selection does not match, the com-mand fails (which causes the cursor to be deleted inmulti-cursor drop mode).

Limitation of Drop Mode. The main limitation ofmulti-cursor drop mode is that the condition which itchecks cannot be inverted. For example, the condition“none of the other arguments are spread elements” canbe expressed by keeping all cursors where a search forspread elements fails. This is the opposite of the behav-ior that multi-cursor drop mode has. Conditions likethis can be expressed with Forest’s explicit branchingcommand which is typically used in multi-cursor strictmode.

Strict Mode. When a command is issued in multi-cursor strict mode, if any cursor would fail the com-mand, then the command will not be performed atall (even for cursors where it would be possible) andeach cursor will be marked succeeded or failed (visu-alized in Steps 3 and 5) . Using the explicit branchingcommand the user can now either keep the cursorsthat would have succeeded (Step 4) or the ones thatwould have failed (Step 6). Keeping successful cursorsis equivalent to using multi-cursor drop mode. Keep-ing failed cursors is only possible using this explicitbranching command.

4.3.5 Overlapping Cursors. It is not immediately clearwhat the benefits of allowing overlapping cursors are.In this section, we will consider different kinds of over-lap, show how Forest handles them, and discuss whenthey can be useful.

Nested Cursors. Consider the code snippet f(g(x))

with one cursor selecting the whole call to f and an-other selecting the whole call to g. These cursors arenested (one strictly contains the other). This kind ofoverlap is discussed in Section 4.3.6. We call all otherarrangements of cursors non-nested.

Duplicate Cursors. A special case of non-nested cur-sors is duplicate cursors. This is the situation wheremultiple cursors have the same selection range, for ex-ample when 2 cursors both have the whole call to f

selected. Duplicate cursors can be separated by jump-ing to marks (Section 4.3.2). However, duplicate cur-sors can also be used directly to perform insertions, inwhich case the typed text is inserted once for each cursor.Duplicate cursors are ordered against each other basedon the order they had before they became duplicate.This makes it predictable which instance of the newlyinserted text belongs to which duplicate cursor.

Example with Duplicate Cursors. Both interactionswith duplicate cursors (separate using marks and in-sert) are demonstrated in Example A.4. The user cre-ates a cursor for each variable declaration and thenperforms move to parent (Step 4), which results in 2 or3 duplicate cursors on each variable declaration list.They now perform an insertion (Step 5), which createsa new statement for each duplicate cursor, thereby alsoseparating them. Later in the example (Step 7) the userhas duplicate cursors on the var/let/const keyword,which they separate using marks.

Non-nested Non-duplicate Cursors. The arrangementof overlapping cursors that remains to discuss is non-nested non-duplicate cursors, as in the following ex-ample: Given the snippet [a, b, c], one cursor selectsa, b and one cursor selects b, c. Cursors in this ar-rangement are generally not useful. Commands likepaste (a replacement) would be ambiguous. The onlynon-movement command that Forest supports in thisarrangement is delete, which is done by removing anode exactly when it is contained in at least one selec-tion.

4.3.6 Nested Cursors. Nested cursors are a specialcase of overlapping cursors (Section 4.3.5). They aretypically created by searching for structures that can benested (e.g., functions or object literals). Nested cursorspresent a unique challenge for multi-cursor structuralediting.

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

Nested Copy-Paste Problem. Forest supports all com-mands (e.g., insert, paste, delete) with nested cursors.Even with this support, it is still not possible to performcertain edits with nested cursors as shown in Exam-ples A.5 and A.6. To convert an individual functionexpression to an arrow function the user could copythe function body, insert an arrow function, paste overits body, and delete the function expression. With mul-tiple non-overlapping cursors, the procedure is exactlythe same (Example A.5). However, this approach doesnot work with nested cursors (Example A.6). Since thecopy command copies the function bodies before anyedit operations, the final result is a converted versionof the outer function, but all function expressions con-tained within are still unconverted. While the innercursors do still exist and make edits, they remain in thebody of the old outer function expression that eventu-ally gets deleted. The core problem is that if a cursorcopies a region containing other cursors, any futureedits made by those cursors will not affect the copy.

Workaround. To manually work around the problem,the user could first edit with the innermost cursors,then repeat the process with the next level of surround-ing cursors (Example A.7). This requires repeating thewhole edit as many times as the deepest level of nest-ing (3 in our example). Forest provides a command toremove all cursors except the outermost/innermost ones, sothat at least the user does not have to manually selectthe cursors for each level. In Section 7.1.1 we proposechanges to Forest so that such a workaround would nolonger be required.

5 Evaluation against Refactoring ScriptsIn Section 2.7, we proposed multi-cursor structuraleditors as an interactive equivalent to refactoring scriptsbased on AST transformation. In order to investigatethis, we collected real refactoring scripts from GitHuband attempted to perform their edits interactively inForest.

5.1 MethodScript Framework. We only considered scripts writ-

ten with the jscodeshift [2] framework. It is the de-factostandard for scripts that refactor JavaScript and Type-Script code. Although scripts that use the TypeScriptcompiler API instead of jscodeshift exist, we did notconsider them, since jscodeshift is more widely usedand has been available for longer. Generally, scriptswhich use the TypeScript compiler API are similar tothose which use jscodeshift.

Finding Scripts. Since there is no official collectionof representative jscodeshift scripts, we considered all

those in the most-starred list of jscodeshift scripts onGitHub [29]. There is also a longer list [7] which wedid not use, although we expect that using it wouldgive qualitatively similar results. We ignored refactor-ing scripts that used non-standard syntax (e.g., newoperators which are only in the proposal stage), un-realistically simple scripts (one script only removeddebugger statements), and scripts that did not have cor-responding before/after example programs.

Classification. Most repositories in the list containedmultiple refactoring scripts. We considered each scriptseparately. We looked at the scripts themselves, as wellas the before/after example programs that were used astest cases. Each script was classified based on whetherits edits could be reproduced in Forest:

• No: From reading the code it is clear that this ora similar refactor would not be possible in Forestfor a major reason.

• Maybe: This refactor or something similar wouldlikely be possible, but would require a new fea-ture, extra manual work, or a trade-off in theresult.

• Yes: This refactor is clearly possible. The samekinds of limitations as for Maybe are acceptable,but they must be quite minor.

Editing Attempts. For scripts that were classified asMaybe or Yes, we tried performing their edits in Forest.We wrote down any unforeseen issues and limitationsof our solution. Since Forest is a prototype, it lacks sup-port for certain language constructs. We approximatedunsupported constructs using existing supported syn-tax. For example, import { render } from "react-dom"

was written as fakeImport([render], "react-dom"). Thesecases still count towards the “unsupported syntax” is-sue in our results.

Previously Used Examples. Prior to conducting thisevaluation, we had already tried to reproduce the editsof some refactoring scripts in Forest and added newfeatures accordingly. Three of the scripts [25] includedin our evaluation were previously used. Each of thesescripts has a corresponding example (Examples A.4,A.7, and A.8).

5.2 ResultsWe used a total of 48 refactoring scripts in our evalu-ation (not counting 12 ignored scripts). We classifiedthem as follows: 20 No, 17 Maybe, and 11 Yes. Table 1lists the issues that we encountered during our editingattempts. The rest of this section describes those issues.We focus on commonly occurring issues that are notclear from the name alone. Additionally, we describe

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

some less common issues that we consider especiallyimportant.

“unsupported syntax”. The most common issue inour evaluation was “unsupported syntax”. However,this was almost never the issue that caused a script tobe classified No — it just happened to be a commonissue overall. The only exception was a script that re-quired template literals, which are not similar to anysupported syntax in Forest and would require extensivework to accommodate.

“have to recreate cursor multiple times”. Considerthe cursors in the final step of Example A.8. It is possi-ble to keep only the cursors containing object literalsand perform an edit with them. It is also possible tokeep only the cursors containing identifiers and per-form a different edit with them. However, it is not pos-sible to perform these two edits in sequence, withoutmanually recreating some cursors. Once all cursors ex-cept those containing object literals have been deleted,there is no command to restore them (“undo selectionchange” does not work across edits; marks are savedper cursor, so they cannot recreate deleted cursors). Bydeleting cursors before an edit, Forest can representedits with pseudocode of the form:

if (cursor.matchesCondition(conditionA )) {

cursor.performEdit(editA)

}

// no further edits

However, since checking the condition is done by delet-ing cursors, edits of the following form can not berepresented:

if (cursor.matchesCondition(conditionA )) {

cursor.performEdit(editA)

}

cursor.performEdit(editB)

“cannot handle separately found locations together”.Consider a program containing multiple object literals,each with a similar set of fields. A user would like toedit some of these object literals simultaneously usingmultiple cursors. Specifically, they would like to editobject literals which are arguments in a call to thefunction f (f({ ... })), as well as object literals used inreturn statements (return { ... }). In Forest, the usercan find object literals used in a call to f, edit those,then delete all cursors except one, find all object literalsused in return statements, and edit those. However,there is no way for the user to find object literals usedeither in a call to f or in a return statement, then edit allof them simultaneously (regardless of which of the twoconditions they matched). The following pseudocodedescribes such an edit (which is not possible in Forest):

if (

cursor.matchesCondition(conditionA) ||

cursor.matchesCondition(conditionB)

) {

cursor.performEdit(editA)

}

“nested copy-paste would be an issue”. This issueis discussed extensively in section 4.3.6, since it wasknown to us before this evaluation.

“lookup tables are possible but impractical”. Somerefactoring scripts contained large constants, for exam-ple, a list of all reserved words in JavaScript, or a list ofall commonly used functions in a specific library. Suchconstants were generally used to not edit locations con-taining specific identifiers, in order to avoid performingan incorrect refactoring. Forest does support regularexpression matching for identifiers, and it would bepossible to encode (for example) all reserved words inJavaScript in a regular expression. However, doing soduring interactive editing would not be practical at all.

“none-one-many issue”. Some commands in Forestwork differently depending on whether a list containsno items, one item, or more than one item. Considerthree cursors focused on the argument lists (excludingparentheses) of a(), b(x.y) and c(x.y, z). If the userexecutes the “split cursor” command (in relaxed multicursor mode), they would get one cursor in the argu-ment list of a (unchanged, since the command failed),a cursor on x in b, a cursor on y in b, a cursor on x.y inc, and a cursor on z in c. The differences between oneitem and more than one item are typically caused byForest’s handling of equivalent cursors. The differencesbetween no items and some items are typically causedby commands requiring at least one item to function.

6 Evaluation against Manual EditsRefactoring scripts are generally used to perform large-scale edits which would be impractical with manualediting. In some situations, a developer needs to make asimilar change in multiple locations, but too few to jus-tify writing a refactoring script. A multi-cursor editorcould be used to potentially perform such edits moreefficiently than manual single-cursor editing. To under-stand whether the small-scale edits that occur in realTypeScript codebases could be performed effectivelywith a multi-cursor editor, we manually inspected allof the commits in Forest’s codebase and classified thekinds of edits that were performed.

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

Affected Scripts

Total No Maybe Yes Issue

unsupported syntax 6 4 1 11have to recreate cursors multiple times 2 5 1 8

too complicated 7 0 0 7cannot handle separately found locations together 2 4 0 6

nested copy-paste would be an issue 0 4 1 5no strict “find usages of variable” 2 3 0 5

lookup tables are possible but impractical 3 0 0 3bug in paste 0 2 0 2

cannot edit multiple files 0 2 0 2cannot filter top-level statements 1 1 0 2cannot remove duplicate items 2 0 0 2

cannot search for dynamic query 2 0 0 2manual parenthesizing required 0 1 1 2

no strict “find declarations of variable” 2 0 0 2none-one-many issue 1 1 0 2

adding after first import or as first statement if no imports doesn’t work 0 1 0 1can do the flatten sometimes, but not in the general case 1 0 0 1

cannot create AST by parsing arbitrary string using JS 1 0 0 1cannot filter for exactly one item in list 1 0 0 1

cannot filter for exactly one search match 0 1 0 1cannot search up 1 0 0 1

matching is different because of search in flattened AST 0 1 0 1no sort feature 1 0 0 1

no support for zipping lists of cursors 1 0 0 1

Table 1. Issues encountered while performing the edits of real-world refactoring scripts (Section 5). The numbersin each row indicate the number of scripts where we encountered the given issue. The Total column countsscripts regardless of how they were classified. The other columns only count scripts that had the correspondingclassification. For example, we encountered the issue “have to recreate cursors multiple times” with 8 scripts, 2 ofwhich were classified No. Note that each script may have multiple issues. Scripts classified Yes often had no issues.

6.1 MethodWe manually inspected every commit in Forest’s code-base to identify edits which a multi-cursor editor couldpotentially perform effectively. We then more closelyanalyzed a subset of the commits which had potentialfor multi-cursor editing.

Initial Classification. Forest’s codebase contained913 commits at the time of our evaluation. There areroughly 5-20 commits per full workday, so most com-mits were small enough that individual edits wereclearly visible in their diffs. We manually classifiedeach commit based on whether it would potentially beuseful to perform part of its edit with a multi-cursoreditor:

• No: The commit contains no edits that a multi-cursor editor could perform especially effectively.

• Yes: Some edit in the commit could potentiallybe performed effectively using multiple cursorsin a multi-cursor editor.

Classification Guidelines. Estimating potential for ef-fective multi-cursor editing is subjective. We looked forpatterns such as multiple insertions or changes thatappeared similar. We considered whether a hypotheticalmulti-cursor editor could simultaneously edit these lo-cations. We based this on our experience with existingmulti-cursor editors (Kakoune for text and Forest forstructure) and considered whether a similar editor (po-tentially with added features) could perform the edit.When in doubt, we classified commits as Yes, to avoiddiscarding potentially interesting commits before thenext stage of our evaluation.

Ignoring Renames. Importantly, we classified editsthat could be performed using a “rename identifier”

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

refactor in a standard IDE as No. This refactor wascommonly used in the Forest codebase and alwaysperformed the desired edits perfectly. We think thatthere is no room for multi-cursor editors to improvethis result. However, multi-cursor editors could includethe same refactor that is used in existing IDEs, so theyare not at a disadvantage either.

Further Classification. We further investigated thecommits which were classified Yes in the previous eval-uation stage. Due to time constraints, we only consid-ered a random sample of 50% of those commits. Foreach commit, we grouped the changes (excluding thosewhere multi-cursor editing would not be useful) intomultiple edits. Each edit would be performed simulta-neously using multiple cursors. For each edit, we triedto describe the type of edit that was performed andestimated the number of cursors that the edit woulduse.

6.2 ResultsIn our initial classification stage, we classified 142 of 913commits (∼15%) Yes, meaning that they contained editsthat could potentially be performed effectively usingmultiple cursors in a multi-cursor editor. We furtherinvestigated a random sample of 50% (71) of the 142Yes commits. The number of distinct edits with multi-cursor potential per commit is shown in Figure 3. Mostcommits contained one such edit. Although all com-mits were assigned Yes in the initial classification, closerinspection during further classification revealed that 10commits contained no edits that we believe could plau-sibly be performed with a multi-cursor editor. Theseedits typically changed multiple similar locations, butinserted varied content in each one. Most edits useda small number of cursors (2–5) as shown in Figure4. Table 2 shows the types of edits that we found. Inthe rest of this section, we describe the most commontypes of edits in detail.

0 1 2 3 4Number of edits

0

20

40

Num

ber o

f com

mits

Figure 3. Number of edits with multi-cursor potentialin each commit

2 3 4 5 6 7 8 9 10 >10Number of cursors

0

10

20

Num

ber o

f edi

ts

Figure 4. Estimated number of cursors required foreach edit with multi-cursor potential

Count Category

22 specialized edit17 rename-like replace16 add/remove required argument/property11 wrap expression/statement6 identical insertion4 insertion with small unique part4 delete a declaration and everything related3 extract expression into function/variable2 add/remove modifier2 add destructure, type and value1 flatten variable declarations

Table 2. Categories of edits with potential for multi-cursor editing from commits in the codebase of Forest.

“specialized edit”. This category contains edits thatdo not fit in any other categories. These edits were gen-erally simple structural transformations, but ones thatare specific to the program being edited. To give anintuition of the kinds of edits in this category, we pro-vide some examples: Figure 5 (Example A.10) showsthe simplest edit in this category, where booleans arereplaced by one of two enum values. Figure 6 (Ex-ample A.11) shows an edit of slightly above averagecomplexity, where one function call is replaced by an-other, which requires introducing new arguments andchanging the handling of return values. Figures 7 and 8(Example A.12) show the most complex edit in this cat-egory, where a function call is removed and a new callis inserted in the nearest array literal, which requiresvarious related adjustments to the code.

“rename-like replace”. In such an edit, every usageof a variable, class property, or object field was replacedby a new expression. For example, consider a class withthe properties cursors: Cursor[] and oldCursors: Cursor[]

(accessed via this.cursors and this.oldCursors). If the

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

developer replaces all usages (potentially within a lim-ited area) of this.oldCursors by usages of this.cursors,that would be a rename-like replace. The defining fea-ture of this edit is that it affects the same locations asrenaming the oldCursors property affects, except thatthe property declaration itself is not changed (onlythe usages are replaced). An additional difference to atypical rename is that target locations can be replacedby any expression, not just the same one with a newidentifier (e.g., this.oldCursors could be replaced bythis.oldState.cursors). If the new expression differs ateach replacement location (e.g., it wraps the old expres-sion), the edit is counted as a specialized edit.

“add/remove required argument/property”. When arequired parameter is added to a function declaration,an appropriate expression must be added to the argu-ment list at every call site to fix type errors. Similaredits must be performed when a required argumentis removed, or when an interface property is added orremoved. This category captures such edits, but onlythose where the added argument expression is the sameat every call site. If the added argument differs at eachcall site (e.g., is copied from near the call), the edit iscounted as a specialized edit.

“wrap expression/statement”. In such an edit, anexpression (e.g., getInput()) is replaced by an expres-sion which contains the original somewhere inside (forexample, { device: 'keyboard', input: getInput() }). Ifthe original expression appears more than once in thereplacement, or if any part of the original expression ismodified (e.g., adding an argument to getInput()), thenthe edit is counted as a specialized edit.

“identical insertion”. In such an edit, the same codeis inserted in multiple locations. The inserted code doesnot wrap or replace any existing code. If any part ofthe inserted code varies, then the edit is counted asinsertion with small unique part.

“insertion with small unique part”. In such an edit,nearly the same code is inserted in multiple locations,but a small part of the inserted code differs. For ex-ample, a large object literal with boilerplate values isinserted in multiple locations, and then a single prop-erty value is changed at each location. The differingpart is typically hard to systematically guess, requiringa deep understanding of the code.

“delete a declaration and everything related”. Whena declaration (e.g., an enum declaration) is deleted, theuser often also deletes related code (e.g., every interfacedeclaration or function argument where the deletedenum was used). The extent of the deletion varies andgenerally has to be decided by the user (e.g., should

the properties of an interface which have the type ofthe deleted enum type be deleted, or should the wholeinterface declaration be deleted?).

“extract expression into function/variable”. In suchan edit, multiple copies of an identical expression arereplaced by references to a newly created variable con-taining the expression. If part of the expression variesat each usage site, the common part of the expressioncould be expected into a function.

“add/remove modifier”. In such an edit, modifiers(e.g public, async, export) are added or removed (e.g.,from methods or variable declarations). In our evalu-ation, this edit was only ever performed by adding orremoving the export keyword from function declara-tions.

7 DiscussionIn Section 5 we successfully used Forest to performthe edits from 11 of the 48 refactoring scripts whichwe tested. However, during this process, we discov-ered various issues that limited the possible edits of17 further scripts. In Section 7.1 we propose changesto Forest which we believe would address these issues.Section 7.2 more generally discusses the feasibility ofusing multi-cursor structural editors instead of refac-toring scripts. The evaluation in Section 6 showed that∼15% of edits in a real-world codebase had potentialfor multi-cursor editing. Although we did not performthese edits in Forest, we categorized them to allowfor discussion. Section 7.3 discusses how multi-cursorstructural editors and multi-cursor text editors wouldlikely handle the tasks in each category.

7.1 Solving the Discovered IssuesOur evaluation in Section 5 highlighted several issues,many of which are specific to multi-cursor editing. Inthis section, we outline changes to Forest that wouldsolve some of these issues.

7.1.1 Nested Copy-Paste Problem. In Section 4.3.6we described a workaround to the nested copy-pasteproblem. This problem appeared in our evaluation as“nested copy-paste would be an issue”. In this section,we propose two different changes to Forest that wouldsolve it.

Nested Mode Proposal. Our first proposal for addingproper nested editing support is inspired by the manualworkaround. Specifically, the user would create thenested cursors and then enter a nested editing mode.Upon entering this mode the editor would deactivateall cursors except the innermost ones. The user wouldperform their edit and then exit the nested editing

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

mode. The editor would then replay their commandsfor each level of nesting (innermost to outermost). Thiswould significantly reduce the amount of manual effortrequired.

Issue with Nested Mode Proposal. A potential issuewith the nested editing mode proposal is the mecha-nism by which commands are recorded. If all cursorsexcept the outermost cursors are effectively deletedwhile recording, some commands in Forest will notwork as expected, since they perform certain checksacross the set of all cursors. For example reduce tofirst/last cursor (see Section 4.3.1) checks if the com-mand would not remove any cursors at all (every groupcontains exactly one cursor), in which case it wouldautomatically repeat the operation for the next levelof the hierarchy of cursors. This automatic skip mighttrigger if only the innermost cursors are considered,but not if all cursors (including outer ones) are consid-ered. Differences in behavior like this would be visibleto the user, effectively leaking that the nested editingmode uses a record/replay implementation.

Improved Nested Mode Proposal. We propose a wayto fix most differences in the nested cursor mode: Cre-ate a copy of the document for each level of nestingwhen entering the nested editing mode and keep eachcursor in its corresponding document while recording.Commands which consider all cursors at once wouldget the set of cursors from all documents, avoiding anydifferences in behavior. The user would be shown thedocument with the innermost cursors while editing.After exiting the nested editing mode, all commandswould be replayed as before. This is a quite complicateddesign proposal and it is not clear whether further is-sues would arise.

Copy-by-Reference Proposal. An alternative way tosolve the problem (without a nested editing mode) is tochange the semantics of copy and paste. Currently, thecopy command in Forest takes an immutable snapshotof the selected nodes (not including any cursors in thatregion). We could change the copy command to workby reference: When copying, the command would markthe selected region. When pasting, the latest content ofthe copied region would be used. Note that the pasteoperations must still occur from innermost to outer-most, but this would now be an internal detail of theeditor. Interestingly the nested editing mode solutionis at least as powerful as copy-by-reference: Enteringnested editing mode, marking the copy region (Sec-tion 4.3.2), editing, marking the paste region, jumpingto the copy region, copying, jumping to the paste re-gion, pasting, and exiting nested editing mode wouldbe equivalent.

7.1.2 Cursor Snapshots. In Section 5.2 when discussingthe issue “have to recreate cursor multiple times” wenoted that “marks are saved per cursor, so they can-not recreate deleted cursors”. We propose to introducecursor snapshots, a feature similar to marks. However,whereas marks save the position of each cursor percursor, cursor snapshots would save the positions ofall cursors globally. Restoring cursor snapshots wouldrestore all cursors that existed at the time that thesnapshot was created. This is not a new idea: cursorsnapshots exist in Kakoune [10] (although they call thisfeature marks).

7.1.3 Stricter Multi-Cursor Commands. In Section4.3.4 we describe Forest’s multi-cursor strict mode (“ifany cursor would fail the command, then the commandwill not be performed at all“). In our evaluation, weencountered situations where no cursors failed a com-mand, but some cursors handled it differently fromothers, which prevented us from performing certainedits. This is described in Section 5.2 as the “none-one-many issue”. We propose to extend the multi-cursorstrict mode to consider more details of how a commandwill be executed across the set of all cursors, instead ofonly considering failures.

Example. Consider a similar example to the one fromSection 5.2 with two cursors focused on the argumentlists (excluding parentheses) of a(x.y) and b(x.y, z).The cursor in a is ambiguously focused on either asingle-item argument list or the argument itself. Thecursor in b is unambiguously focused on a multi-itemargument list Applying our proposal to this concreteexample: Forest’s “split cursor” command would con-sider both cursors together, and since both selectionscould be disambiguated as an argument list, it wouldsplit the argument list. This would result in no change(but a successful operation) for the cursor in a and asplit of two new cursors in b. This is much more in-tuitive than the current result, where the cursor in a

is split into x and y, since the disambiguation occursseparately for each cursor, choosing the deepest non-single-item list.

7.2 Potential to Replace Refactoring ScriptsBased on our evaluation in Section 5, we believe thatmulti-cursor structural editing could become a viablealternative to writing refactoring scripts in simple cases,but that such editors cannot replace refactoring scriptsin general.

Combining with Refactoring Scripts. Based on ourusage of Forest, we think that interactivity is a majorstrength of multi-cursor editing. It may be possible tocombine the expressiveness of refactoring scripts with

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

the interactivity of multi-cursor editing. For example,consider a hypothetical IDE where the user writes nor-mal refactoring scripts, but can interactively inspectintermediate search results and transformations. Addi-tionally, by treating the intermediate search results ascursors in a multi-cursor editor, the user would be ableto define transformations by performing them interac-tively. We think that this is an interesting direction forfuture research.

7.3 Edits with Multi-Cursor PotentialThe results of our evaluation in Section 6 showed thatedits where multi-cursor editors are potentially usefuldo occur in real-world codebases. Based on our catego-rization, we think that multi-cursor structural editingwould be a good fit for many of these edits. However,for many categories text editors (especially multi-cursortext editors) seem equally practical.

7.3.1 Potential for Structural Editing. We believe thatmulti-cursor structural editors are better suited thantext editors for edits in the categories “specialized edit”and “wrap expression/statement”.

“specialized edit”. The most frequent category (“spe-cialized edit”) contained edits that were naturally struc-tural and project-specific. Since the edits are structural,structural editors can perform them more naturallythan text editors. Since the edits are project-specific,an IDE can not provide built-in refactors for them.The edits in this category were also significantly sim-pler than those in many refactoring scripts in Section5. This makes it more likely that they can be per-formed effectively in an interactive multi-cursor struc-tural editor. Admittedly, it is likely that programming-by-demonstration tools would work well with theseedits for the same reason.

“wrap expression/statement”. Some edits from thecategory “wrap expression/statement” are significantlybetter suited to structural editing than to text editing.The wrapping operation itself can be easily performedusing a text editor once the range of the expressionis known (one insertion before this range, and one in-sertion after it). However, finding the range of certainexpressions can be difficult in a text editor, as shownin our motivational example in Figure 1. In these cases,structural editors have an advantage. However, in manycases, the target expressions happen to also be conve-nient to select using a text editor (e.g., parenthesizedexpressions or expressions occupying exactly one line).

7.3.2 Potential for Refactors. We believe that editsin the categories “rename-like replace”, “add/removerequired argument/property”, “delete a declarationand everything related”, and “extract expression into

function/variable” could be effectively accommodatedby providing built-in refactors in an IDE. This wouldbe possible irrespective of whether the normal editingexperience is structural or text-based. However, someof these refactors may be more useful when combinedwith structural editing features.

“extract expression into function/variable”. The “ex-tract expression into function/variable” edit is alreadya relatively common refactor, although often withoutsupport for deleting duplicate code.

“rename-like replace”. The existing rename refactorin current IDEs could be extended to support “rename-like replace”. As an alternative for most cases, a struc-tural find-replace feature (which some existing IDEssupport) could be used instead.

“delete a declaration and everything related”. The“delete a declaration and everything related” edit couldbe accommodated with a classic dialog-based refactor.IntelliJ has a limited version of such a refactor (namedsafe delete). However, we think that it is worth investigat-ing new UI designs to let the user efficiently describethe extent of the deletion.

“add/remove required argument/property”. Edits fromthe category “add/remove required argument/prop-erty” could be accommodated with a classic dialog-based refactor. However, it may be advantageous toperform this edit directly with a multi-cursor structuraleditor. The user would naturally have significant flex-ibility in how to insert new argument values (e.g., bycopying and modifying one of the existing arguments).

7.3.3 Potential for Text Editing. For the remainingcategories of edits, it would likely be equally efficientto perform them in a text editor as in a structural editor.

“identical insertion”. Edits from the category “iden-tical insertion” don’t depend on the surrounding code,so they are the same when performed in a text editoror in a structural editor with text-based insertion. Editsfrom the category “insertion with small unique part”could be performed in a structural editor by insertingthe unique part and then wrapping all inserted loca-tions by using multi-cursor editing. In a single cursortext editor, users can perform the insertion once, copyit, and adjust the unique part.

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

8 ConclusionWe presented Forest, an editor with unique integrationbetween structural editing and multi-cursor editing. Weintroduced the novel concept of a hierarchy of cursors,which allows multi-cursor edits to be reused as part ofother multi-cursor edits. Additionally, we designed aninteractive filtering system by introducing alternativemulti-cursor modes. Our investigation of a real-worldcodebase showed that ∼15% of commits had poten-tial for multi-cursor edits. We identified a subset ofthese edits where structural multi-cursor editing couldbe particularly useful. Additionally, we attempted toperform edits from real-world refactoring scripts, andshowed that Forest could perform the edits comparablywell to the scripts in 11 of the 48 cases. We presentedpossible improvements to our prototype, which wouldallow a further 17 refactoring scripts from our evalua-tion to be replaced. We believe that a hybrid approach,an IDE for editing refactoring scripts that shows in-termediate results and allows interactive multi-cursorstructural edits, is an interesting direction for futureresearch.

References[1] [n.d.]. MPS: The Domain-Specific Language Creator by JetBrains.

https://www.jetbrains.com/mps/[2] 2021. jscodeshift. https://github.com/facebook/jscodeshift original-

date: 2015-03-07T00:32:16Z.[3] 2021. reactjs/react-codemod. https://github.com/reactjs/react-

codemod original-date: 2015-10-19T20:47:22Z.[4] Dimitar Asenov and Peter Muller. 2014. Envision: A fast and

flexible visual code editor with fluid interactions (Overview).In 2014 IEEE Symposium on Visual Languages and Human-CentricComputing (VL/HCC). IEEE, Melbourne, Australia, 9–12. https://doi.org/10.1109/VLHCC.2014.6883014

[5] Thorsten Berger, Markus Völter, Hans Peter Jensen, TaweesapDangprasert, and Janet Siegmund. 2016. Efficiency of projec-tional editing: a controlled experiment. In Proceedings of the2016 24th ACM SIGSOFT International Symposium on Founda-tions of Software Engineering (FSE 2016). Association for Com-puting Machinery, New York, NY, USA, 763–774. https://doi.org/10.1145/2950290.2950315

[6] Marat Boshernitsan, Susan L. Graham, and Marti A. Hearst.2007. Aligning development tools with the way programmersthink about code changes. In Proceedings of the SIGCHI Confer-ence on Human Factors in Computing Systems. ACM, San JoseCalifornia USA, 567–576. https://doi.org/10.1145/1240624.1240715

[7] Rajasegar Chandran. 2021. Awesome Codemods. https://github.com/rajasegar/awesome-codemods original-date: 2019-12-11T00:38:56Z.

[8] Yair Chuchem and Eyal Lotem. [n.d.]. Lamdu. https://www.lamdu.org/

[9] James R. Cordy. 2006. The TXL source transformation language.Science of Computer Programming 61, 3 (Aug. 2006), 190–210.https://doi.org/10.1016/j.scico.2006.04.002

[10] Maxime Coste. [n.d.]. Kakoune - Official site. https://kakoune.org/

[11] Véronique Donzeau-Gouge, Gérard Huet, Bernard Lang, andGilles Kahn. 1980. Programming environments based on structured

editors: the Mentor experience. Technical Report. INRIA.[12] Robert J. Ellison and Barbara J. Staudt. 1985. The evolution of

the GANDALF system. Journal of Systems and Software 5, 2 (May1985), 107–119. https://doi.org/10.1016/0164-1212(85)90012-3

[13] C. N. Fischer, Gregory F. Johnson, Jon Mauney, Anil Pal, andDaniel L. Stock. 1984. The Poe language-based editor project.In Proceedings of the first ACM SIGSOFT/SIGPLAN software engi-neering symposium on Practical software development environments(SDE 1). Association for Computing Machinery, New York, NY,USA, 21–29. https://doi.org/10.1145/800020.808245

[14] E. Gansner, J. R. Horgan, D. J. Moore, P. Surko, D. Swartwout,and J. Reppy. 1983. Syned – A Language-Based Editor for anInteractive Programming Environment. Technical Report.

[15] David B. Garlan and Philip L. Miller. 1984. GNOME: An intro-ductory programming environment based on a family of struc-ture editors. In Proceedings of the first ACM SIGSOFT/SIGPLANsoftware engineering symposium on Practical software developmentenvironments (SDE 1). Association for Computing Machinery,New York, NY, USA, 65–72. https://doi.org/10.1145/800020.808250

[16] Kiran Gopinathan. 2021. GopCaml: A Structural Editor forOCaml. https://icfp21.sigplan.org/details/ocaml-2021-papers/11/GopCaml-A-Structural-Editor-for-OCaml

[17] Wilfred J. Hansen. 1971. User engineering principles for inter-active systems. In Proceedings of the May 16-18, 1972, spring jointcomputer conference on - AFIPS ’72 (Spring). ACM Press, AtlanticCity, New Jersey, 523. https://doi.org/10.1145/1479064.1479159

[18] Miryung Kim and Na Meng. 2014. Recommending ProgramTransformations. In Recommendation Systems in Software Engi-neering, Martin P. Robillard, Walid Maalej, Robert J. Walker,and Thomas Zimmermann (Eds.). Springer, Berlin, Heidelberg,421–453. https://doi.org/10.1007/978-3-642-45135-5_16

[19] L. Meertens, S. Pemberton, and G. Rossum. 1992. The ABCstructure editor – Structure-based editing for the ABC programmingenvironment. Technical Report.

[20] B Melese, V Migot, and D Verove. 1985. The Mentor-V5 documen-tation. Technical Report. INRIA.

[21] Na Meng, Miryung Kim, and Kathryn S. McKinley. 2011. Sys-tematic editing: generating program transformations from anexample. In Proceedings of the 32nd ACM SIGPLAN Conferenceon Programming Language Design and Implementation (PLDI ’11).Association for Computing Machinery, New York, NY, USA,329–342. https://doi.org/10.1145/1993498.1993537

[22] Martin Mikelsons. 1983. Interactive program execution inLispedit. In Proceedings of the symposium on High-level debugging(SIGSOFT ’83). Association for Computing Machinery, NewYork, NY, USA, 71–80. https://doi.org/10.1145/1006147.1006164

[23] Philip Miller, John Pane, Glenn Meter, and Scott Vorthmann.1994. Evolution of Novice Programming Environments: TheStructure Editors of Carnegie Mellon University. InteractiveLearning Environments 4, 2 (Jan. 1994), 140–158. https://doi.org/10.1080/1049482940040202

[24] Robert C. Miller and B. Myers. 2001. Interactive SimultaneousEditing of Multiple Text Regions. In USENIX Annual TechnicalConference, General Track.

[25] Christoph Nakazawa. 2022. cpojer/js-codemod. https://github.com/cpojer/js-codemod original-date: 2015-03-23T04:45:13Z.

[26] Wode Ni, Joshua Sunshine, Vu Le, Sumit Gulwani, and Ti-tus Barik. 2021. reCode : A Lightweight Find-and-Replace In-teraction in the IDE for Transforming Code by Example. InThe 34th Annual ACM Symposium on User Interface Softwareand Technology. ACM, Virtual Event USA, 258–269. https://doi.org/10.1145/3472749.3474748

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

[27] Cyrus Omar, Ian Voysey, Ravi Chugh, and Matthew A. Hammer.2019. Live functional programming with typed holes. Proceed-ings of the ACM on Programming Languages 3, POPL (Jan. 2019),14:1–14:32. https://doi.org/10.1145/3290327

[28] Rob Pike. 1987. Structural Regular Expressions. Technical Report.AT&T Bell Laboratories.

[29] Yevgen Safronov. 2021. awesome jscodeshift. https://github.com/sejoker/awesome-jscodeshift original-date: 2016-03-05T21:07:18Z.

[30] Magnar Sveen. 2021. multiple-cursors.el. https://github.com/magnars/multiple-cursors.el original-date: 2012-01-24T08:45:50Z.

[31] Tim Teitelbaum and Thomas Reps. 1981. The Cornell pro-gram synthesizer: a syntax-directed programming environ-ment. Commun. ACM 24, 9 (Sept. 1981), 563–573. https://doi.org/10.1145/358746.358755

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

A ExamplesThis appendix contains step-by-step examples of Forestbeing used for various editing tasks (Examples A.2to A.9). Example A.9 is the motivating example fromSection 1. The remaining examples are discussed inSection 4. Additionally, this appendix contains diffsshowing the edits described in Section 6.2 (ExamplesA.10 to A.12).

The step-by-step editing examples show the initialstate of a program in Step 1. Each following step de-scribes an edit, shows the state after the edit was per-formed, and lists the used keystrokes. Section A.1 con-tains all available commands in Forest, along with theirkeybindings.

A.1 Commands in ForestThis section lists all the editing commands which areavailable in Forest.

A.1.1 Basic Commands. The following commands areused for both single-cursor and multi-cursor editingin Forest. Section 3 describes the general behavior ofForest, as well as the design decisions concerning someof the following commands.

Move to parent

/ Move to previous/next leaf node+ / Extend selection to previous/next leaf node

Space Reduce selection to element just addedby extend

Alt + / Reduce selection to first/last element( , [ , { , < Select contents of first list delimited

by this matching pair (descendant of current selection)

Select contents of first list delimited by any match-ing pair (descendant of current selection)

) , ] , } , > Select closest list delimited by thismatching pair (ancestor of current selection)

+ Select closest list delimited by any matchingpair (ancestor of current selection)

z , + z Undo or redo selection change

Ctrl + + / Remove last/first element from selec-tioni , a Insert text before/after cursord Delete selected nodesc , p Copy and paste

A.1.2 Multi-Cursor Commands. The following com-mands are used for performing multi-cursor editing.Their behavior is described in Section 4.s Split cursor by creating cursors for each selected

list itemq Queue selection to later create a cursor with

+ q Create cursors from each queued selection(replaces existing cursor)

+ s / / / Remove all cursors except the first/last/outermost/innermost onesm letter Save current selection as mark (named by

letter)+ m letter Jump to selection that was saved as

mark (named by letter)

r Rename all selected identifiers using JavaScriptexpression

/ Open structural search

y r / d / s Change multi-cursor mode to relaxed/drop/strict

+ y s / f / a Restore state before failure and keepsuccessful/failed/all cursors (branching)

+ y i Ignore failure (keep current state andcursors)

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

A.2 multi-cursor-reduce-across

export function f(a, b, c) {

return a * b * c;

}

export function g(a, b, c) {

return a + b + c;

}

Step 1. Initial state

export function f(a, b, c) {

return a * b * c;

}

export function g(a, b, c) {

return a + b + c;

}

s

Step 2. Split cursor (one per function)

export function f(a, b, c) {

return a * b * c;

}

export function g(a, b, c) {

return a + b + c;

}

( s

Step 3. Go to parameters and split cursor (one perparameter)

export function f(

a: number,

b: number,

c: number,

) {

return a * b * c;

}

export function g(

a: number,

b: number,

c: number,

) {

return a + b + c;

}

a

Type :number

Esc

Step 4. Add type annotation to parameter

export function f(

a: number,

b: number,

c: number,

) {

return a * b * c;

}

export function g(

a: number,

b: number,

c: number,

) {

return a + b + c;

}

+ s

Step 5. Remove all cursors except the first (perfunction)

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

export function f(

a: number,

b: number,

c: number,

): number {

return a * b * c;

}

export function g(

a: number,

b: number,

c: number,

): number {

return a + b + c;

}

) a

Type :number

Esc

Step 6. Add return type annotation

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

A.3 multi-cursor-marks

export function f(

thing: string = "test",

count: number = 1,

) {

return count === 1 ? thing : thing + "s";

}

Step 1. Initial state

export function f(

thing: string = "test",

count: number = 1,

) {

if (debug) {

console.log({});

}

return count === 1 ? thing : thing + "s";

}

{ i

Type if(debug){console.log({})}

Esc

Step 2. Insert at start of function body

export function f(

thing: string = "test",

count: number = 1,

) {

if (debug) {

console.log({});

}

return count === 1 ? thing : thing + "s";

}

{ { } m a

Step 3. Select and mark empty object literal

export function f(

thing: string = "test",

count: number = 1,

debug: boolean = false,

) {

if (debug) {

console.log({});

}

return count === 1 ? thing : thing + "s";

}

} } + Space a

Type ,debug:boolean=false

Esc

Step 4. Append function parameter

export function f(

thing: string = "test",

count: number = 1,

debug: boolean = false,

) {

if (debug) {

console.log({});

}

return count === 1 ? thing : thing + "s";

}

Ctrl + + s

Step 5. Select parameters except debug and splitcursor

export function f(

thing: string = "test",

count: number = 1,

debug: boolean = false,

) {

if (debug) {

console.log({});

}

return count === 1 ? thing : thing + "s";

}

m b Alt + c

Step 6. Mark parameter and copy parameter name

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

export function f(

thing: string = "test",

count: number = 1,

debug: boolean = false,

) {

if (debug) {

console.log({

x: { current: x, default: x },

x: { current: x, default: x },

});

}

return count === 1 ? thing : thing + "s";

}

+ m a a

Type x: {current: x, default: x},

Esc

Step 7. Insert inside marked empty object literal

export function f(

thing: string = "test",

count: number = 1,

debug: boolean = false,

) {

if (debug) {

console.log({

thing: { current: thing, default: x },

count: { current: count, default: x },

});

}

return count === 1 ? thing : thing + "s";

}

Alt + p p

Step 8. Paste name over first two "x"s

export function f(

thing: string = "test",

count: number = 1,

debug: boolean = false,

) {

if (debug) {

console.log({

thing: { current: thing, default: x },

count: { current: count, default: x },

});

}

return count === 1 ? thing : thing + "s";

}

m c

Step 9. Move to last "x" and mark it

export function f(

thing: string = "test",

count: number = 1,

debug: boolean = false,

) {

if (debug) {

console.log({

thing: { current: thing, default: x },

count: { current: count, default: x },

});

}

return count === 1 ? thing : thing + "s";

}

+ m b Alt + c

Step 10. Jump to marked parameter declaration andcopy initializer

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

export function f(

thing: string = "test",

count: number = 1,

debug: boolean = false,

) {

if (debug) {

console.log({

thing: { current: thing, default: "test" },

count: { current: count, default: 1 },

});

}

return count === 1 ? thing : thing + "s";

}

+ m c p

Step 11. Jump to last "x" and paste initializer

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

A.4 cpojer-js-codemod-unchain-variables

var foo = true,

bar = false;

const baz = 1,

fiz = "2";

let buzz = 3.3,

biz = {};

var hello,

ohai = function ohai() {},

hi;

var Neil, deGrasse, Tyson;

for (var i = 0, j = 10; i < j; i++, j--) {

console.log(i, j);

}

Step 1. Initial state

var foo = true,

bar = false;

const baz = 1,

fiz = "2";

let buzz = 3.3,

biz = {};

var hello,

ohai = function ohai() {},

hi;

var Neil, deGrasse, Tyson;

for (var i = 0, j = 10; i < j; i++, j--) {

console.log(i, j);

}

Ctrl + + s

Step 2. Deselect for loop. Split cursor (one perstatement).

var foo = true,

bar = false;

const baz = 1,

fiz = "2";

let buzz = 3.3,

biz = {};

var hello,

ohai = function ohai() {},

hi;

var Neil, deGrasse, Tyson;

for (var i = 0, j = 10; i < j; i++, j--) {

console.log(i, j);

}

Ctrl + + s m a c

Step 3. Deselect keyword. Split cursor (one perdeclaration). Mark and copy declaration.

var foo = true,

bar = false;

const baz = 1,

fiz = "2";

let buzz = 3.3,

biz = {};

var hello,

ohai = function ohai() {},

hi;

var Neil, deGrasse, Tyson;

for (var i = 0, j = 10; i < j; i++, j--) {

console.log(i, j);

}

Step 4. Move up to statement (cursors overlap)

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

var x;

var x;

var foo = true,

bar = false;

var x;

var x;

const baz = 1,

fiz = "2";

var x;

var x;

let buzz = 3.3,

biz = {};

var x;

var x;

var x;

var hello,

ohai = function ohai() {},

hi;

var x;

var x;

var x;

var Neil, deGrasse, Tyson;

for (var i = 0, j = 10; i < j; i++, j--) {

console.log(i, j);

}

i

Type var x;

Esc

Step 5. Insert new statements (multiple copies inserteddue to overlapping cursors)

var foo = true;

var bar = false;

var foo = true,

bar = false;

var baz = 1;

var fiz = "2";

const baz = 1,

fiz = "2";

var buzz = 3.3;

var biz = {};

let buzz = 3.3,

biz = {};

var hello;

var ohai = function ohai() {};

var hi;

var hello,

ohai = function ohai() {},

hi;

var Neil;

var deGrasse;

var Tyson;

var Neil, deGrasse, Tyson;

for (var i = 0, j = 10; i < j; i++, j--) {

console.log(i, j);

}

Alt + p m b

Step 6. Paste declaration into new statement and markthe paste location

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

var foo = true;

var bar = false;

var foo = true,

bar = false;

const baz = 1;

const fiz = "2";

const baz = 1,

fiz = "2";

let buzz = 3.3;

let biz = {};

let buzz = 3.3,

biz = {};

var hello;

var ohai = function ohai() {};

var hi;

var hello,

ohai = function ohai() {},

hi;

var Neil;

var deGrasse;

var Tyson;

var Neil, deGrasse, Tyson;

for (var i = 0, j = 10; i < j; i++, j--) {

console.log(i, j);

}

+ m a Alt + c + m b p

Step 7. Go to old keyword, copy it, and jump back,and paste it.

var foo = true;

var bar = false;

const baz = 1;

const fiz = "2";

let buzz = 3.3;

let biz = {};

var hello;

var ohai = function ohai() {};

var hi;

var Neil;

var deGrasse;

var Tyson;

for (var i = 0, j = 10; i < j; i++, j--) {

console.log(i, j);

}

+ s + m a d

Step 8. Delete all cursors except the first (per oldstatement). Delete the old statement.

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

A.5 cpojer-js-codemod-jest-arrow-flat

it("should be happy", function () {

console.log("actually forwards body");

});

it("another one", function () {});

Step 1. Initial state

it("should be happy", function () {

console.log("actually forwards body");

});

it("another one", function () {});

Use structural search UI (not shown) to search for functionexpressions (with any content)

Step 2. Search for function expressions

it("should be happy", function () {

console.log("actually forwards body");

});

it("another one", function () {});

{ } c

Step 3. Copy function body

it(

"should be happy",

() => {

console.log("actually forwards body");

},

function () {

console.log("actually forwards body");

},

);

it(

"another one",

() => {},

function () {},

);

i

Type ()=>{},

Esc { } p

Step 4. Create arrow function and paste body

it("should be happy", () => {

console.log("actually forwards body");

});

it("another one", () => {});

+ Space d

Step 5. Delete function expression

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

A.6 cpojer-js-codemod-jest-arrow-fail

describe("describe", function () {

it("should be happy", function () {

console.log("actually forwards body");

});

it("should leave arrow functions", () => {});

describe("nested describe", function () {

xit("disabled still counts", function () {});

xdescribe("disabled describe", function () {});

});

});

function containsit() {}

containsit(function () {});

Step 1. Initial state

describe("describe", function () {

it("should be happy", function () {

console.log("actually forwards body");

});

it("should leave arrow functions", () => {});

describe("nested describe", function () {

xit("disabled still counts", function () {});

xdescribe("disabled describe", function () {});

});

});

function containsit() {}

containsit(function () {});

Use structural search UI (not shown) to find calls with calleenames matching a regular expression and having a functionexpression as the second argument

Step 2. Search for “it” and “describe” calls

describe("describe", function () {

it("should be happy", function () {

console.log("actually forwards body");

});

it("should leave arrow functions", () => {});

describe("nested describe", function () {

xit("disabled still counts", function () {});

xdescribe("disabled describe", function () {});

});

});

function containsit() {}

containsit(function () {});

{ } c

Step 3. Copy function body

describe(

"describe",

() => {

it("should be happy", function () {

console.log("actually forwards body");

});

it("should leave arrow functions", () => {});

describe("nested describe", function () {

xit("disabled still counts", function () {});

xdescribe("disabled describe", function () {});

});

},

function () {

it(

"should be happy",

() => {

console.log("actually forwards body");

},

function () {

console.log("actually forwards body");

},

);

it("should leave arrow functions", () => {});

describe(

"nested describe",

() => {

xit("disabled still counts", function () {});

xdescribe("disabled describe", function () {});

},

function () {

xit(

"disabled still counts",

() => {},

function () {},

);

xdescribe(

"disabled describe",

() => {},

function () {},

);

},

);

},

);

function containsit() {}

containsit(function () {});

i

Type ()=>{},

Esc { } p

Step 4. Create arrow function and paste body. This isthe problematic step. Changes from inner cursors arelost, because they were made after the function bodywas copied.

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

describe("describe", () => {

it("should be happy", function () {

console.log("actually forwards body");

});

it("should leave arrow functions", () => {});

describe("nested describe", function () {

xit("disabled still counts", function () {});

xdescribe("disabled describe", function () {});

});

});

function containsit() {}

containsit(function () {});

+ Space d

Step 5. Delete function expression

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

A.7 cpojer-js-codemod-jest-arrow

describe("describe", function () {

it("should be happy", function () {

console.log("actually forwards body");

});

it("should leave arrow functions", () => {});

describe("nested describe", function () {

xit("disabled still counts", function () {});

xdescribe("disabled describe", function () {});

});

});

function containsit() {}

containsit(function () {});

Step 1. Initial state

describe("describe", function () {

it("should be happy", function () {

console.log("actually forwards body");

});

it("should leave arrow functions", () => {});

describe("nested describe", function () {

xit("disabled still counts", function () {});

xdescribe("disabled describe", function () {});

});

});

function containsit() {}

containsit(function () {});

Use structural search UI (not shown) to find calls with calleenames matching a regular expression and having a functionexpression as the second argument

+ s

Step 2. Pass 1: Search for “it“ and “describe” calls.Delete all cursors except innermost.

describe("describe", function () {

it("should be happy", () => {

console.log("actually forwards body");

});

it("should leave arrow functions", () => {});

describe("nested describe", function () {

xit("disabled still counts", () => {});

xdescribe("disabled describe", () => {});

});

});

function containsit() {}

containsit(function () {});

{ } c i

Type ()=>{},

Esc { } p + Space d

Step 3. Pass 1: Copy function body. Create arrowfunction. Paste body. Delete function expression.

describe("describe", function () {

it("should be happy", () => {

console.log("actually forwards body");

});

it("should leave arrow functions", () => {});

describe("nested describe", function () {

xit("disabled still counts", () => {});

xdescribe("disabled describe", () => {});

});

});

function containsit() {}

containsit(function () {});

) ) ) + s f

Step 4. Select whole document and remove duplicatecursors.

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

describe("describe", function () {

it("should be happy", () => {

console.log("actually forwards body");

});

it("should leave arrow functions", () => {});

describe("nested describe", function () {

xit("disabled still counts", () => {});

xdescribe("disabled describe", () => {});

});

});

function containsit() {}

containsit(function () {});

Use structural search UI (not shown) to find calls with calleenames matching a regular expression and having a functionexpression as the second argument

+ s

Step 5. Pass 2: Search for “it“ and “describe” calls.Delete all cursors except innermost.

describe("describe", function () {

it("should be happy", () => {

console.log("actually forwards body");

});

it("should leave arrow functions", () => {});

describe("nested describe", () => {

xit("disabled still counts", () => {});

xdescribe("disabled describe", () => {});

});

});

function containsit() {}

containsit(function () {});

{ } c i

Type ()=>{},

Esc { } p + Space d

Step 6. Pass 2: Copy function body. Create arrowfunction. Paste body. Delete function expression.

describe("describe", function () {

it("should be happy", () => {

console.log("actually forwards body");

});

it("should leave arrow functions", () => {});

describe("nested describe", () => {

xit("disabled still counts", () => {});

xdescribe("disabled describe", () => {});

});

});

function containsit() {}

containsit(function () {});

) ) ) + s f

Step 7. Select whole document and remove duplicatecursors.

describe("describe", function () {

it("should be happy", () => {

console.log("actually forwards body");

});

it("should leave arrow functions", () => {});

describe("nested describe", () => {

xit("disabled still counts", () => {});

xdescribe("disabled describe", () => {});

});

});

function containsit() {}

containsit(function () {});

Use structural search UI (not shown) to find calls with calleenames matching a regular expression and having a functionexpression as the second argument

+ s

Step 8. Pass 3: Search for “it“ and “describe” calls.Delete all cursors except innermost.

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

describe("describe", () => {

it("should be happy", () => {

console.log("actually forwards body");

});

it("should leave arrow functions", () => {});

describe("nested describe", () => {

xit("disabled still counts", () => {});

xdescribe("disabled describe", () => {});

});

});

function containsit() {}

containsit(function () {});

{ } c i

Type ()=>{},

Esc { } p + Space d

Step 9. Pass 3: Copy function body. Create arrowfunction. Paste body. Delete function expression.

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

A.8 cpojer-js-codemod-rm-object-assign-basic

let x = Object.assign({}, { a: 1 }, { b: 2 });

Object.assign({}, a);

Object.assign({ a: 1 }, b, { c: 3 });

Object.assign(a, b);

Object.assign({}, ...b);

Object.assign(

{

a: 1,

},

{ b: 2 },

c,

d,

);

Step 1. Initial state

let x = Object.assign({}, { a: 1 }, { b: 2 });

Object.assign({}, a);

Object.assign({ a: 1 }, b, { c: 3 });

Object.assign(a, b);

Object.assign({}, ...b);

Object.assign(

{

a: 1,

},

{ b: 2 },

c,

d,

);

s ( y s

Step 2. Split cursors (one per call) and select itsarguments. Switch to multi-cursor strict mode.

let x = Object.assign({}, { a: 1 }, { b: 2 });

Object.assign({}, a);

Object.assign({ a: 1 }, b, { c: 3 });

Object.assign(a, b);

Object.assign({}, ...b);

Object.assign(

{

a: 1,

},

{ b: 2 },

c,

d,

);

Alt +

Use structural search UI (not shown) to check whether theselected node is an object literal

+ y

Step 3. Check whether first argument is an object andshow successful/failed cursors

let x = Object.assign({}, { a: 1 }, { b: 2 });

Object.assign({}, a);

Object.assign({ a: 1 }, b, { c: 3 });

Object.assign(a, b);

Object.assign({}, ...b);

Object.assign(

{

a: 1,

},

{ b: 2 },

c,

d,

);

s Ctrl + +

Step 4. Keep successful cursors. Select all argumentsexcept first.

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

let x = Object.assign({}, { a: 1 }, { b: 2 });

Object.assign({}, a);

Object.assign({ a: 1 }, b, { c: 3 });

Object.assign(a, b);

Object.assign({}, ...b);

Object.assign(

{

a: 1,

},

{ b: 2 },

c,

d,

);

Use structural search UI (not shown) to search for spreadelements

+ y

Step 5. Search for spread elements and showsuccessful/failed cursors

let x = Object.assign({}, { a: 1 }, { b: 2 }, {});

Object.assign({}, a, {});

Object.assign({ a: 1 }, b, { c: 3 }, {});

Object.assign(a, b);

Object.assign({}, ...b);

Object.assign({ a: 1 }, { b: 2 }, c, d, {});

f a

Type ,{}

Esc Ctrl + + s

Step 6. Keep failed cursors. Append object literal toargument list. Select other arguments and split cursor(one for each old argument).

let x = Object.assign(

{},

{ a: 1 },

{ b: 2 },

{ ...({}), ...({ a: 1 }), ...({ b: 2 }) },

);

Object.assign({}, a, { ...({}), ...(a) });

Object.assign(

{ a: 1 },

b,

{ c: 3 },

{ ...({ a: 1 }), ...(b), ...({ c: 3 }) },

);

Object.assign(a, b);

Object.assign({}, ...b);

Object.assign({ a: 1 }, { b: 2 }, c, d, {

...({ a: 1 }),

...({ b: 2 }),

...(c),

...(d),

});

c ) Alt + { a

Type ...(x),

Esc ( p

Step 7. Copy argument. Insert placeholder spreadelement into object. Paste argument.

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

let x =

(x) &&

Object.assign(

{},

{ a: 1 },

{ b: 2 },

{ ...({}), ...({ a: 1 }), ...({ b: 2 }) },

);

(x) && Object.assign({}, a, { ...({}), ...(a) });

(x) &&

Object.assign(

{ a: 1 },

b,

{ c: 3 },

{ ...({ a: 1 }), ...(b), ...({ c: 3 }) },

);

Object.assign(a, b);

Object.assign({}, ...b);

(x) &&

Object.assign({ a: 1 }, { b: 2 }, c, d, {

...({ a: 1 }),

...({ b: 2 }),

...(c),

...(d),

});

+ s } c ) i

Type (x) &&

Esc

Step 8. Remove all cursors except first. Copy newobject literal. Insert placeholder where it will later bepasted.

let x = ({ ...({}), ...({ a: 1 }), ...({ b: 2 }) });

({ ...({}), ...(a) });

({ ...({ a: 1 }), ...(b), ...({ c: 3 }) });

Object.assign(a, b);

Object.assign({}, ...b);

({ ...({ a: 1 }), ...({ b: 2 }), ...(c), ...(d) });

( p ) Ctrl + + d { s Alt +

Step 9. Paste object literal and delete “Object.assign”call.

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

A.9 wrap-handlers

export const handlers = {

"ctrl-v": node.actions.setVariant

? tryAction("setVariant", (n) => n.id, true)

: tryAction("setFromString"),

"ctrl-d": tryDeleteChild,

"ctrl-c": () => copyNode(node),

"ctrl-p": tryAction("replace", (n) => n.id),

"ctrl-f": editFlags,

"ctrl-4": () =>

setMarks({

...marks,

TODO: idPathFromParentIndexEntry(

parentIndexEntry,

),

}),

};

for (const [k, v] of handlers) {

handlers[k] = warnOnError(v);

}

Step 1. Initial state

export const handlers = {

"ctrl-v": node.actions.setVariant

? tryAction("setVariant", (n) => n.id, true)

: tryAction("setFromString"),

"ctrl-d": tryDeleteChild,

"ctrl-c": () => copyNode(node),

"ctrl-p": tryAction("replace", (n) => n.id),

"ctrl-f": editFlags,

"ctrl-4": () =>

setMarks({

...marks,

TODO: idPathFromParentIndexEntry(

parentIndexEntry,

),

}),

};

for (const [k, v] of handlers) {

handlers[k] = warnOnError(v);

}

{ s

Step 2. Select all properties and split cursor (cursor perproperty)

export const handlers = {

"ctrl-v": placeholder,

"ctrl-d": placeholder,

"ctrl-c": placeholder,

"ctrl-p": placeholder,

"ctrl-f": placeholder,

"ctrl-4": placeholder,

};

for (const [k, v] of handlers) {

handlers[k] = warnOnError(v);

}

Alt + c d

Step 3. Select, copy and delete property value.

export const handlers = {

"ctrl-v": warnOnError(

node.actions.setVariant

? tryAction("setVariant", (n) => n.id, true)

: tryAction("setFromString"),

),

"ctrl-d": warnOnError(tryDeleteChild),

"ctrl-c": warnOnError(() => copyNode(node)),

"ctrl-p": warnOnError(

tryAction("replace", (n) => n.id),

),

"ctrl-f": warnOnError(editFlags),

"ctrl-4": warnOnError(() =>

setMarks({

...marks,

TODO: idPathFromParentIndexEntry(

parentIndexEntry,

),

}),

),

};

for (const [k, v] of handlers) {

handlers[k] = warnOnError(v);

}

a

Type warnOnError(x)

Esc ( p

Step 4. Add call to “warnOnError” and paste oldproperty value inside.

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

export const handlers = {

"ctrl-v": warnOnError(

node.actions.setVariant

? tryAction("setVariant", (n) => n.id, true)

: tryAction("setFromString"),

),

"ctrl-d": warnOnError(tryDeleteChild),

"ctrl-c": warnOnError(() => copyNode(node)),

"ctrl-p": warnOnError(

tryAction("replace", (n) => n.id),

),

"ctrl-f": warnOnError(editFlags),

"ctrl-4": warnOnError(() =>

setMarks({

...marks,

TODO: idPathFromParentIndexEntry(

parentIndexEntry,

),

}),

),

"ctrl-x": ignoreError(cut),

};

for (const [k, v] of handlers) {

handlers[k] = warnOnError(v);

}

+ s + a

Type ,"ctrl-x": ignoreError(cut)

Esc

Step 5. Remove all cursors except the last and add anew property.

export const handlers = {

"ctrl-v": warnOnError(

node.actions.setVariant

? tryAction("setVariant", (n) => n.id, true)

: tryAction("setFromString"),

),

"ctrl-d": warnOnError(tryDeleteChild),

"ctrl-c": warnOnError(() => copyNode(node)),

"ctrl-p": warnOnError(

tryAction("replace", (n) => n.id),

),

"ctrl-f": warnOnError(editFlags),

"ctrl-4": warnOnError(() =>

setMarks({

...marks,

TODO: idPathFromParentIndexEntry(

parentIndexEntry,

),

}),

),

"ctrl-x": ignoreError(cut),

};

} + Space d

Step 6. Delete the loop that would wrap each property.

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

A.10 245877a7b73dd3b81d63fe382c23f3eb17c79b31

...} else if (ev.key === "l" && !hasAltLike(ev)) {

const result = cursorMoveLeaf({

root: this.doc.root,

cursor: this.getCursor(),

direction: 1,

extend: false,

});

this.setFromCursor(result.cursor);

} else if (ev.key === "L" && !ev.ctrlKey) {

const result = cursorMoveLeaf({

root: this.doc.root,

cursor: this.getCursor(),

direction: 1,

extend: true,

});

this.setFromCursor(result.cursor);

this.nextEnableReduceToTip = true;

} else if (ev.key === "h" && !hasAltLike(ev)) {

const result = cursorMoveLeaf({

root: this.doc.root,

cursor: this.getCursor(),

direction: -1,

extend: false,

});

this.setFromCursor(result.cursor)

...

...} else if (ev.key === "l" && !hasAltLike(ev)) {

const result = cursorMoveLeaf({

root: this.doc.root,

cursor: this.getCursor(),

direction: 1,

mode: CursorMoveLeafMode.Move,

});

this.setFromCursor(result.cursor);

} else if (ev.key === "L" && !ev.ctrlKey) {

const result = cursorMoveLeaf({

root: this.doc.root,

cursor: this.getCursor(),

direction: 1,

mode: CursorMoveLeafMode.ExtendSelection,

});

this.setFromCursor(result.cursor);

this.nextEnableReduceToTip = true;

} else if (ev.key === "h" && !hasAltLike(ev)) {

const result = cursorMoveLeaf({

root: this.doc.root,

cursor: this.getCursor(),

direction: -1,

mode: CursorMoveLeafMode.Move,

});

this.setFromCursor(result.cursor);

...

Figure 5. A diff for the simplest edit in the “specialized edit” category (Section 6). The “extend” option wasreplaced by a “mode” option, with the boolean value changed to a corresponding enum value. This diff only shows3 of the 4 edited locations. See Forest commit 245877a for all changes.

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

A.11 8d8ac11f5d8faae6868d7a003beb7b04eeea5ed0

...} else if (ev.key === "k") {

this.cursors = this.cursors.map(

(cursor) =>

cursorMoveInOut({

root: this.doc.root,

cursor: cursor,

direction: CursorMoveInOutDirection.Out,

bigStep: false,

}).cursor,

);

} else if (ev.key === "K") {

...} else if ([")", "]", "}", ">"].includes(ev.key)) {

this.cursors = this.cursors.map(

(cursor) =>

cursorMoveInOut({

root: this.doc.root,

cursor: cursor,

direction: CursorMoveInOutDirection.Out,

bigStep: true,

delimiter: ev.key,

}).cursor,

);

...

...} else if (ev.key === "k") {

this.multiCursorHelper(

(strict) =>

multiCursorMoveInOut({

root: this.doc.root,

cursors: this.cursors,

direction: CursorMoveInOutDirection.Out,

bigStep: false,

strict,

}),

(result) => {

this.cursors = result.cursors;

},

);

} else if (ev.key === "K") {

...} else if ([")", "]", "}", ">"].includes(ev.key)) {

this.multiCursorHelper(

(strict) =>

multiCursorMoveInOut({

root: this.doc.root,

cursors: this.cursors,

direction: CursorMoveInOutDirection.Out,

bigStep: true,

delimiter: ev.key,

strict,

}),

(result) => {

this.cursors = result.cursors;

},

);

...

Figure 6. A diff for an edit of slightly above average complexity in the “specialized edit” category (Section 6).Instead of handling cursors directly using “this.cursors.map”, the helper function “multiCursorHelper” isintroduced. The passing of the “cursors” argument and the handling of the return value changes accordingly.Additionally a new “strict” option must be forwarded. This diff only shows 2 of the 5 edited locations. See Forestcommit 8d8ac11 for all changes.

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

A.12 44df2e6ba178b33e075e554565a615216338f476

...buildDoc: withExtendedArgsList(

({ nodeForDisplay, childDocs, newTextNode, updatePostLayoutHints }) => {

({ childDocs, newTextNode, newFocusMarker }) => {

updatePostLayoutHints(nodeForDisplay.id, (oldHints) => ({

...oldHints,

styleAsText: true,

}));

return groupDoc(

return groupDoc([

newFocusMarker(),

childDocs.map((c, i) =>

...childDocs.map((c, i) =>

i === 0

? c

: groupDoc([

leafDoc(newTextNode(", ", LabelStyle.SYNTAX_SYMBOL)),

c,

]),

),

);

]);

},

),

...

Figure 7. A diff for the most complex edit in the “specialized edit” category (Section 6). Calls to“updatePostLayoutHints” within a “buildDoc” method are removed. Instead, a call to “newFocusMarker” is addedto the an array literal in the returned “groupDoc” call. If “groupDoc” does not contain an array literal, it is createdand the old content is spread. Required/unused arguments are added to the “withExtendedArgsList” and“withExtendedArgsStruct” calls as needed. This diff only shows 1 of the 5 edited locations. A second edited locationis shown in Figure 8. See Forest commit 44df2e6 for all changes.

Forest: Structural Code Editing with Multiple Cursors Philippe Voinov

...buildDoc: withExtendedArgsStruct(

["dotDotDotToken", "name", "questionToken", "type", "initializer"],

({

nodeForDisplay,

updatePostLayoutHints,

shouldHideChild,

childDocs,

showChildNavigationHints,

newTextNode,

newFocusMarker,

}) => {

if (showChildNavigationHints) {

return undefined;

}

updatePostLayoutHints(nodeForDisplay.id, (oldHints) => ({

...oldHints,

styleAsText: true,

label: [],

}));

...return groupDoc(

filterTruthyChildren([

newFocusMarker(),

!shouldHideChild("dotDotDotToken") && childDocs.dotDotDotToken,

childDocs.name,

!shouldHideChild("questionToken") && childDocs.questionToken,

!shouldHideChild("type") && typeWithColon,

!shouldHideChild("initializer") && initializerWithEqualsSign,

]),

);

},

),

...

Figure 8. The second edited location described in Figure 7.