Git Product home page Git Product logo

simpletalk's People

Contributors

approximateidentity avatar darth-cheney avatar dkrasner avatar launeh avatar mdeland avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

simpletalk's Issues

Proposal: Custom Text Editor for Field

What

This proposal outlines a custom text editor component that can be use as/in our Field Part.

Why

We have wrestled endlessly with the DOM's existing contenteditable capability, and continue to battle against its shortcomings. These shortcomings have stymied our ability to predictably and consistently deal with the structure of text and styling in our Field part.

Shortcomings include:

  • Inability to have full control over the cursor;
  • Differences across browsers in the handling of newline insertion (ie, what happens when you push the return key);
  • Inability to "smartly" move the next insertion outside of some parent element. Because text looks flat but the HTML representation is a tree, this presents many problems when attempting to style/group text ranges

Component Proposal

I propose that we create a new, vanilla webcomponent that deals with basic text editing in terms that we define and control. It will have several design rules, outlined below, that will allow us to have predictable, testable behavior, and that will have a structure that makes it more amenable to our Field behaviors.

Mirrored text and visual content

The new text editor will be composed of two simultaneously interacting sets of data: the source text (a string) and the visual display of that text (the html markup). The component will handle updating both of these at the same time during change operations, which include insertions and deletions.

At any time in the lifecycle of a program, the stored text will be said to match the contents of the HTML's text nodes as if they were extracted and appended together (with the exception of newlines, see below).

Additionally, styling on ranges of the text will be stored as special objects that provide the start and end points of such stylings within the text. These, too, will be linked too and mirrored on the visual (HTML) side.

Keeping Selections in Sync

The question of how to keep selections in sync is crucial. For example, how can we create a DOM Range object that correctly maps to the same substring of the source text (and visa versa)?

This can be solved with relative ease using the createTreeWalker document method, like so:

    _getTextNodes(){
        let nodes = [];
        let walker = document.createTreeWalker(
            this,
            NodeFilter.SHOW_ALL,
            null
        );
        let currentNode = walker.nextNode();
        while(currentNode){
            nodes.push(currentNode);
            currentNode = walker.nextNode();
        }
        let lineAdjustedNodes = [];
        nodes.forEach(node => {
            if(node.nodeType == Node.TEXT_NODE){
                lineAdjustedNodes.push({
                    str: node.nodeValue,
                    node: node
                });
            } else if(node.nodeName == this.lineElement.toUpperCase() && node.classList.contains('st-text-line')){
                if(lineAdjustedNodes.length){
                    let prev = lineAdjustedNodes[lineAdjustedNodes.length - 1];
                    prev.str += "\n";
                }
            }
        });
        return lineAdjustedNodes;
    }

Let's break down what is happening. The point of this function is to create a list of dictionaries, each of which has two values: a string version of the text at the given DOM text node, and a reference to the actual node that it comes from. We walk the tree of all DOM nodes (this includes both elements and text nodes), attempting to isolate text nodes for the most part. The only exception here is if we come across the special .st-text-line span (our central organizing idea, see below). In this case, we insert an additional newline into the stored string representation of the previously created dictionary.

If we have ensured that the presence of this line element corresponds with a newline (see below), then this will always yield the correct raw text value, and should be equivalent to the stored raw text value. What's important is the coupled information about the nodes -- this allows us to create Ranges with the correct start node, start offset, end node, and end offset.

Lines: the primary organizing property

Lines will be the main organizing principle of the contents of the text editor. A line has two related definitions in this component: a text definition and a visual definition:

  • text line - Any set of valid, non-newline display characters that terminates in a newline character (or a newline character that is directly preceded by another newline character)
  • visual line - A <span> element whose classList contains st-text-line

For the visual representation, there are some additional rules. First, DOM Text Nodes will always be enclosed by one of these line <span> elements, either as a direct parent or ancestor (styling or syntax highlighting spans can serve as intervening ancestors). This means that a Text Node will never be the direct child of the component element itself, and all text can be found by come collecting of line <span> elements.

Second, the insertion of a newline into the text will create a new text line <span> element and update the cursor location (see below about the cursor). Likewise, the removal of a line of text from the text source will result in the whole corresponding visual text <span> being removed (and visa versa).

Examples

Let's say we have the following source text:

This is the first line\nThis is the second line

It will be displayed as:

<span class="st-text-line">This is the first line</span>
<span class="st-text-line">This is the second line</span>

However, if there are some styling ranges associated with some parts of the text -- for example, bolding the second and third word of each line respectively, the visual representation could also look like this:

<span class="st-text-line">This <span class="st-styling style="font-weight:bold;">is</span> the first line</span>
<span class="st-text-line">This is <span class="st-styling style="font-weight:bold;">the</span> second line</span>

The Cursor

One central advantage of this custom approach is that we have complete control over the cursor.

For this component, the Cursor will be a <div> element with a special class. We will style it to look just like a normal cursor (we can even animate it to "blink" if we want).

The Cursor div, like all DOM text nodes, will always live inside of a <span class="st-text-line"> ancestor. Because we can query the location of the cursor and easily get information about its ancestors/siblings, we can more easily perform intelligent insertions and deletions.

