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 · 3 comments
Open

✨ Placeholder Support #287

austincondiff opened this issue Jan 8, 2025 · 3 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.

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

2 participants