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

Triggering completion leaves initial character duplicated #714

Closed
rchl opened this issue Sep 12, 2019 · 9 comments
Closed

Triggering completion leaves initial character duplicated #714

rchl opened this issue Sep 12, 2019 · 9 comments

Comments

@rchl
Copy link
Member

rchl commented Sep 12, 2019

Using LSP-vue on OSX, LSP v0.8.6.

This requires latest version of vue-language-server which LSP-vue at the current time doesn't use. So manual upgrade of bundled vue-language-server is needed. It's now up to date.

Repro

  1. Create new *.vue file
  2. Type < to trigger completions (snippets)
  3. Select first snippet to add basic vue scaffolding

Expected

Snippet is inserted properly

Actual

The < character is duplicated at the beginning. So basically trigger character wasn't replaced with snippet content.

Screenshots:

Screenshot 2019-09-12 at 14 54 21

Screenshot 2019-09-12 at 14 54 27

Logs

Type < to trigger completions:

LSP:  --> textDocument/didChange
LSP:  --> textDocument/completion
LSP:      {'isIncomplete': False, 'items': [{'documentation': {'kind': 'markdown', 'value': '```vue\n<template>\n  ${0}\n</template>\n\n<script>\nexport default {\n\n}\n</script>\n\n<style>\n\n</style>\n```'}, 'sortText': '2a<vue> with default.vue ✌', 'detail': 'default.vue | .vue', 'insertTextFormat': 2, 'textEdit': {'range': {'end': {'character': 1, 'line': 0}, 'start': {'character': 0, 'line': 0}}, 'newText': '<template>\n  ${0}\n</template>\n\n<script>\nexport default {\n\n}\n</script>\n\n<style>\n\n</style>'}, 'insertText': '<template>\n  ${0}\n</template>\n\n<script>\nexport default {\n\n}\n</script>\n\n<style>\n\n</style>', 'kind': 17, 'label': '<vue> with default.vue ✌'}, {'documentation': {'kind': 'markdown', 'value': '```vue\n<template>\n\t${0}\n</template>\n```'}, 'sortText': '2b<template> html.vue ✌', 'detail': 'html.vue | .html', 'insertTextFormat': 2, 'textEdit': {'range': {'end': {'character': 1, 'line': 0}, 'start': {'character': 0, 'line': 0}}, 'newText': '<template>\n\t${0}\n</template>'}, 'insertText': '<template>\n\t${0}\n</template>', 'kind': 17, 'label': '<template> html.vue ✌'}, {'documentation': {'kind': 'markdown', 'value': '```vue\n<template lang="pug">\n\t${0}\n</template>\n```'}, 'sortText': '2b<template> pug.vue ✌', 'detail': 'pug.vue | .html', 'insertTextFormat': 2, 'textEdit': {'range': {'end': {'character': 1, 'line': 0}, 'start': {'character': 0, 'line': 0}}, 'newText': '<template lang="pug">\n\t${0}\n</template>'}, 'insertText': '<template lang="pug">\n\t${0}\n</template>', 'kind': 17, 'label': '<template> pug.vue ✌'}, {'documentation': {'kind': 'markdown', 'value': '```vue\n<style scoped>\n${0}\n</style>\n```'}, 'sortText': '2c<style> css-scoped.vue ✌', 'detail': 'css-scoped.vue | .css', 'insertTextFormat': 2, 'textEdit': {'range': {'end': {'character': 1, 'line': 0}, 'start': {'character': 0, 'line': 0}}, 'newText': '<style scoped>\n${0}\n</style>'}, 'insertText': '<style scoped>\n${0}\n</style>', 'kind': 17, 'label': '<style> css-scoped.vue ✌'}, {'documentation': {'kind': 'markdown', 'value': '```vue\n<style>\n${0}\n</style>\n```'}, 'sortText': '2c<style> css.vue ✌', 'detail': 'css.vue | .css', 'insertTextFormat': 2, 'textEdit': {'range': {'end': {'character': 1, 'line': 0}, 'start': {'character': 0, 'line': 0}}, 'newText': '<style>\n${0}\n</style>'}, 'insertText': '<style>\n${0}\n</style>', 'kind': 17, 'label': '<style> css.vue ✌'}, {'documentation': {'kind': 'markdown', 'value': '```vue\n<style lang="less" scoped>\n${0}\n</style>\n```'}, 'sortText': '2c<style> less-scoped.vue ✌', 'detail': 'less-scoped.vue | .css', 'insertTextFormat': 2, 'textEdit': {'range': {'end': {'character': 1, 'line': 0}, 'start': {'character': 0, 'line': 0}}, 'newText': '<style lang="less" scoped>\n${0}\n</style>'}, 'insertText': '<style lang="less" scoped>\n${0}\n</style>', 'kind': 17, 'label': '<style> less-scoped.vue ✌'}, {'documentation': {'kind': 'markdown', 'value': '```vue\n<style lang="less">\n${0}\n</style>\n```'}, 'sortText': '2c<style> less.vue ✌', 'detail': 'less.vue | .css', 'insertTextFormat': 2, 'textEdit': {'range': {'end': {'character': 1, 'line': 0}, 'start': {'character': 0, 'line': 0}}, 'newText': '<style lang="less">\n${0}\n</style>'}, 'insertText': '<style lang="less">\n${0}\n</style>', 'kind': 17, 'label': '<style> less.vue ✌'}, {'documentation': {'kind': 'markdown', 'value': '```vue\n<style lang="postcss" scoped>\n${0}\n</style>\n```'}, 'sortText': '2c<style> postcss-scoped.vue ✌', 'detail': 'postcss-scoped.vue | .css', 'insertTextFormat': 2, 'textEdit': {'range': {'end': {'character': 1, 'line': 0}, 'start': {'character': 0, 'line': 0}}, 'newText': '<style lang="postcss" scoped>\n${0}\n</style>'}, 'insertText': '<style lang="postcss" scoped>\n${0}\n</style>', 'kind': 17, 'label': '<style> postcss-scoped.vue ✌'}, {'documentation': {'kind': 'markdown', 'value': '```vue\n<style lang="postcss">\n${0}\n</style>\n```'}, 'sortText': '2c<style> postcss.vue ✌', 'detail': 'postcss.vue | .css', 'insertTextFormat': 2, 'textEdit': {'range': {'end': {'character': 1, 'line': 0}, 'start': {'character': 0, 'line': 0}}, 'newText': '<style lang="postcss">\n${0}\n</style>'}, 'insertText': '<style lang="postcss">\n${0}\n</style>', 'kind': 17, 'label': '<style> postcss.vue ✌'}, {'documentation': {'kind': 'markdown', 'value': '```vue\n<style lang="sass" scoped>\n${0}\n</style>\n```'}, 'sortText': '2c<style> sass-scoped.vue ✌', 'detail': 'sass-scoped.vue | .css', 'insertTextFormat': 2, 'textEdit': {'range': {'end': {'character': 1, 'line': 0}, 'start': {'character': 0, 'line': 0}}, 'newText': '<style lang="sass" scoped>\n${0}\n</style>'}, 'insertText': '<style lang="sass" scoped>\n${0}\n</style>', 'kind': 17, 'label': '<style> sass-scoped.vue ✌'}, {'documentation': {'kind': 'markdown', 'value': '```vue\n<style lang="sass">\n${0}\n</style>\n```'}, 'sortText': '2c<style> sass.vue ✌', 'detail': 'sass.vue | .css', 'insertTextFormat': 2, 'textEdit': {'range': {'end': {'character': 1, 'line': 0}, 'start': {'character': 0, 'line': 0}}, 'newText': '<style lang="sass">\n${0}\n</style>'}, 'insertText': '<style lang="sass">\n${0}\n</style>', 'kind': 17, 'label': '<style> sass.vue ✌'}, {'documentation': {'kind': 'markdown', 'value': '```vue\n<style lang="scss" scoped>\n${0}\n</style>\n```'}, 'sortText': '2c<style> scss-scoped.vue ✌', 'detail': 'scss-scoped.vue | .css', 'insertTextFormat': 2, 'textEdit': {'range': {'end': {'character': 1, 'line': 0}, 'start': {'character': 0, 'line': 0}}, 'newText': '<style lang="scss" scoped>\n${0}\n</style>'}, 'insertText': '<style lang="scss" scoped>\n${0}\n</style>', 'kind': 17, 'label': '<style> scss-scoped.vue ✌'}, {'documentation': {'kind': 'markdown', 'value': '```vue\n<style lang="scss">\n${0}\n</style>\n```'}, 'sortText': '2c<style> scss.vue ✌', 'detail': 'scss.vue | .css', 'insertTextFormat': 2, 'textEdit': {'range': {'end': {'character': 1, 'line': 0}, 'start': {'character': 0, 'line': 0}}, 'newText': '<style lang="scss">\n${0}\n</style>'}, 'insertText': '<style lang="scss">\n${0}\n</style>', 'kind': 17, 'label': '<style> scss.vue ✌'}, {'documentation': {'kind': 'markdown', 'value': '```vue\n<style lang="stylus" scoped>\n${0}\n</style>\n```'}, 'sortText': '2c<style> stylus-scoped.vue ✌', 'detail': 'stylus-scoped.vue | .css', 'insertTextFormat': 2, 'textEdit': {'range': {'end': {'character': 1, 'line': 0}, 'start': {'character': 0, 'line': 0}}, 'newText': '<style lang="stylus" scoped>\n${0}\n</style>'}, 'insertText': '<style lang="stylus" scoped>\n${0}\n</style>', 'kind': 17, 'label': '<style> stylus-scoped.vue ✌'}, {'documentation': {'kind': 'markdown', 'value': '```vue\n<style lang="stylus">\n${0}\n</style>\n```'}, 'sortText': '2c<style> stylus.vue ✌', 'detail': 'stylus.vue | .css', 'insertTextFormat': 2, 'textEdit': {'range': {'end': {'character': 1, 'line': 0}, 'start': {'character': 0, 'line': 0}}, 'newText': '<style lang="stylus">\n${0}\n</style>'}, 'insertText': '<style lang="stylus">\n${0}\n</style>', 'kind': 17, 'label': '<style> stylus.vue ✌'}, {'documentation': {'kind': 'markdown', 'value': '```vue\n<script>\nexport default {\n\t${0}\n}\n</script>\n```'}, 'sortText': '2d<script> javascript.vue ✌', 'insertTextFormat': 2, 'textEdit': {'range': {'end': {'character': 1, 'line': 0}, 'start': {'character': 0, 'line': 0}}, 'newText': '<script>\nexport default {\n\t${0}\n}\n</script>'}, 'insertText': '<script>\nexport default {\n\t${0}\n}\n</script>', 'kind': 17, 'label': '<script> javascript.vue ✌'}, {'documentation': {'kind': 'markdown', 'value': '```vue\n<script lang="ts">\nimport Vue from \'vue\'\nexport default Vue.extend({\n\t${0}\n})\n</script>\n```'}, 'sortText': '2d<script> typescript.vue ✌', 'insertTextFormat': 2, 'textEdit': {'range': {'end': {'character': 1, 'line': 0}, 'start': {'character': 0, 'line': 0}}, 'newText': '<script lang="ts">\nimport Vue from \'vue\'\nexport default Vue.extend({\n\t${0}\n})\n</script>'}, 'insertText': '<script lang="ts">\nimport Vue from \'vue\'\nexport default Vue.extend({\n\t${0}\n})\n</script>', 'kind': 17, 'label': '<script> typescript.vue ✌'}]}
LSP: textEdit from col 0, 1 applied at col 1
LSP: textEdit from col 0, 1 applied at col 1
LSP: textEdit from col 0, 1 applied at col 1
LSP: textEdit from col 0, 1 applied at col 1
LSP: textEdit from col 0, 1 applied at col 1
LSP: textEdit from col 0, 1 applied at col 1
LSP: textEdit from col 0, 1 applied at col 1
LSP: textEdit from col 0, 1 applied at col 1
LSP: textEdit from col 0, 1 applied at col 1
LSP: textEdit from col 0, 1 applied at col 1
LSP: textEdit from col 0, 1 applied at col 1
LSP: textEdit from col 0, 1 applied at col 1
LSP: textEdit from col 0, 1 applied at col 1
LSP: textEdit from col 0, 1 applied at col 1
LSP: textEdit from col 0, 1 applied at col 1
LSP: textEdit from col 0, 1 applied at col 1
LSP: textEdit from col 0, 1 applied at col 1
LSP: <--  textDocument/publishDiagnostics
LSP:      {'diagnostics': [], 'uri': 'file:///.../_document.vue'}
LSP:  --> textDocument/documentColor
LSP:      []
LSP:  --> textDocument/codeAction
LSP:      []

Pick completion with tab:

LSP: could not find completion item for inserted "<<template>
  "
LSP:  --> textDocument/didChange
LSP: <--  textDocument/publishDiagnostics
LSP:      {'diagnostics': [], 'uri': 'file:///.../_document.vue'}
LSP:  --> textDocument/documentColor
LSP:      []
LSP:  --> textDocument/codeAction
LSP:      []
@tomv564
Copy link
Contributor

tomv564 commented Sep 13, 2019

Sounds like the same issue we already fix in completions starting with ‘$’ and ‘:’ and ‘-‘. Perhaps you want to see if we can generalize the “trim leading char if not alphanumeric” logic in format_completion?

@rchl
Copy link
Member Author

rchl commented Sep 16, 2019

I've looked a bit but the whole logic of figuring out position to insert is quite complicated and probably easy to break for someone who doesn't have a good overview of all the quirks.

From my naive understanding though, when I insert < at the beginning of the document, and VLS returns completion like this one:

{'documentation': {'kind': 'markdown',
                   'value': '```vue\n<script lang="ts">\nimport Vue from \'vue\'\nexport default Vue.extend({\n\t${0}\n})\n</script>\n```'},
 'insertText': '<script lang="ts">\nimport Vue from \'vue\'\nexport default Vue.extend({\n\t${0}\n})\n</script>',
 'insertTextFormat': 2,
 'kind': 17,
 'label': '<script> typescript.vue ✌',
 'sortText': '2d<script> typescript.vue ✌',
 'textEdit': {'newText': '<script lang="ts">\nimport Vue from \'vue\'\nexport default Vue.extend({\n\t${0}\n})\n</script>',
              'range': {'end': {'character': 1, 'line': 0},
                        'start': {'character': 0, 'line': 0}}}}