For example, here is a rough prototype of what might happen during the handling of a backspace:

    visualDeleteBackward(){
        // If the cursor is the first child node
        // in its parent, then we move the cursor to
        // the end of the parent's previous sibling
        // and erase the parent.
        if(this.cursor == this.cursor.parentElement.childNodes[0]){
            console.log('Cursor is first child node');
            let targetElement = this.cursor.parentElement;
            if(targetElement){
                targetElement.previousElementSibling.append(this.cursor);
                targetElement.remove();

                // If the removed element was a text line
                // element, it's the equivalent to deleting a newline
                // in the underlying textarea. So we do not recursively
                // call the delete backward action.
                if(!targetElement.classList.contains('st-text-line')){
                    return this.visualDeleteBackward();
                }
                return true;
            }
        }
        let previousSibling = this.cursor.previousSibling;
        if(previousSibling.nodeType == Node.ELEMENT_NODE){
            console.log('previousSibling is element');
            previousSibling.append(this.cursor);
            return this.visualDeleteBackward();
        }
        if(previousSibling.nodeType == Node.TEXT_NODE){
            console.log('previousSibling is text node');
            if(previousSibling.nodeValue == ""){
                console.log('previous sib text node is empty');
                previousSibling.remove();
                return this.visualDeleteBackward();
            } else {
                let val = previousSibling.nodeValue;
                previousSibling.nodeValue = val.slice(0, val.length - 1);
            }
        }
        return false;
    }

Here's the gist of this code. When a backspace happens, there are several possibilities:

  1. The cursor div is directly preceded by a text node. If the node is an empty string, delete the text node and then recursively call the function (since nothing "visual" has been deleted yet); otherwise, delete the last character of that text node.
  2. The cursor div is directly preceded by a sibling element. Append the cursor div to the end of that sibling element, then recursively call the function;
  3. The cursor div has no preceding node at all, and therefore is the first child node in its direct parent. If the parent has no other child nodes inside of it at all, this means it is empty and should be deleted entirely from the DOM. Append the cursor to the end of its parent's parent, then remove the original parent completely. Otherwise (if there are DOM node children in the parent), append the cursor to the end of the parent's parent. In both cases we recursively call the function

There is a little more to this when it comes to removing <span class="st-text-line"> elements, however, since they also require removing a newline char from the underlying source text representation. But the complete control is simple enough to handle and this should give you an idea of the advantages of a completely controlled cursor.

Styling Ranges

