Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

✨ Placeholder Support #287

Open
austincondiff opened this issue Jan 8, 2025 · 4 comments
Open

✨ Placeholder Support #287

austincondiff opened this issue Jan 8, 2025 · 4 comments
Labels
enhancement New feature or request

Comments

@austincondiff
Copy link
Collaborator

austincondiff commented Jan 8, 2025

Description

Implement support for placeholders in CodeEdit, similar to Xcode’s <#placeholder#> feature. This would allow developers to insert and cycle through editable placeholders in their code, improving productivity and code clarity during development.

Use Case

This feature would be particularly useful for defining temporary or template-like code constructs, such as:

let <#name#> = <#value#>

Placeholders would:

  1. Be highlighted visually.
  2. Be navigable (e.g., via Tab key).
  3. Allow inline editing to replace the placeholder text.

Expected Behavior

  • When a placeholder is inserted (e.g., <#placeholder#>), it should:
  • Appear visually distinct (e.g., highlighted with a subtle background color or outline).
  • Be editable directly in the code editor.
  • Allow navigation between placeholders using keyboard shortcuts like Tab and Shift+Tab.
  • Placeholders should support nested constructs (e.g., let <#name#> = <#type#>(<#arguments#>)).

Technical Notes

  • Integrate placeholder handling with the current text editing features.
  • Consider compatibility with existing syntax highlighting and LSP integration.

Steps to Implement

  1. Define a syntax for placeholders (e.g., <#placeholder#>).
  2. Update the text editor to recognize and render placeholders.
  3. Add keyboard navigation support for placeholders.
  4. Ensure placeholder edits are reflected in the editor state.

Additional Context

This feature would align CodeEdit with Xcode’s behavior, making it a more familiar and seamless experience for developers who work on macOS. It also complements other code editing enhancements, such as autocompletion and LSP support.

Screenshots

image
@austincondiff austincondiff added the enhancement New feature or request label Jan 8, 2025
@thecoolwinter thecoolwinter moved this from 🆕 New to 📋 Todo in CodeEdit Project Jan 10, 2025
@thecoolwinter
Copy link
Collaborator

I wonder if we want to go the route VSCode went with placeholders. They call them tab stops, and I think they're a little more flexible than Xcode's in a specific way. I like that Xcode's allow you to create the placeholders by inserting a special character sequence. It allows for copying placeholders from anywhere, and they automatically are formatted so you can tab between them. VSCode's implementation, though, allows you to have named placeholders. For instance, you may have a snippet like this:

for (let $1 = 0; $1 < $2.length; $1++) {
    const $3 = $2[$1];
}

As the user types in $1 or $2, all the other placeholder positions that match that number are filled in. In the example, the user only has to type i, array, and element once to get:

for (let i = 0; i < array.length; i++) {
    const element = array[i];
}

This approach may be easier to implement for our case as well. If we're not using a special character sequence, we don't have to modify how text is rendered. Instead, we could insert the placeholders as regular text and keep track of the tab stops as the user edits.

We could also try to detect content with placeholders as the user pastes and suggest they paste it as a snippet when it's detected. That would help band-aid share-ability that we lose by not going the Xcode route.

@austincondiff
Copy link
Collaborator Author

austincondiff commented Jan 10, 2025

I like the idea of reusable placeholders. What if we use Xcodes approach but look at the name to see if it is the same? For example:

for (let <#iterator#> = 0; <#iterator#> < <#array#>.length; <#iterator#>++) {
    const <#element#> = <#array#>[<#iterator#>];
}

In this case, placeholders with the same name (e.g., <#iterator#> or <#array#>) would automatically update all occurrences as the user edits one of them. This approach aims to combine the easier-to-read syntax and shareability of Xcode’s method with the flexibility of linked placeholders like in VSCode. I can see the appeal of VSCode’s use of numbered tab stops for simplicity and easier tracking in more complex snippets, so perhaps there’s a middle ground we could explore. What do you think about balancing these approaches to keep both usability and flexibility in mind?

My concern with the simplicity of VS Code's approach is that it may interfere with the syntax of the code in the snippet whereas Xcode's syntax is less likely to interfere. You could always do something like <#1#>, <#2#>, <#3#>, etc.

The advantage of VS Code's tab stops approach is that you can customize the order of the tab stops.

@thecoolwinter
Copy link
Collaborator

Okay after some more discussion, @austincondiff and I came up with this syntax for placeholders:

<#varnumber:defaultvalue#>

When importing snippets from VSCode or TextMates we'll have to convert their syntax ${number:defaultval} into ours, but it also means Xcode placeholders will work out of the box too.

We discussed what changes are required for this to work, and it'll require two changes in CodeEditTextView. First is a delegate method for modifying how a selection is updated. So when a user puts a cursor on a placeholder, or selects around it, the selection updates to include the entire placeholder range. Something like

protocol SelectionManagerDelegate: AnyObject {
    func selectionWillUpdate(selectionManager: SelectionManager, selection: TextSelection, newRange: NSRange) -> NSRange?
}

The second change is a change to allow a delegate object to custom render line fragments. This is similar to how NSTextView allows for custom rendering, and would only be triggered by the placeholder drawing code when necessary. This would look something like

protocol LineFragmentDrawing {
    // return false if not drawn by this method.
    func drawLineFragment(ctLine: CTLine, context: CGDrawingContext) -> Bool
}

Screenshot 2025-01-11 at 10 14 21 AM

Alternative to the custom drawing method, we could set the font size to 0.1 for the <# and #> characters, which effectively hides those characters entirely.

@hi2gage
Copy link

hi2gage commented Jan 18, 2025

Based on our conversation in the weekly meeting:

I asked:
Is there a use case for placeholders that would make "reusable placeholders" unhelpful or problematic?
What is the root problem are we trying to solve by adopting VSCode-style placeholders?


Placeholder Use Beyond Variable Names

In Xcode, placeholders are used for more than just variable names. They can represent larger code bodies, for example:

if <#condition#> {
    <#statements#>
} else {
    <#statements#>
}

or

func <#name#>(<#parameters#>) -> <#return type#> {
    <#function body#>
}

This illustrates that placeholders often represent different elements of the code structure, not just linked values. I think we need to think beyond variable names when considering placeholder functionality.


Namespacing Concerns

One major issue with "reusable placeholders" is the forced namespacing of placeholder names and values. Consider this snippet:

if <#condition#> {
    <#statements#>
} else {
    <#statements#>
}

In this case, the statements placeholder is used twice but represents different content. With reusable placeholders, users would need to namespace them (e.g., <#statements1#> and <#statements2#>), which adds unnecessary cognitive load and complexity.


Xcode's Reusable Placeholder Workaround

After further research, I found that Xcode does provide a workaround for reusable placeholders by leveraging multi-cursor editing:

  • Select Previous Occurrence: ⌥⇧⌘E
  • Select Next Occurrence: ⌥⌘E
  • Select Find All Matches: (No default shortcut, but one can be added)

Source

Image

Quirks of Xcode's multi-cursor Implementation

While that work around is functional, it has quirks. For example, exiting multi-cursor editing causes the cursor to jump to the last token. This means users need to press TAB again to navigate to the next placeholder.

Snippet flow can also affect the placeholder navigation:

Example 1:

var <#variable name#>: <#type#> {
    set {
        <#variable name#> = newValue
    }
    get {
        <#statements#>
    }
}

Flow:

  1. Tab to the first variable name.
  2. Use ⌥⌘E to select all occurrences.
  3. Enter the variable name.
  4. Press ESC, then TAB to move to statements.

Example 2:

var <#variable name#>: <#type#> {
    get {
        <#statements#>
    }
    set {
        <#variable name#> = newValue
    }
}

