-
Notifications
You must be signed in to change notification settings - Fork 28
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add better auto-closing UX for Liquid pairs
Fixes #202 Fixes Shopify/theme-check-vscode#108
- Loading branch information
1 parent
9c32ec9
commit baa6d37
Showing
10 changed files
with
299 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
--- | ||
'@shopify/theme-language-server-common': minor | ||
'theme-check-vscode': minor | ||
--- | ||
|
||
Add better auto-closing UX for Liquid pairs |
143 changes: 143 additions & 0 deletions
143
packages/theme-language-server-common/src/formatting/OnTypeFormattingProvider.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
import { describe, beforeEach, it, expect, assert } from 'vitest'; | ||
import { OnTypeFormattingProvider } from './OnTypeFormattingProvider'; | ||
import { DocumentManager } from '../documents'; | ||
import { DocumentOnTypeFormattingParams } from 'vscode-languageserver'; | ||
import { Position, TextEdit, Range } from 'vscode-languageserver-protocol'; | ||
import { TextDocument } from 'vscode-languageserver-textdocument'; | ||
|
||
const options: DocumentOnTypeFormattingParams['options'] = { | ||
insertSpaces: true, | ||
tabSize: 2, | ||
}; | ||
|
||
describe('Module: OnTypeFormattingProvider', () => { | ||
let documentManager: DocumentManager; | ||
let onTypeFormattingProvider: OnTypeFormattingProvider; | ||
|
||
beforeEach(() => { | ||
documentManager = new DocumentManager(); | ||
onTypeFormattingProvider = new OnTypeFormattingProvider(documentManager); | ||
}); | ||
|
||
it('should return null for non-existent documents', async () => { | ||
const params: DocumentOnTypeFormattingParams = { | ||
textDocument: { uri: 'file:///path/to/non-existent-document.liquid' }, | ||
position: Position.create(0, 0), | ||
ch: ' ', | ||
options, | ||
}; | ||
|
||
const result = await onTypeFormattingProvider.onTypeFormatting(params); | ||
expect(result).toBeNull(); | ||
}); | ||
|
||
it('should return null if character is space and cursor position is less than or equal to 2', async () => { | ||
const params: DocumentOnTypeFormattingParams = { | ||
textDocument: { uri: 'file:///path/to/document.liquid' }, | ||
position: Position.create(0, 1), | ||
ch: ' ', | ||
options, | ||
}; | ||
|
||
documentManager.open(params.textDocument.uri, 'Sample text content', 1); | ||
|
||
const result = await onTypeFormattingProvider.onTypeFormatting(params); | ||
expect(result).toBeNull(); | ||
}); | ||
|
||
it('should return null if character is not space and cursor position is less than or equal to 1', async () => { | ||
const params: DocumentOnTypeFormattingParams = { | ||
textDocument: { uri: 'file:///path/to/document.liquid' }, | ||
position: Position.create(0, 0), | ||
ch: 'a', | ||
options, | ||
}; | ||
|
||
documentManager.open(params.textDocument.uri, 'Sample text content', 1); | ||
|
||
const result = await onTypeFormattingProvider.onTypeFormatting(params); | ||
expect(result).toBeNull(); | ||
}); | ||
|
||
it('should return a TextEdit to insert a space after "{{" in "{{ }}"', async () => { | ||
const params: DocumentOnTypeFormattingParams = { | ||
textDocument: { uri: 'file:///path/to/document.liquid' }, | ||
position: Position.create(0, 2), | ||
ch: '{', | ||
options, | ||
}; | ||
|
||
documentManager.open(params.textDocument.uri, '{{ }}', 1); | ||
const document = documentManager.get(params.textDocument.uri)?.textDocument; | ||
assert(document); | ||
|
||
const result = await onTypeFormattingProvider.onTypeFormatting(params); | ||
assert(result); | ||
expect(TextDocument.applyEdits(document, result)).to.equal('{{ }}'); | ||
}); | ||
|
||
it('should return a TextEdit to insert a space after "{%" in "{% %}"', async () => { | ||
const params: DocumentOnTypeFormattingParams = { | ||
textDocument: { uri: 'file:///path/to/document.liquid' }, | ||
position: Position.create(0, 2), | ||
ch: '%', | ||
options, | ||
}; | ||
|
||
documentManager.open(params.textDocument.uri, '{% %}', 1); | ||
const document = documentManager.get(params.textDocument.uri)?.textDocument; | ||
assert(document); | ||
|
||
const result = await onTypeFormattingProvider.onTypeFormatting(params); | ||
assert(result); | ||
expect(TextDocument.applyEdits(document, result)).to.equal('{% %}'); | ||
}); | ||
|
||
it('should return a TextEdit to replace and insert characters in "{{ - }}"', async () => { | ||
const params: DocumentOnTypeFormattingParams = { | ||
textDocument: { uri: 'file:///path/to/document.liquid' }, | ||
position: Position.create(0, 4), | ||
ch: '-', | ||
options, | ||
}; | ||
|
||
documentManager.open(params.textDocument.uri, '{{ - }}', 1); | ||
const document = documentManager.get(params.textDocument.uri)?.textDocument; | ||
assert(document); | ||
|
||
const result = await onTypeFormattingProvider.onTypeFormatting(params); | ||
assert(result); | ||
expect(TextDocument.applyEdits(document, result)).to.equal('{{- -}}'); | ||
}); | ||
|
||
it('should return a TextEdit to replace and insert characters in "{% - %}"', async () => { | ||
const params: DocumentOnTypeFormattingParams = { | ||
textDocument: { uri: 'file:///path/to/document.liquid' }, | ||
position: Position.create(0, 4), | ||
ch: '-', | ||
options, | ||
}; | ||
|
||
documentManager.open(params.textDocument.uri, '{% - %}', 1); | ||
const document = documentManager.get(params.textDocument.uri)?.textDocument; | ||
assert(document); | ||
|
||
const result = await onTypeFormattingProvider.onTypeFormatting(params); | ||
assert(result); | ||
expect(TextDocument.applyEdits(document, result)).to.equal('{%- -%}'); | ||
}); | ||
|
||
it('should return null for characters not matching any case', async () => { | ||
const params: DocumentOnTypeFormattingParams = { | ||
textDocument: { uri: 'file:///path/to/document.liquid' }, | ||
position: Position.create(0, 3), | ||
ch: 'a', | ||
options, | ||
}; | ||
|
||
documentManager.open(params.textDocument.uri, 'Sample text content', 1); | ||
|
||
const result = await onTypeFormattingProvider.onTypeFormatting(params); | ||
expect(result).toBeNull(); | ||
}); | ||
}); |
116 changes: 116 additions & 0 deletions
116
packages/theme-language-server-common/src/formatting/OnTypeFormattingProvider.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
import { DocumentManager } from '../documents'; | ||
import { DocumentOnTypeFormattingParams } from 'vscode-languageserver'; | ||
import { Range, Position, TextEdit } from 'vscode-languageserver-protocol'; | ||
|
||
export class OnTypeFormattingProvider { | ||
constructor(public documentManager: DocumentManager) {} | ||
|
||
/** | ||
* This very complex piece of code here exists to provide a good autoclosing UX. | ||
* | ||
* The story is kind of long so here goes... | ||
* | ||
* What we want: | ||
* 1. Basic autoclosing of {{, {% with the corresponding pair (and spaces) | ||
* - user types: {{ | ||
* - user sees: {{ | }} (with cursor position at |) | ||
* 2. Autoclosing of {{- with -}}, {%- with -%} | ||
* - user types: {{- | ||
* - user sees: {{- | -}} (with cursor at |) | ||
* 3. User adds whitespace stripping on one side of the braces of an existing tag | ||
* - user types: - at | in `{{| drop }}` | ||
* - user sees: {{- drop }} | ||
* | ||
* Why we can't do it with autoclosingPairs: | ||
* - VS Code's settings accepts autoclosingPairs and autocloseBefore | ||
* - autoclosingPairs is a set of pairs that should be autoclosed (e.g. ['{%', '%}']) | ||
* - autocloseBefore is a character set of 'allowed next characters' that would cause a closing pair | ||
* - If we put a space (' ') the autoclosingPairs set, then (3) from above becomes funky: | ||
* - assume autoclosingPairs = {|}, {{|}}, {{ | }} | ||
* - user types: a space at | in `{{| drop }}` | ||
* - user sees: {{ }}drop }} | ||
* - This happens because the space is an autocloseBefore character, it sees a space after the cursor | ||
* so it closes '{{ ' with ' }}' at the cursor position, resulting in '{{ }}drop }}' | ||
* - Something similar happens if we include the `-` in the autoclosing pairs | ||
* - This is annoying! | ||
* | ||
* So our solution is the following: | ||
* 1. We change the pairs to include the closing space (this way our cursor remains where we want it to be) | ||
* - {{| }} | ||
* - {%| %} | ||
* 2. We add this OnTypeFormattingProvider that does the following "fixes": | ||
* - {{| }} into {{ | }} | ||
* - {{ -| }} into {{- | -}} | ||
* - {%| %} into {% | %} | ||
* - {% -| %} into {%- | -%} | ||
* | ||
* This lets us avoid the unnecessary close and accomplish 1, 2 and 3 :) | ||
* | ||
* Fallback for editor.onTypeFormatting: false is to let the user type the `-` on both sides manually | ||
*/ | ||
async onTypeFormatting(params: DocumentOnTypeFormattingParams) { | ||
const document = this.documentManager.get(params.textDocument.uri); | ||
if (!document) return null; | ||
const textDocument = document.textDocument; | ||
const ch = params.ch; | ||
// position is position of cursor so 1 ahead of char | ||
const { line, character } = params.position; | ||
// This is an early return to avoid doing currentLine.at(-1); | ||
if ((ch === ' ' && character <= 2) || character <= 1) return null; | ||
const currentLineRange = Range.create(Position.create(line, 0), Position.create(line + 1, 0)); | ||
const currentLine = textDocument.getText(currentLineRange); | ||
const charIdx = ch === ' ' ? character - 2 : character - 1; | ||
const char = currentLine.at(charIdx); | ||
switch (char) { | ||
// here we fix {{| }} with {{ | }} | ||
case '{': { | ||
const chars = currentLine.slice(charIdx - 1, charIdx + 4); | ||
if (chars === '{{ }}') { | ||
return [TextEdit.insert(Position.create(line, charIdx + 1), ' ')]; | ||
} | ||
} | ||
|
||
// here we fix {%| %} with {% | %} | ||
case '%': { | ||
const chars = currentLine.slice(charIdx - 1, charIdx + 4); | ||
if (chars === '{% %}') { | ||
return [TextEdit.insert(Position.create(line, charIdx + 1), ' ')]; | ||
} | ||
} | ||
|
||
// here we fix {{ -| }} to {{- | -}} | ||
// here we fix {% -| }} to {%- | -%} | ||
case '-': { | ||
// remember 0-index means 4th char | ||
if (charIdx < 3) return null; | ||
|
||
const chars = currentLine.slice(charIdx - 3, charIdx + 4); | ||
if (chars === '{{ - }}' || chars === '{% - %}') { | ||
// Here we're being clever and doing the {{- -}} if the first character | ||
// you type is a `-`, leaving your cursor in the middle :) | ||
return [ | ||
// Start with | ||
// {{ - }} | ||
// ^ start replace | ||
// ^ end replace (excluded) | ||
// Replace with '- ', get | ||
// {{- }} | ||
TextEdit.replace( | ||
Range.create(Position.create(line, charIdx - 1), Position.create(line, charIdx + 1)), | ||
'- ', | ||
), | ||
// Start with | ||
// {{ - }} | ||
// ^ char | ||
// ^ insertion point | ||
// Insert ' ' , get | ||
// {{ - -}} | ||
// Both together and you get {{- -}} with your cursor in the middle | ||
TextEdit.insert(Position.create(line, charIdx + 2), '-'), | ||
]; | ||
} | ||
} | ||
} | ||
return null; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { OnTypeFormattingProvider } from './OnTypeFormattingProvider'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters