Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/language-server/src/lib/documents/parseHtml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ function preprocess(text: string) {
export interface AttributeContext {
name: string;
inValue: boolean;
elementTag: Node;
valueRange?: [number, number];
}

Expand Down Expand Up @@ -122,6 +123,7 @@ export function getAttributeContextAtPosition(

if (inTokenRange()) {
return {
elementTag: tag,
name: currentAttributeName,
inValue: false
};
Expand All @@ -131,6 +133,7 @@ export function getAttributeContextAtPosition(
const nextToken = scanner.scan();

return {
elementTag: tag,
name: currentAttributeName,
inValue: true,
valueRange: [
Expand All @@ -151,6 +154,7 @@ export function getAttributeContextAtPosition(
}

return {
elementTag: tag,
name: currentAttributeName,
inValue: true,
valueRange: [start, end]
Expand Down
33 changes: 15 additions & 18 deletions packages/language-server/src/plugins/html/HTMLPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
LinkedEditingRangesProvider
} from '../interfaces';
import { isInsideMoustacheTag, toRange } from '../../lib/documents/utils';
import { possiblyComponent } from '../../utils';

export class HTMLPlugin
implements HoverProvider, CompletionsProvider, RenameProvider, LinkedEditingRangesProvider
Expand Down Expand Up @@ -62,7 +63,7 @@ export class HTMLPlugin
}

const node = html.findNodeAt(document.offsetAt(position));
if (!node || this.possiblyComponent(node)) {
if (!node || possiblyComponent(node)) {
return null;
}

Expand Down Expand Up @@ -102,12 +103,23 @@ export class HTMLPlugin
)
]);
}

const results = this.isInComponentTag(html, document, position)
? // Only allow emmet inside component element tags.
// Other attributes/events would be false positives.
CompletionList.create([])
: this.lang.doComplete(document, position, html);
const items = this.toCompletionItems(results.items);

items.forEach((item) => {
if (item.label.startsWith('on:') && item.textEdit) {
item.textEdit = {
...item.textEdit,
newText: item.textEdit.newText.replace('="$1"', '$2="$1"')
Copy link
Member Author

Choose a reason for hiding this comment

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

Added a tab stop before the = sign. Making it the second one so that it less affect users' muscle memory

};
}
});

return CompletionList.create(
[
...this.toCompletionItems(items),
Expand Down Expand Up @@ -225,7 +237,7 @@ export class HTMLPlugin
}

const node = html.findNodeAt(document.offsetAt(position));
if (!node || this.possiblyComponent(node)) {
if (!node || possiblyComponent(node)) {
return null;
}

Expand All @@ -244,12 +256,7 @@ export class HTMLPlugin

const offset = document.offsetAt(position);
const node = html.findNodeAt(offset);
if (
!node ||
this.possiblyComponent(node) ||
!node.tag ||
!this.isRenameAtTag(node, offset)
) {
if (!node || possiblyComponent(node) || !node.tag || !this.isRenameAtTag(node, offset)) {
return null;
}
const tagNameStart = node.start + '<'.length;
Expand All @@ -276,16 +283,6 @@ export class HTMLPlugin
return { ranges };
}

/**
*
* The language service is case insensitive, and would provide
* hover info for Svelte components like `Option` which have
* the same name like a html tag.
*/
private possiblyComponent(node: Node): boolean {
return !!node.tag?.[0].match(/[A-Z]/);
}

/**
* Returns true if rename happens at the tag name, not anywhere inbetween.
*/
Expand Down
4 changes: 2 additions & 2 deletions packages/language-server/src/plugins/svelte/SveltePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,15 +160,15 @@ export class SveltePlugin
return null;
}

return getCompletions(svelteDoc, position);
return getCompletions(document, svelteDoc, position);
}

async doHover(document: Document, position: Position): Promise<Hover | null> {
if (!this.featureEnabled('hover')) {
return null;
}

return getHoverInfo(await this.getSvelteDoc(document), position);
return getHoverInfo(document, await this.getSvelteDoc(document), position);
}

async getCodeActions(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ import {
CompletionList,
CompletionItemKind,
CompletionItem,
InsertTextFormat
InsertTextFormat,
MarkupKind
} from 'vscode-languageserver';
import { SvelteTag, documentation, getLatestOpeningTag } from './SvelteTags';
import { isInTag } from '../../../lib/documents';
import { isInTag, Document } from '../../../lib/documents';
import { AttributeContext, getAttributeContextAtPosition } from '../../../lib/documents/parseHtml';
import { getModifierData } from './getModifierData';
import { attributeCanHaveEventModifier } from './utils';

const HTML_COMMENT_START = '<!--';

Expand All @@ -26,6 +30,7 @@ const componentDocumentationCompletion: CompletionItem = {
};

export function getCompletions(
document: Document,
svelteDoc: SvelteDocument,
position: Position
): CompletionList | null {
Expand All @@ -39,18 +44,22 @@ export function getCompletions(
.getText()
// use last 10 characters, should cover 99% of all cases
.substr(Math.max(offset - 10, 0), Math.min(offset, 10));
const notPreceededByOpeningBracket = !/[\s\S]*{\s*[#:/@]\w*$/.test(
lastCharactersBeforePosition
);
const precededByOpeningBracket = /[\s\S]*{\s*[#:/@]\w*$/.test(lastCharactersBeforePosition);
if (isInStyleOrScript) {
return null;
}

if (notPreceededByOpeningBracket) {
return getComponentDocumentationCompletions();
if (precededByOpeningBracket) {
return getTagCompletionsWithinMoustache();
}

const attributeContext = getAttributeContextAtPosition(document, position);

if (attributeContext) {
return getEventModifierCompletion(attributeContext);
}

return getTagCompletionsWithinMoustache();
return getComponentDocumentationCompletions();

/**
* Get completions for special svelte tags within moustache tags.
Expand Down Expand Up @@ -78,6 +87,32 @@ export function getCompletions(
}
}

function getEventModifierCompletion(attributeContext: AttributeContext): CompletionList | null {
const modifiers = getModifierData();

if (!attributeContext || !attributeCanHaveEventModifier(attributeContext)) {
return null;
}

const items = modifiers
.filter(
(modifier) =>
!attributeContext.name.includes('|' + modifier.modifier) &&
!modifier.modifiersInvalidWith?.some((invalidWith) =>
attributeContext.name.includes(invalidWith)
)
)
.map(
(m): CompletionItem => ({
label: m.modifier,
documentation: m.documentation,
kind: CompletionItemKind.Event
})
);

return CompletionList.create(items);
}

/**
* Get completions with regard to trigger character.
*/
Expand Down Expand Up @@ -207,7 +242,7 @@ function createCompletionItems(
kind: CompletionItemKind.Keyword,
preselect: true,
documentation: {
kind: 'markdown',
kind: MarkupKind.Markdown,
value: documentation[item.tag]
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,19 @@ import { Hover, Position } from 'vscode-languageserver';
import { SvelteDocument } from '../SvelteDocument';
import { documentation, SvelteTag, getLatestOpeningTag } from './SvelteTags';
import { flatten } from '../../../utils';
import { isInTag } from '../../../lib/documents';
import { Document, isInTag } from '../../../lib/documents';
import { AttributeContext, getAttributeContextAtPosition } from '../../../lib/documents/parseHtml';
import { attributeCanHaveEventModifier } from './utils';
import { getModifierData } from './getModifierData';

/**
* Get hover information for special svelte tags within moustache tags.
*/
export function getHoverInfo(svelteDoc: SvelteDocument, position: Position): Hover | null {
export function getHoverInfo(
document: Document,
svelteDoc: SvelteDocument,
position: Position
): Hover | null {
const offset = svelteDoc.offsetAt(position);

const isInStyleOrScript =
Expand All @@ -20,13 +27,34 @@ export function getHoverInfo(svelteDoc: SvelteDocument, position: Position): Hov
.getText()
// use last 10 and next 10 characters, should cover 99% of all cases
.substr(offsetStart, 20);
const isNoSvelteTag = !tagRegexp.test(charactersAroundOffset);
const isSvelteTag = tagRegexp.test(charactersAroundOffset);

if (isInStyleOrScript) {
return null;
}

if (isSvelteTag) {
return getTagHoverInfoAtOffset(svelteDoc, offsetStart, charactersAroundOffset, offset);
}

if (isInStyleOrScript || isNoSvelteTag) {
const attributeContext = getAttributeContextAtPosition(document, position);

if (!attributeContext || !attributeCanHaveEventModifier(attributeContext)) {
return null;
}

const tag = getTagAtOffset(svelteDoc, offsetStart, charactersAroundOffset, offset);
const attributeOffset = svelteDoc.getText().lastIndexOf(attributeContext.name, offset);

return getEventModifierHoverInfo(attributeContext, attributeOffset, offset);
}

function getTagHoverInfoAtOffset(
svelteDoc: SvelteDocument,
charactersOffset: number,
charactersAroundOffset: string,
offset: number
): Hover | null {
const tag = getTagAtOffset(svelteDoc, charactersOffset, charactersAroundOffset, offset);
if (!tag) {
return null;
}
Expand Down Expand Up @@ -91,3 +119,24 @@ const tagPossibilities: Array<{ tag: SvelteTag | ':else'; values: string[] }> =
const tagRegexp = new RegExp(
`[\\s\\S]*{\\s*(${flatten(tagPossibilities.map((p) => p.values)).join('|')})(\\s|})`
);
function getEventModifierHoverInfo(
attributeContext: AttributeContext,
attributeOffset: number,
offset: number
): Hover | null {
const { name } = attributeContext;

const modifierData = getModifierData();

const found = modifierData.find((modifier) =>
isAroundOffset(attributeOffset, name, modifier.modifier, offset)
);

if (!found) {
return null;
}

return {
contents: found.documentation
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { MarkupContent, MarkupKind } from 'vscode-languageserver';

export interface ModifierData {
modifier: string;
documentation: MarkupContent;
modifiersInvalidWith?: string[];
}

export function getModifierData(): ModifierData[] {
return [
{
modifier: 'preventDefault',
documentation: 'calls `event.preventDefault()` before running the handler',
modifiersInvalidWith: ['passive']
},
{
modifier: 'stopPropagation',
documentation:
'calls `event.stopPropagation()`, preventing the event reaching the next element'
},
{
modifier: 'passive',
documentation:
'improves scrolling performance on touch/wheel events ' +
"(Svelte will add it automatically where it's safe to do so)",
modifiersInvalidWith: ['nopassive', 'preventDefault']
},
{
modifier: 'nonpassive',
documentation: 'explicitly set `passive: false`',
modifiersInvalidWith: ['passive']
},
{
modifier: 'capture',
documentation:
'fires the handler during the capture phase instead of the bubbling phase'
},
{
modifier: 'once',
documentation: 'remove the handler after the first time it runs'
},
{
modifier: 'self',
documentation: 'only trigger handler if `event.target` is the element itself'
},
{
modifier: 'trusted',
documentation:
'only trigger handler if event.isTrusted is true. ' +
'I.e. if the event is triggered by a user action'
}
].map((item) => ({
...item,
documentation: {
kind: MarkupKind.Markdown,
value: `\`${item.modifier}\` event modifier

${item.documentation}

https://svelte.dev/docs#on_element_event`
}
}));
}
11 changes: 11 additions & 0 deletions packages/language-server/src/plugins/svelte/features/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { AttributeContext } from '../../../lib/documents/parseHtml';
import { possiblyComponent } from '../../../utils';

export function attributeCanHaveEventModifier(attributeContext: AttributeContext) {
return (
!attributeContext.inValue &&
!possiblyComponent(attributeContext.elementTag) &&
attributeContext.name.startsWith('on:') &&
attributeContext.name.includes('|')
);
}
3 changes: 2 additions & 1 deletion packages/language-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,8 @@ export function startServer(options?: LSOptions) {
// of other completion providers

// Svelte
':'
':',
'|'
]
},
documentFormattingProvider: true,
Expand Down
Loading