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

feat: Autocomplete using parameter objects #410

Merged
merged 11 commits into from
Feb 12, 2024
34 changes: 28 additions & 6 deletions app/javascript/src/components/editor/CodeMirror.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
import { onDestroy, onMount, createEventDispatcher, tick } from "svelte"
import { basicSetup } from "codemirror"
import { EditorView, keymap } from "@codemirror/view"
import { EditorState, EditorSelection } from "@codemirror/state"
import { EditorState, EditorSelection, Transaction } from "@codemirror/state"
import { indentUnit, StreamLanguage, syntaxHighlighting } from "@codemirror/language"
import { autocompletion } from "@codemirror/autocomplete"
import { autocompletion, pickedCompletion } from "@codemirror/autocomplete"
import { redo } from "@codemirror/commands"
import { linter, lintGutter } from "@codemirror/lint"
import { indentationMarkers } from "@replit/codemirror-indentation-markers"
Expand Down Expand Up @@ -94,9 +94,12 @@
{ key: "Enter", run: autoIndentOnEnter },
{ key: "Ctrl-Shift-z", run: redoAction }
]),
EditorView.updateListener.of((state) => {
if (state.docChanged) updateItem()
if (state.selectionSet) $editorStates[currentId].selection = view.state.selection
EditorView.updateListener.of((transaction) => {
if (transaction.docChanged) {
indentMultilineInserts(transaction)
updateItem()
}
if (transaction.selectionSet) $editorStates[currentId].selection = view.state.selection
}),
basicSetup,
parameterTooltip(),
Expand Down Expand Up @@ -225,6 +228,7 @@

const tabs = /^\t*/.exec(lineText)?.[0].length
const spaces = /^\s*/.exec(lineText)?.[0].length - tabs

return Math.floor(spaces / 4) + tabs
}

Expand Down Expand Up @@ -281,10 +285,28 @@
})
}

async function setScrollPosition() {
function setScrollPosition() {
view.scrollDOM.scrollTo({ top: $editorScrollPositions[currentId] || 0 })
}

function indentMultilineInserts(transaction) {
// Only perform this function if transaction is of an expected type performed by the user to prevent infinite loops on changes made by CodeMirror
if (transaction.transactions.every(tr => ["input.paste", "input.complete"].includes(tr.annotation(Transaction.userEvent)))) {
const [range] = transaction.changedRanges
const text = transaction.state.doc.toString().slice(range.fromB, range.toB)
const indents = getIndentForLine(view.state, range.fromB)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indentation is not all sunshine and rainbows, sadly

brave_IJ8rPnV7Up.mp4

I think this could benefit from reapplying indentation on each line of the pasted content?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made some improvements and it should be much better. It's not quite perfect but the imperfections make enough sense. VSCode has similar (but different) inconsistencies. I think this is about as close as we can get without fully parsing the text, which would be too slow for larger texts anyway.

let tabs = ""
for (let i = 0; i < indents; i++) { tabs += "\t" }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let tabs = ""
for (let i = 0; i < indents; i++) { tabs += "\t" }
const tabs = "\t".repeat(indents)

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, neat!


const changes = {
from: range.fromB,
to: range.toB,
insert: text.replaceAll(/\n/g, "\n" + tabs)
}

view.dispatch({ changes })
}
}
</script>

<svelte:window
Expand Down
48 changes: 27 additions & 21 deletions app/javascript/src/components/editor/Editor.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@
$: if ($currentProject && $sortedItems?.length && $currentItem && !Object.keys($currentItem).length)
$currentItem = $sortedItems.filter(i => i.type == "item")?.[0] || {}

$: $completionsMap = parseKeywords($settings)

onMount(() => {
$completionsMap = parseKeywords()
$workshopConstants = constants
$currentItem = $items?.[0] || {}
$projects = _projects || []
Expand Down Expand Up @@ -76,32 +77,37 @@
if (nullCount) params.args_min_length = v.args.length - nullCount
}

if (v.args?.length) {
// Add detail arguments in autocomplete results
const detail = v.args.map(a => `${ toCapitalize(a.name) }`)
const joinedDetail = detail.join(", ")
if (!params.args_length) return params

params.detail_full = joinedDetail
params.detail = `(${ joinedDetail.slice(0, 30) }${ joinedDetail.length > 30 ? "..." : "" })`
// Add detail arguments in autocomplete results
const detail = v.args.map(a => `${ toCapitalize(a.name) }`)
const joinedDetail = detail.join(", ")

// Add apply values when selecting autocomplete, filling in default args
const lowercaseDefaults = Object.keys(defaults).map(k => k.toLowerCase())
const apply = v.args.map(a => {
const string = a.default?.toString().toLowerCase().replaceAll(",", "")
params.detail_full = joinedDetail
params.detail = `(${ joinedDetail.slice(0, 30) }${ joinedDetail.length > 30 ? "..." : "" })`

if (lowercaseDefaults.includes(string)) return defaults[toCapitalize(string)]
// Add apply values when selecting autocomplete, filling in default args
const lowercaseDefaults = Object.keys(defaults).map(k => k.toLowerCase())
const useParameterObject = $settings["autocomplete-parameter-objects"] && params.args_length >= $settings["autocomplete-min-parameter-size"]
const apply = v.args.map(a => {
let string = a.default?.toString().toLowerCase().replaceAll(",", "")
if (useParameterObject) string = `\n\t${ a.name }: ${ string }`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to have a limit on when each parameter is put on a new line and when not?

If I was one of those people that put autocomplete-min-parameter-size to 1, maybe I'd like for autocomplete to not expand the parameter object into multiple lines if the parameter amount is <3 or something.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea!

min-newline


return toCapitalize(string)
})
if (lowercaseDefaults.includes(string)) return defaults[toCapitalize(string)]

params.parameter_keys = detail
params.parameter_defaults = apply
params.apply = `${ v["en-US"] }(${ apply.join(", ") })`
return toCapitalize(string)
})

// Add arguments to info box
params.info += "\n\nArguments: "
params.info += detail
}
params.parameter_keys = detail
params.parameter_defaults = apply

params.apply = useParameterObject ?
`${ v["en-US"] }({ ${ apply.join(", ") }\n})` :
`${ v["en-US"] }(${ apply.join(", ") })`

// Add arguments to info box
params.info += "\n\nArguments: "
params.info += detail

return params
})
Expand Down
60 changes: 49 additions & 11 deletions app/javascript/src/components/editor/Settings.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script>
import { fly } from "svelte/transition"
import { fly, slide } from "svelte/transition"
import { onMount, tick } from "svelte"
import { escapeable } from "../actions/escapeable"
import { outsideClick } from "../actions/outsideClick"
Expand Down Expand Up @@ -61,6 +61,54 @@

{#if active}
<div transition:fly={{ duration: 150, y: 20 }} use:escapeable on:escape={() => active = false} class="dropdown__content block p-1/4" style="width: 300px">
<h5 class="mt-0 mb-1/8">Settings</h5>

<div class="checkbox tooltip mt-1/8">
<input id="show-line-indent-markers" type="checkbox" bind:checked={$settings["show-indent-markers"]} />
<label for="show-line-indent-markers">Show line indent markers</label>

<div class="tooltip__content bg-darker">
Show line markers at the expected indents at 1 tab or 4 spaces.
</div>
</div>

<div class="checkbox tooltip mt-1/8">
<input id="word-wrap" type="checkbox" bind:checked={$settings["word-wrap"]} />
<label for="word-wrap">Word wrap</label>

<div class="tooltip__content bg-darker">
Wrap lines that no longer fit on screen.
</div>
</div>

<div class="checkbox tooltip mt-1/8">
<input id="autocomplete-parameter-objects" type="checkbox" bind:checked={$settings["autocomplete-parameter-objects"]} />
<label for="autocomplete-parameter-objects">
Autocomplete using parameter objects
</label>

<div class="tooltip__content bg-darker">
Parameter objects change the format of parameters in actions and values to be more readable and less cumbersome to write. You can exclude any parameters you don't change the default off.
</div>
</div>

{#if $settings["autocomplete-parameter-objects"]}
<div class="form-group mt-1/8 tooltip" transition:slide|local={{ duration: 100 }}>
<label for="" class="text-base nowrap">Minimum parameter length</label>

<div class="flex align-center">
<input type="range" min=1 max=20 step=1 class="range mr-1/8" bind:value={$settings["autocomplete-min-parameter-size"]} />
{$settings["autocomplete-min-parameter-size"]}
</div>

<div class="tooltip__content bg-darker">
Only autocomplete when an action or value has equal or more than this value in parameters.
</div>
</div>
{/if}

<hr />

<h5 class="mt-0 mb-1/8">Font</h5>

<div class="form-group-inline">
Expand Down Expand Up @@ -100,16 +148,6 @@

<hr>

<div class="checkbox mt-1/8">
<input id="show-line-indent-markers" type="checkbox" bind:checked={$settings["show-indent-markers"]} />
<label for="show-line-indent-markers">Show line indent markers</label>
</div>

<div class="checkbox mt-1/8 mb-1/4">
<input id="word-wrap" type="checkbox" bind:checked={$settings["word-wrap"]} />
<label for="word-wrap">Word wrap</label>
</div>

<button class="button button--link button--small pb-0" on:click={resetToDefault}>Reset all to default</button>
</div>
{/if}
Expand Down
4 changes: 3 additions & 1 deletion app/javascript/src/stores/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,5 +104,7 @@ export const settings = writable({
"color-invalid": "#b33834",
"color-custom-keyword": "#c678dd",
"show-indent-markers": true,
"word-wrap": false
"word-wrap": false,
"autocomplete-parameter-objects": false,
"autocomplete-min-parameter-size": 2
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description says 3, but this is set to 2. Which one should it be?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, it should be 2. I had it at 3 initially, but 2 felt better

})
Loading