We want to be able to style specific substrings of the source text easily. This can be done using some structured collection (a dictionary, list, or similar) of custom styling range objects, which all have the following properties at minimum:

  • Start index of the substring in the full text representation;
  • End index of the substring in the full text representation;
  • Something about what styling to apply (perhaps a class or the contents of the inline style= attribute

When used in combination with something like the tree walking function described earlier, we have enough information to reliably add <span> wrappers around the DOM nodes corresponding to the substring.

There is one complication, however. What if a given styling range crosses several dom nodes that are not contained in the same hierarchical subtree? For example, imagine a styling range that encloses all of one line and only half of the next line. What do we do in this case?

Because lines are our primary organizing element, when applying the style range we can simply break the resulting style <span> elements between them. In the example case, the result would be two <span> elements: one as the sole child element of the first line (thus enclosing all nodes within the line) and one that encloses only the sub-substring nodes of the second line. Both are easily accomplished using the DOM Range API, since we have easy access to correct node and offset information via the treewalker function.

Syntax Highlighting

Syntax highlighting is extremely error-prone and difficult, but it is especially so under the plain contenteditable system. This is because wrapping bits of parsed text in <span> elements corresponding to some grammatical aspect is not like styling ranges! There is much more structure in wrapped bits of syntax.

Consider this example. In Simpletalk, the MessageHandler rule is a grouping of a MessageHandlerOpen, StatementLines, and a MessageHandlerClose. Each of those parts also has their own set of constituent parts that would need to be highlighted too, but ignore those for now. The important point here is this: how do I know if the lines I am actively typing are "inside" of a MessageHandler? In other words, if I am typing in the body of a handler and hit the return key, what prevents the text editor from creating a new <span class="st-text-line"> outside of the enclosed handler rather than in it? And, furthermore, how to I hit return to make a newline outside of the handler?

With contenteditable this was a real nightmare. However, under this proposal we have access to more useful information. Because we know the cursor position, the fact that line spans are the organizing principle, any syntax highlighter can so with the cursor what it needs to, as well as deal with any custom wrapping elements it inserts. For example, once a MessageHandlerOpen is parsed and highlighted, any subsequent <span class="st-text-line"> elements can be assumed to be inside of a potential enclosing handler. It's only when pushing return at the end of a parsed MessageHandlerClose that a newline will then be created outside of the enclosing handler.

If we take this route, I'm confident we will find some hiccups with syntax highlighting. But the complete control afforded to any highlighter object will be paramount to making this all work correctly. The new custom text editor is exactly what can give it such control.

Questions / Comments?

@dkrasner @ApproximateIdentity Let me know what you think about the outline of this proposal, and any questions / potential issues you have.

I have prototyped a vanilla version of some of what I am talking about and it seems to be working well so far.

Pinning does not fix top/left properly

Main Points

Setting the pinning should fix the corresponding top and left properties. Otherwise you will get weird effects where, for example, the position properties keep changing but the element is not being moved in the corresponding direction.

Example:
Take any view and set "pinning" to "bottom". As you move it around the element will be pinned to the bottom but you'll see the top property continue to update.

Layout icons

Perhaps it would be nice to start adding layout icons, either to menus or to the halo.

For example, we could add border, list row/col icons from here (search for "design" category) to the area halo or to the button menu

Layout Examples Stack Broken

What

Many of the example buttons on the layout-examples snapshot no longer work. This is due to changes to the ST language and the old scripts that are present. Go through and update all the button scripts.

Deserialized script editor does not save script

Main Points

If you save/serialize a Snapshot with a scripting editor open, it will be there when you load/de-serialize. However, the "save script" button will fail to update the target part's script property.

The reason for this is that the "save script" button does not have a proper script. The handler is defined under the hood and is added when System responds to an openScriptEditor command. The net result is that the button is de-serialized but has no click handler.

We should either add the script there - sorting out the preventing issue which sends some sort of infinite recursion error - to the button or build the entire editor in ST (related to #150)

Show me more

We will need embedded teaching within the environment, which should be incremental and keep track of where and when the person was last time of use.

Nav stack view on new stack

try this:

add stack
add card to second stack

click on the stack in the nav and you'll see

image

after some clicking around it will become normal

ST for sub-part reordering

Sub-parts of list layout parts (like area) don't have any way to reorder themselves.

We should update a part property (calling it something like order number or order-number) and add a handler on view which will effectively reorder the corresponding DOM element in the childrens' list.

In addition, we should consider adding some grammar and semantics for this, to make it more natural, i.e. instead of just tell button "My Button" to set "order-number" to 1 we could have tell button "My Button" to be first ...

Drag and Drop

What

Drag and drop, which is at the moment half implemented and based on DOM drag events, needs to be updated and replaced.

With layouts and Area parts coming into the fold, I suggest we implement our own drag and drop operations, since these will be more conducive to Halo movement and we can have more fine-grained control over what gets dropped.

Image Halo Resizing UX

We need to rethink how the halo operates during resizing on Images that preserve their aspect ratio.

One option is to adopt the same behavior as SVG currently does (resize the bounding area exactly, but preserving the aspect ratio of the image element inside that box), then "snap" it back to shrink-wrap the actual container.

Another option is to have the resize button move exactly with the mouse (Squeak-halo style) while the resizing is occurring, then to have it snap back to its correct location once a resize has completed.

Editors should be written in ST

Main Points

Editors should be replaced by window parts/subparts like toolbox, or if this is not possible for some reason rewritten to have proper model/view separation.

This will ensure that updates, part property changes + subscriptions, move smoothly between the editor target the editor window

Current property should be an ID not an index

Main Issue

The current property on world (for stacks) and stack (for cards) is an index, i.e. it's the list order of the card that is current. This works fine with adding new stacks/cards, since indexes remain valid when a list grows, but creates issues when removing them, i.e. the current property does not behave the same way with add (function) and delete (its inverse) which is bad design bound to cause issues.

For example, if we want to allow delete current card we immediately hit race conditions, such as the one described in #217. Moreover, we are likely to have all sorts of issues if we allow for stack and card reordering on the fly, combined with adding and deleting.

Related to re-ordering as described in #182

Target and area

main points

Using the halo target selector for elements in an area does not work. Only the area itself is selected

Image fetch

Main issue

For many urls, fetch(url) will throw a CORS error. We could get around this by letting the browser grab the image under the hood with something like imgElement._src = url but that will screw up our flexible (img, svg) image part.

Does it make sense to use an addon like this?

https://addons.mozilla.org/en-US/firefox/addon/cors-everywhere/

Or if using chrome/chromium to use the flag --disable-web-security when starting up? Maybe as a policy we require that to be our "runtime"?

Another possibility might be to bundle this with electron (there's info on disabling CORS here: electron/electron#23664 )

Put all icon resources in a single module

Right now we have inlined icon SVG strings inside of the various webcomponents that make use of them (Halo, for example). However we are starting to re-use a lot of these icons as they become a part of our UI. Instead, we should have a central module that holds all of our SVG icons by name and then import that wherever needed.

Field view can't find textarea

loading a serialized snapshot with populated fields can cause the following (breaking) error:

Uncaught (in promise) TypeError: this.textarea is undefined
    styleTextCSS FieldView.js:221
    setModel PartView.js:138
    setViewModel serialization.js:296
    deserializeData serialization.js:143
    deserializeData serialization.js:141
    deserializeData serialization.js:99
    deserialize serialization.js:63
    deserialize System.js:629
    initialLoad System.js:87
    js System.js:1062
FieldView.js:221

Live todo

  • text-overflow on buttons (and windows)
  • not id for any new part, just call "new X"
  • use the material toolbox
  • move the default image svg inline
  • context menu for toolbox hide/unhide
  • make sure tensorflow is not being loaded

viewChanged and new views

Main Points

Currently new views are created by sending a type viewChanged/ name subpart-new message to the parent (see for example here).

The issue is that we are sending along the entire view in the message which is going to cause issues if we plan on serializing these messages and doing remote communications in the future.

Moving the new view creation into the parent element is easy, but this is not compatible with the current de-serialization framework where all views are first created and then added

Debugging Info should appear in Window

For now, debugging info (grammar, available command names) will appear in a Field in the current Card context. This presents problems when the Card itself is using a list layout, since the field(s) will appear in a pre-determined position that might be hard to access.

We should have all debugging information appear inside of Windows instead.

Go to should accept expressions

Go to commands should accept expressions

For example,

go to card id the "id" of first card
go to card id myVariable
and related should be valid

On Chunking

Chunking: Proposals and Discussion

What is Chunking?

(For more detail, see PDF page 143 of The HyperTalk manual)

The original Hypercard/HyperTalk system made use of a kind of querying and manipulation pattern it called 'chunk expressions.' These expressions are essentially queries on types of objects that can be iterated upon, by the referring to indexes of named 'chunks'.

For example, consider the following HyperTalk expression:

first character of word 7 of third line of card field 1

This expression tells the system to get the value of the first field on the card (more on values later), which is text. It then says to 'chunk' that text into types of chunks called 'line', then get the third line in a sequence of such chunks. It then says to further chunk that smaller line/chunk of text into a type of chunk called 'word' -- and so forth.

Chunking is an important part of the HC system, and it was a powerful feature for users. Though it results in somewhat verbose scripting if wielded too often, there is never any doubt about "what is going on"

When implementing a generalized chunking system, we want to consider use cases that HC didn't even have the opportunity (or need) to reckon with, including:

  • Referencing or chunking objects whose references are remote;
  • Chunking objects like responses from different types of remote requests;
  • Chunking things like JSON?;
  • Chunking bits and bytes, if needed, in some low-level reach for scripting
  • Mathematical constructs
  • Table structures (chunks of rows, columns, or cells)

I argue that because we will probably want at least a couple of these speculative abilities for chunking, we really need to think hard about how the chunking implementation should be structured.

Implementation Questions

I have pushed work to a couple of branches that does two different types of basic implementation for chunking. They have important features in common, which I will explain below. There are probably other methods that I haven't even thought of, and I hope that others will have suggestions.

The Most Primitive Chunkables: Strings and Arrays

In our HyperTalk example above, you'll notice that the expression is really just a kind of recursive 'chunking' of ever-smaller strings using different kinds of chunks (line, word, then character).

Perhaps the most common chunkable object type in the system is a String. Another type of thing that can be chunked is an Array of values. This tell us something important about the kinds of objects that should be chunkable: they are sequences of some kind.

With that in mind -- and given that we are implementing this system in Javascript -- I assumed for my implementations here that it makes the most sense to add to the base Array and String constructors in JS by giving them new 'chunking' abilities. This should handle 99% of our earliest and most common uses for chunking.

Approach 1

In my first implementation, which you can find at eric-chunking, only implements retrieval of chunks for a given object (more on this later). It does this by directly adding properties to String.prototype and Array.prototype, so that all Arrays and Strings in the system will have chunking capabilities. The implementation is small enough to include inline here:

/* JS String Chunking */
String.prototype.chunkers = {
    line: function(){
        return this.split("\n");
    },

    character: function(){
        let chars = [];
        for(let i = 0; i < this.length; i++){
            chars.push(
                this.charAt(i)
            );
        }
        return chars;
    }
};

String.prototype.chunkBy = function(aChunkType){
    if(!this.chunkers){
        return null;
    }
    if(!Object.keys(this.chunkers).includes(aChunkType)){
        return null;
    }

    return this.chunkers[aChunkType].bind(this)();
};

/* JS Array Chunking */
Array.prototype.chunkers = {
    item: function(){
        return this;
    }
};

Array.prototype.chunkBy = function(aChunkType){
    if(!this.chunkers){
        return null;
    }
    if(!Object.keys(this.chunkers).includes(aChunkType)){
        return null;
    }

    return this.chunkers[aChunkType].bind(this)();
};

The short explanation: each prototype now has an object called 'chunkers' whose keys are the names of kinds of chunks it can make of itself (think line, word, or character) mapped to functions that will produce those chunks. In order to call these functions with the correct context binding (ie, the string instance itself as the this keyword), a consumer will call someString.chunkBy('line') or whatever chunk name they are looking for. The return value, as it should be with anything that chunks, is an Array of the chunks.

This approach is fairly small, simple, and easy to grasp. However it leaves out a crucial capability of HC/HT that we have not discussed yet: scripters should be allowed to not only get the values of chunks, but also to set them.

Consider this HyperTalk script:

put "hello" into the second word of the third line of card field 2

Our simple implementation above will not be able to handle this setting. This is because it requires that we somehow keep track of the original path of the nested chunkers, and also implement a more universal way of "setting" something at that index.

For Array the pattern is simple, because Arrays only have one chunk type (item) and setting a value at an index of an array. But Strings in JS are pretty much immutable -- you can't modify them in place. Furthermore a word can be a subchunk of a line, but once we've pulled out a given line to chunk into words, we no longer (in the chunker function) have any concept of where the original lines came from, and we therefore cannot update the object of origin.

This is currently my biggest mental block to implementing chunking. My second pass attempts to deal with this complexity.

Attempt 2

Unfortunately, my next implementation is more complicated. You can find it on the branch eric-chunking-alt1.

Like the previous implementation, we are still adding to base String and Array prototype objects. We also still have a chunkers object, but rather than having its keys be chunknames referring to functions for making chunks, these instead now refer to a generic object that has three required methods of its own:

Method Name Description
getAll() Returns an array of all chunks of the given kind
getAt(anIndex) Gets the single chunk of the given kind at the specified index
setAt(anIndex, aValue) This is key: this method returns a new copy of the original object, with the chunk at the provided index modified to be the passed in value

You can find the implementation here. I won't post it into this issue since it's a big longer.

How Else?

Given the summary above, how else could we implement generalized chunking? How can we deal with this "setting" issue in a clean, simple way?

Quick Note on 'Values'

In the original HyperTalk/Hypercard system, certain of the available Part objects also had 'values'. In practice this meant mapping one or more of the Part's properties to be its 'value', where a scripter could get or set. For example, the 'value' of a Field is simply a mapping to its textContent property (another way of saying a Field's value is its current text). This manifests itself in script as something like put "hello" into card field 1 (where put is setting the 'value' of card field , which is a mapping to its text property).

The 'values' of parts are designed for the scripter/user, and therefore have mapping that we as programmers might find unintuitive. For example, the 'value' of a Button part is the title that appears inside of the Button. put "click me" into button 3 will set the value of the Button's title to the given string. first character of button 3 will likewise get the 'value' of the button (its title text), chunk by characters, and return the first one -- in this case 'c'.

Both of my implementation branches above have accounted for the 'values' of Parts by introducing a new superclass for Part called ValueHolder, which expects a getter/setter for .value. The default implementation is to do nothing.

Open Questions

  1. What is a reasonable implementation that gives us the universality we are hoping for?
  2. What use cases can we imagine for chunking?
  3. Do we even want to implement chunking at all, or do something completely different (or not at all)?

--------------------------- @ApproximateIdentity

I'm not totally sure I understand why these two are so different:

first character of word 7 of third line of card field 1

put "hello" into the second word of the third line of card field 2

Why not do it all at the "pointer" level? I.e. you implicitly change first character of word 7 of third line of card field 1 to get the first character of word 7 of third line of card field 1 internally. Then you basically view first character of word 7 of third line of card field 1 as returning a pointer. This way the get the is sort of like a dereferencing/access operator while the put ... into is basically a dereferencing/assignment operator. I mean could get a little tricky to get right, but it doesn't seem that bad to me.

That said I really feel like the second approach. In this case the generality seems to be a bit of a feature to me. I.e. by making it general you restrict the possible solutions. This seems good to me.

But I guess I have a final question. Is actually that important if you do the more general version first? I.e. is the more general version backwards-compatible with the less general version? In that case you'd just be saying that you can use chunking with some restrictions and then later you might remove those restrictions. In that case, the less general implementation doesn't really seem to lose anything to me. In other words, doing the (seemingly) simpler more specific caseisn't any long-term loss and might clarify thinking anyway. In that case, why not do the simpler more restricted case first?

------------------------- @dkrasner

Essentially "to chunk" is an operation that transforms an object into an array (or array-like object, i.e. something that has a lookup based on passed key, see more below), which can then be indexed (looked up), the items in the array manipulated (gotten, set, etc), and potentially "de-chunked" i.e. put back together

Various chunk references, like "word" "line" "5th" etc, are really just incidental. All they say is <> where the appropriate thing is known via context.

Another way to think about this is that a statement like put "hello" into the 5th word of the 10th line of card field 1 is equivalent to something like set("hello", chunk(5, chunk(10, chunk(1, card)) i.e. a nested set of chunk operations where each object understands what it is to be chunked (and then subsequently returns the corresponding index value).

I am imagining a setup where any object can accept a message of type: chunkMe, element: N to which it will either return DoesNotUnderstand or the Nth element of the "array"

I put array in quotes b/c in theory we can return whatever the thing is referenced by element: N - this could be a value in a json/dict where N is the key, or a subpart (put "hello" into button "hello button" of card 5 of stack1)

In addition, every object knows how to deal with a change to one of its chunks (i.e. a set-like operation) which then updates the value, or whatever "value" means for said object; or alternatively every object out there is made up of "atomic" parts, i.e. those irreducible by chunking (characters, basic objects - buttons, cards...), and a get operation recursively puts any object back together from its chunks (character -> word -> line -> paragraph -> field -> card ..)

----------------------- @darth-cheney

@ApproximateIdentity

Why not do it all at the "pointer" level? I.e. you implicitly change first character of word 7 of third line of card field 1 to get the first character of word 7 of third line of card field 1 internally. Then you basically view first character of word 7 of third line of card field 1 as returning a pointer. This way the get the is sort of like a dereferencing/access operator while the put ... into is basically a dereferencing/assignment operator. I mean could get a little tricky to get right, but it doesn't seem that bad
to me.

Totally makes sense. The only issue currently is that we cannot easily set values based in these pointers when we are chunking a String, because the Strings cannot be modified in place (though basic Arrays can).

@dkrasner

Another way to think about this is that a statement like put "hello" into the 5th word of the 10th line of card field 1 is equivalent to something like set("hello", chunk(5, chunk(10, chunk(1, card)) i.e. a nested set of chunk operations where each object understands what it is to be chunked (and then subsequently returns the corresponding index value).

This is mostly right, though the recursive chunking pseudo-code you've provided does not take into account the kind of chunk being produced. In the case of plain arrays there is only the itemchunk. For Strings we have (for now) line, word, and character. It just so happens that these are hierarchical and will reduce, but can we imagine kinds of objects that provide chunks that won't necessarily reduce hierarchically (for example, an array of more complex objects that themselves are chunkable somehow)?

In addition, every object knows how to deal with a change to one of its chunks

This would be the ideal scenario for sure. Perhaps for the moment we can say that since the "ultimate source" of the initial chunking will be from a Part, we attach this chunking behavior only to Parts. For example, the Field part will know how to chunk its own value (which is the text content) into lines, words, and characters and likewise how to set based on indices of these chunks. That gets rid of some of my problems with modifying String.prototype and also gets us closer to Thomas' pointer notion.

What we would not be able to do in this case is something like character 2 of second word of "hello world", since the "ultimate source" would be a literal string and not a Part. I don't know a way to make a consistent interface for both actual Strings and Parts whose values are strings.

What I was trying to leave space for was future remote data returns et cetera -- information coming in that could also be 'chunkable.' But as long as we make the chunking interface on objects consistent then we can just use some kind of polymorphism when we need to. Double true if we implement this in messaging.

-------------------------------- @ApproximateIdentity

Why not do it all at the "pointer" level? I.e. you implicitly change first character of word 7 of third line of card field 1 to get the first character of word 7 of third line of card field 1 internally. Then you basically view first character of word 7 of third line of card field 1 as returning a pointer. This way the get the is sort of like a dereferencing/access operator while the put ... into is basically a dereferencing/assignment operator. I mean could get a little tricky to get right, but it doesn't seem that bad
to me.

Totally makes sense. The only issue currently is that we cannot easily set values based in these pointers when we are chunking a String, because the Strings cannot be modified in place (though basic Arrays can).

Yeah I think that it's totally fine to change the string class to some sort of STString class and just add whatever methods you need. The fact that you've been able to represent your strings by js strings until now is just sort of a lucky accident. But if it no longer fits, then can replace the implementation.

--------------------------------------- @dkrasner

This is mostly right, though the recursive chunking pseudo-code you've provided does not take into account the kind of chunk being produced. In the case of plain arrays there is only the itemchunk. For Strings we have (for now) line, word, and character. It just so happens that these are hierarchical and will reduce, but can we imagine kinds of objects that provide chunks that won't necessarily reduce hierarchically (for example, an array of more complex objects that themselves are chunkable somehow)?

All I meant by that pseudo code is that any one object that we interact knows how to "chunk" itself (understands the message "chunk" etc), or is an atomic object that does not chunk and responds accordingly. The output, whatever output here means, of chunk<- get(N) is another object. IE both chunk and get are polymorphisms and it's up to the objects to deal with what needs to happen.

So yes you can have something like

MyStack -> chunk() -> get(5) -> chunk() -> get(7)  -> chunk() -> get(4) -> chunk() -> set(1, "NewVar")
//stack    ->       //5th card        ->// 7th field (a math eqn)  -> // 4th eqn part     // 1st variable in eqn is now set to "NewVar"

all of the above is trying to show is that chunking here can produce Parts, fields, weird things we haven't defined like a math expression/equation which knows how to chunk itself (which might not mean transforming itself into an array at all), etc

This would force us to add a chunk message command handler to every object out there, with the default being DnU, i.e. atomic or unchunkable (there could be a distinction between these two states)

-------------------------------- @dkrasner

In addition, every object knows how to deal with a change to one of its chunks

This would be the ideal scenario for sure. Perhaps for the moment we can say that since the "ultimate source" of the initial chunking will be from a Part, we attach this chunking behavior only to Parts. For example, the Field part will know how to chunk its own value (which is the text content) into lines, words, and characters and likewise how to set based on indices of these chunks. That gets rid of some of my problems with modifying String.prototype and also gets us closer to Thomas' pointer notion.

I think the distinction on sending "chunk" to a Part or to a Part's value (note I imagine that Part.value is also an object which understand what to do when it gets the "chunk" message), is going to be mostly a problem in grammar/semantics and handled by the compiler. We would need to make sure that this distinction is clear in the grammar itself.

For example, field 2 of card is chunk operation on a part Card while paragraph 2 of field is a chunk operation on Field.value.

What we would not be able to do in this case is something like character 2 of second word of "hello world", since the "ultimate source" would be a literal string and not a Part. I don't know a way to make a consistent interface for both actual Strings and Parts whose values are strings.

If Part.value is an object that understands both "chunk" and "represent" then character 2 of second word of "hello world" would "return" a Part.value object :

Part.value.representation()
> "world"
Part.value.chunk(2)
> O // Value type Object
O.representation()
> "o"

What I was trying to leave space for was future remote data returns et cetera -- information coming in that could also be 'chunkable.' But as long as we make the chunking interface on objects consistent then we can just use some kind of polymorphism when we need to. Double true if we implement this in messaging.

------------------------------------------ @darth-cheney

In addition, every object knows how to deal with a change to one of its chunks

This would be the ideal scenario for sure. Perhaps for the moment we can say that since the "ultimate source" of the initial chunking will be from a Part, we attach this chunking behavior only to Parts. For example, the Field part will know how to chunk its own value (which is the text content) into lines, words, and characters and likewise how to set based on indices of these chunks. That gets rid of some of my problems with modifying String.prototype and also gets us closer to Thomas' pointer notion.

I think the distinction on sending "chunk" to a Part or to a Part's value (note I imagine that Part.value is also an object which understand what to do when it gets the "chunk" message), is going to be mostly a problem in grammar/semantics and handled by the compiler. We would need to make sure that this distinction is clear in the grammar itself.

For example, field 2 of card is chunk operation on a part Card while paragraph 2 of field is a chunk operation on Field.value.

What we would not be able to do in this case is something like character 2 of second word of "hello world", since the "ultimate source" would be a literal string and not a Part. I don't know a way to make a consistent interface for both actual Strings and Parts whose values are strings.

If Part.value is an object that understands both "chunk" and "represent" then character 2 of second word of "hello world" would "return" a Part.value object :

Part.value.representation()
> "world"
Part.value.chunk(2)
> O // Value type Object
O.representation()
> "o"

What I was trying to leave space for was future remote data returns et cetera -- information coming in that could also be 'chunkable.' But as long as we make the chunking interface on objects consistent then we can just use some kind of polymorphism when we need to. Double true if we implement this in messaging.

---------------------------- @ApproximateIdentity

I must be missing something here because I can't understand why we don't just chunk objects into arrays of other objects. I.e. if we were looking at things at the type-level the function would be something like this:

    Chunk: A -> List[B]

Then there could be an inverse function:

    Join: List[B] -> A

In the simple cases we might have Chunk splitting a "paragraph" into an array of "sentences". In that case we would just have our other operations being regular indexing. I.e. if you want to grab the third chunk, then you just grab the third element of the array. If you want to set the third chunk then you do this:

  1. Chunk: A -> List[B]
  2. Replace the third value with the new one.
  3. Join: List[B] -> A

That would take a paragraph and then return a new paragragh where the third sentence is replaced.

There are of course details here, but I feel like the idea is totally straight-forward. Am I missing something basic here? I mean I'm guessing I'm just re-hashing discussions you've had on calls and I don't want to second-guess those conclusions, but I just don't understand what is so fundamentally wrong here.

I think what might be helpful for me would be something like the following:

  1. Given an explicit example paragraph, what should "get the third sentence of" return? In other words, what is the desired functionality entirely ignoring andy questions of implementation.
  2. Given an explicit example paragraph, what should "put blah into paragraph" return? Once again ignoring implementation.

We have the option to define "paragraph" and "sentence" however we want. Should it be a string? Should it be an abstract object? What methods should it have? Given that I just don't see the issue here.

--------------------------------------- @darth-cheney

@ApproximateIdentity

I can't understand why we don't just chunk objects into arrays of other objects.

This is indeed the strategy I used in my previous comment. The rule in that example is that all chunking results in an array of Chunk objects.

the function would be something like this:
Chunk: A -> List[B]
Then there could be an inverse function:
Join: List[B] -> A

This is also what I had in the chunkers in the example above, except that I called it combine instead of join.

There are of course details here, but I feel like the idea is totally straight-forward. Am I missing something basic here? I mean I'm guessing I'm just re-hashing discussions you've had on calls and I don't want to second-guess those conclusions, but I just don't understand what is so fundamentally wrong here.

For me, it's precisely those details that are confusing. Consider these two examples:

put "b" into  the last character of the third word of line 10 of card field 1
put "b" into the last character of line 10 of card field 1

In the first example we are chunking down 3 times and in the second twice -- how does each kind of chunk know how to "join" itself with respect to the chunk it was chunked from? Should it have some knowledge about the "parent" chunk? Between the two examples, as far as I can tell, joined character chunks need to know to either join themselves as words or as lines. And since we don't want to set specific hierarchies, the objects that do this need to retain some information about the chunking abilities.

Additionally, in the first of those examples, we are not actually "setting" on any of the line or word chunks, but merely "getting" them to find a single word, then "setting" the appropriate character. This makes for awkward function calling.

This is the task I took up in my previous comment. I've already done what you suggested, I think. The problem is that the API is awful (see the very last test in the test file).

Given an explicit example paragraph, what should "get the third sentence of" return? In other words, what is the desired functionality entirely ignoring andy questions of implementation.

Since this question is asking about String chunking specifically, we probably want to return a String. In my latest implementation -- the comment above yours -- I decided it was easier to have it return as Chunk instance whose value is a String, but that might not be the way to go (ie, wrap everything in a Chunk before chunking). "Getting" has not really been a problem so far though.

Given an explicit example paragraph, what should "put blah into paragraph" return? Once again ignoring implementation.

This is a bit of an open question, and one of the reasons for this github issue. Strings are immutable in JS so we cannot change them in place. For Strings, setting any subchunk will ultimately have to return a copy of the whole modified String. Do we want to say now that we will always return copies of the original chunked item, modified?

We have the option to define "paragraph" and "sentence" however we want. Should it be a string? Should it be an abstract object? What methods should it have? Given that I just don't see the issue here.

Yes, but one of the points in opening this issue was to try and address those exact questions. For me this whole thing has been "easy in theory" but not in implementation. Now I could be making a complete mess of it by overcomplicating, and that's also kind of what I'd like to know. Strings will not be the only things that can be chunked, so I've been trying to find the most general pattern I can. We can always go the verbose route of making new object types for each chunk (ie StringWordChunk, StringLineChunk, FooObjectChunk, whatever) and attaching capabilities to those. But I hope it's clear why I was trying to avoid that.

-------------------------- @ApproximateIdentity
I think I understand what you're saying. I'm going to change the terminology a little to ignore any past implementation. Say you have the following pseudo code and concepts of producing sentences and words:

paragraph = "Sentence one. Sentence two."

paragraph.split_sentences() = ["Sentence one.", "Sentence two."]
paragraph.split_words() = ["Sentence", "one", "Sentence", "two"]

Then you have the following main question: Does joining words back up produce sentences or paragraphs or does it depend on what you produce?

This is actually just equivalent to us defining what semantics we want. This really makes me think it's even more important to write out explicit examples to see what we want to return. Without that I don't see how we can even think about implementations anyway.

By the way, the fact that JS strings is immutable doesn't really matter. We can write whatever implementation we want. Do we want "strings" (e.g. the class of string we use in SimpleTalk) to be immutable? Then we can do that. If we want it to be mutable we can have that as well. We'll just have to write the class/methods to match what we want. Internally somewhere we'll be storing using JS strings, but the fact that they're immutable there doesn't really have any effect on this because we pick our implementation no matter what we do.

The problem is that the API is awful (see the very last test in the test file).

Meant to address this. I think we should write up our idea of what we want in the simpletalk grammar not in a JS api. The JS api can be whatever we want. What I'd like to do is have a list like the following:

  • take the first word of the second sentence of "The boy fell down. It hurt." ---> "hurt"

Then once we have that we can start implementing it. But without that I'm not really sure where to take this. I mean I personally can't really say what implementation I like since I don't know the goal of the implementation.

Script Editor Never Scrolls

For the moment, the Script Editor field will never "go to scrolling," and instead the height of the field will grow continuously on the screen if you write longer scripts. We need to ensure that the Script Editor itself is constructed with good layout properties and that we can force overflow scrolling for the field.

Spreadsheet

Main Points

Perhaps we should add an st-spreadsheet part and view. There a number of reasons for this:

  • spreadsheets are part computer interface semantics and people are accustomed to them
  • they are one of the best mainstream computing environments out there
  • they could be used as our replacement for both a data structure and data store/db
  • building it out of parts like st-field, although not impossible, would lead to performance issues for sure
  • we already made a really good custom web component that is meant to handle massive (infinite) amounts of data and do so quickly in the browser - this was a huge pain and there is no good reason to redo all the work. Moreover, this web component is open source so we could just grab it and integrated as necessary.

Remove Handler Command

There could be instances where programmatically, or at least via message, we would want to remove handlers.

One example if the "doIt" wrapper that allows statement line execution. Since this add a doIt command handler to the underlying part/model it would be nice to remove it after "doIt" has been called...

on add and on delete lifecycle commands

It would be good to have a specific handler run every time a part i created and deleted. This would allow for part configuration and clean up within the script.

I believe for the latter HyperTalk had an on open handler but that doesn't really make sense to me when talking about buttons or stacks.

Since we have the add ... and delete .. . part commands we could do an on add and on delete handler, respectively. This would be pretty trivial to integrate.

Hand can initially be invisible when switching stacks if it is currently grabbing

This was found when messing with this PR UnitedLexCorp/SimpleTalk#190 but it is a little different.

Basically the when the hand grabs an object it sets itself as invisible and instead redirect move commands to that object. If you change the stacks while it is grabbed, then it will still be holding that object. This means that it sends moves to that same object even when on another stack until it is released. Once released it will only grab things on the correct stack, but that doensn't help with this corner case.

Basically whenever a stack change occurs and the hand is running it needs to force release any object it is holding.

Multiple resource parts for the same resource

Main issues

Creating multiple resource parts (&views) on the same resource can create all sorts of clash issues. For example, if the resource assume last call results to be stored somewhere and multiple parts are invoking these calls there is no good way to keep things in check.

Perhaps resources should be classes and resource parts create instances of those classes, or something JS similar.

Halo on nav

Main Issue

You can activate halo on Nav which can then get stuck and break the whole system.

Silent Warnings for Incorrect Refs

We need to ensure that actions that reference non-existing ObjectSpecifiers don't end up throwing errors. Instead, nothing should happen -- but we should have the ability to log warnings somewhere.

Card precedence is not preserved on serialization

Serializing and de-serializing does not guarantee order of parts, and can for example undermine card precedence.

One way to see this is to put a button on a stack and add a number of cards. You'll see the button "disappear" on some cards after de-serializing.

Tell and target contexts not compatible

Main Points

I'll illustrate this issue with an example:

Imagine we have a button and have set a target on it.

The following two scripts will do exactly the same thing:

on click
	tell target to set "text-color" to "red"
end click

and

on click
	tell target to tell target to set "text-color" to "red"
end click

which in my view is counter-intuitive. The reason for this is that target is interpreted in the context of the underlying script, which in this case the on click handler above. And this is true for any target in the script, even if it is an object specifier of a context switching tell command, is interpreted in the original context.

Halo Clipping / Refactoring

What

For a while now we've noticed several issues with Halos when used inside of complex layouts. The most important is the fact that a Halo of a part that is inside another clipped part will not be visible. Similarly, Halos inside of nested Areas might not receive click events appropriately.

Ideas

We need to think of how to deal with this. Taking the Halo out of the target Part's shadow dom is the only way to resolve some of the issues, but it will also present its own problems -- for example, movement will be more complicated and likely not as smooth.

One possibility is to only allow Halos to open in certain contexts, and to have a separate, system-wide editor pane that pops out elsewhere on the screen when a particular Part is being edited, but which has all of the functionality of the Halo too.

Smoother transitions

We should smooth out some of the transitions. For example, hide/unhide, adding/deleting a part, moving to a different card etc.

I don't think at the moment this needs to be a ST native property or utility functions which used stepping and transparency (although certainly in the future), but we can add some basic transitions to smooth things out where it's easy. Perhaps a first pass in pure css (keyframes etc) could be good but there are other options.

Button ellipses affect all buttons

The new ellipses on Button labels is affecting all buttons regardless of their layout or intended width. See the save button in Script Editor for an example.

We should make sure that ellipses only appear in buttons with a specific designated width.

New Nav Issues

There are two new issues with the Nav that might be related to some recent changes (though not sure which):

  1. Windows that are pasted into a Card or Stack will not have their contents updated in the lens-views of the Navigator. For example, pasting any toolbox will work in the main views, but Nav card/stack views will not even have the elements for the buttons in their lensed views;
  2. The StackRow lensed stack will not show the current card when it updates. It continues to show only the first card.

Make halo deal with "small" boxes

This grew out of this comment: UnitedLexCorp/SimpleTalk#52 (comment)

When re-sizing a halo-ed object if it gets too small the mouse's position starts to loose its tracking with the button that it originally clicked on.

An approach is to force the button to "release" when it no longer can make the box any smaller? I.e. a test which just checks if the a non-zero movement results in a zero-movement and then just emitting a release event right there? Then the user would need to click it again. I guess the hard part is that if it is at the edge of the size then it would mean that a click and movement (no matter how small) in the direction would force it to release.

I can think of a lot of partial solutions, but can usually quickly find a corner case that messes it all up. It's kind of tough not being able to hold the pointer in place. What about just allowing all elements to get arbitrarily small and just cutting off what you can't see? Why is there a minimum size?

Grammar parse tree tests and debugging

Currently our grammar tests are crude. We check that the grammar is parsed, or parse fails, and that specific rules apply.

It would be good to check the grammar tree, ohm objects to make sure that we are indeed getting the expect output (not just some output that is valid from the ohm perspective).

Further we should use this to provide more accurate information on grammar error debugging

Adding subparts (views) to stacks or world can break

Main Points

Example:

add stack to this world
add window to second stack
add card to second stack

Adding the card breaks because of the faulty logic here. The assumption there is that if the world, stack, have subparts then one of those subparts is a stack or card, respectively.

Adding subpart views should really be handled by the parts themselves, i.e. by the ownerView.

propertyName should accept "plain" literals

Current implement of the propertyName rule only accepts string literals. We should allow for literals (restricted) and potentially for Factors or Variables etc.

How we restrict literals is unclear but here is a proposal:

only lower case letters
"-" is the only non-aplhanumeric character
starts with a letter

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.