Given that it provided range to replace (0-1) with the completion text, if LSP would follow that, I think it should work. But that information seem to be lost in the process of formatting complations.

What do you think? Is it correct to assume that when textEdit with range is provided, following it would always result in fully correct replacement?

@tomv564
Copy link
Contributor

tomv564 commented Sep 17, 2019

Correct, the textEdit gives us the information we need, but the Sublime API doesn't allow us to "apply an edit" directly, all we can do is make sure the trigger and replacement strings we return will have the correct result.

The fixes are here if you want to have another look:
https://github.com/tomv564/LSP/blob/5caf1585697552d082d32519137de6dc26c7b7f1/plugin/core/completion.py#L44

@rwols
Copy link
Member

rwols commented Sep 19, 2019

@rchl see also sublimehq/sublime_text#2754

@tomv564
Copy link
Contributor

tomv564 commented Oct 11, 2019

@rchl, can you share a bit more of your config?

I cannot get the vue server (0.0.61) to return its snippet completions in a totally empty .vue file.
So far I have:

  • Downgraded to the non-broken vue server
  • Uncommented LSP-vue's default configuration (is this intentionally commented out? server fails to initialize without it and the user is not really told!)
  • Enabled the useScaffoldSnippets (default false in LSP-vue)
  • Added text.html to Vue syntax's auto_complete_selector (this is something LSP recently added, a bit annoying to vue users perhaps?)