Flow:

  1. Tab to the first variable name.
  2. Use ⌥⌘E to select all occurrences.
  3. Enter the variable name.
  4. Press ESC, then TAB to move to type.

This shows that the order of the placeholders matters from the perspective of an auto complete snippet.


Why I Prefer Xcode-Style Placeholders

  1. Better for Sharing Code:
    I often share code snippets with placeholders between coworkers. This style placeholder is much Xcode's style makes sharing simpler and more intuitive. I'll be honest I use placeholders for that more than I do snippets.

  2. No Namespacing Hassles:
    Xcode’s approach avoids the need for users to manually namespace placeholders, which is especially important for placeholders representing distinct bodies of code.

  3. Ease of Use for Defaults:
    The ability to double-click on default values (e.g., <#SomeSpecialType#>) is extremely helpful for opinionated placeholders, allowing quick replacement while maintaining clarity. This is helpful because I can immediately go to that type definition and see documentation etc.


On Supporting Other Editors

During the meeting, @austincondiff raised an important point about ensuring users of other editors (e.g., VSCode) don’t feel alienated or face difficulty importing snippets.

While I agree with this sentiment, I believe we could address it by building a robust snippet importer that converts formats like VSCode’s ${number:defaultvalue} into Xcode-compatible placeholders (<#number#>).

Looking at vscode-swift's snippets they have adopted vscode's placeholder style. This was an itch that I scratched wondering if placeholder style could be language specific instead of editor specific.

I would lean towards aligning CodeEdit more closely with Xcode because it feels like it fits the ethos of this project. Every code editor has its own way of handling placeholders, and perhaps we want to embrace the Xcode approach to make it easier for newcomers / be opinionated about placeholders. (Snippets could be a different story?)

Plus, I bet the server-side Swift users would likely appreciate this consistency.


Snippet Usage with AI tools

With the rise of AI tools I would think the reliance on pre-defined snippets (including those benefiting from reusable placeholders) is diminishing. In this context, being more opinionated and Xcode-aligned has fewer drawbacks, especially from a usability and share-ability standpoint. I would vote for a more human readable placeholder over the ability to have fancy snippets.


Conclusions

I vote Xcode style placeholders.

Any feedback or additional thoughts would be awesome!
Are there any edge cases or placeholder uses that I’ve missed?
Do I sound like an Xcode fanboy yet? 😬

I'm also very new to this project so I can't help with implementation details but maybe once I get up to speed I can talk more in that area.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
Status: 📋 Todo
Development

No branches or pull requests

3 participants