Perhaps @predragnikolic can help?

@rchl
Copy link
Member Author

rchl commented Oct 11, 2019

useScaffoldSnippets is unused in current vue server code (it was replaced with something else but it doesn't have to be set anyway, I think)

It's not working for me either now. Possibly because of the thing you mentioned:

Added text.html to Vue syntax's auto_complete_selector (this is something LSP recently added, a bit annoying to vue users perhaps?)

Where do I have to add it exactly?

@rchl
Copy link
Member Author

rchl commented Oct 11, 2019

@tomv564
The problem is that snippets are not enabled when globalSnippetDir is not defined. They have fixed that in the server but (v0.0.61) doesn't have it yet. And later version has other regression, as you know.

I have made a fix so that snippets worked in v0.0.61 (sublimelsp/LSP-vue#6) but @predragnikolic removed it when updating to latest server version and didn't re-add it back when reverting to version v0.0.61. So with current LSP-Vue snippets won't work. You have to either wait until things stabilize or manually apply my fix (it won't apply cleanly in current code though).

@tomv564
Copy link
Contributor

tomv564 commented Oct 11, 2019

Thanks, I hardcoded a non-existing path and it started working!
Someone should really introduce some sane defaults into that server, so much pain caused in other clients just to even initialize the server.

The LSP problem is an inconsistency in how sublime handles certain prefixes in completions.

If in PHP you trigger with $ and accept a completion $variable, then sublime inserts variable so the end result becomes $variable.

But when using < as a trigger and accepting <script>, sublime inserts <script> verbatim so the result becomes <<script>.
(The HTML language server and it avoids this by returning script> instead, for example.)

That inconsistency broke our "find out which completion was inserted so we can fix the textEdit" logic. I added an exception so if a completion resulted in << then it will assume the inserted text started at the 2nd <.

@tomv564
Copy link
Contributor

tomv564 commented Oct 11, 2019

About the completion selector: Go into "Preferences - Settings (Syntax Specific)", override auto_complete_selector then add "text.html" so your setting looks like this:

{

	"auto_complete_selector": "text.html, meta.tag - punctuation.definition.tag.begin, source - comment - string.quoted.double.block - string.quoted.single.block - string.unquoted.heredoc"
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants