Skip to content

Commit

Permalink
(feat) component events hover info (#485)
Browse files Browse the repository at this point in the history
Provides hover info for events with name, type, doc, if available
  • Loading branch information
dummdidumm authored Aug 27, 2020
1 parent 5068244 commit 137b378
Show file tree
Hide file tree
Showing 11 changed files with 313 additions and 131 deletions.
19 changes: 19 additions & 0 deletions packages/language-server/src/lib/documents/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,3 +313,22 @@ export function getNodeIfIsInComponentStartTag(
return node;
}
}

/**
* Gets word at position.
* Delimiter is by default a whitespace, but can be adjusted.
*/
export function getWordAt(
str: string,
pos: number,
delimiterRegex = { left: /\S+$/, right: /\s/ },
): string {
const left = str.slice(0, pos + 1).search(delimiterRegex.left);
const right = str.slice(pos).search(delimiterRegex.right);

if (right < 0) {
return str.slice(left);
}

return str.slice(left, right + pos);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,7 @@ import {
SymbolInformation,
WorkspaceEdit,
} from 'vscode-languageserver';
import {
Document,
DocumentManager,
mapHoverToParent,
mapSymbolInformationToOriginal,
} from '../../lib/documents';
import { Document, DocumentManager, mapSymbolInformationToOriginal } from '../../lib/documents';
import { LSConfigManager, LSTypescriptConfig } from '../../ls-config';
import { pathToUrl } from '../../utils';
import {
Expand All @@ -42,15 +37,11 @@ import {
CompletionsProviderImpl,
} from './features/CompletionProvider';
import { DiagnosticsProviderImpl } from './features/DiagnosticsProvider';
import { HoverProviderImpl } from './features/HoverProvider';
import { RenameProviderImpl } from './features/RenameProvider';
import { UpdateImportsProviderImpl } from './features/UpdateImportsProvider';
import { LSAndTSDocResolver } from './LSAndTSDocResolver';
import {
convertRange,
convertToLocationRange,
getScriptKindFromFileName,
symbolKindFromString,
} from './utils';
import { RenameProviderImpl } from './features/RenameProvider';
import { convertToLocationRange, getScriptKindFromFileName, symbolKindFromString } from './utils';

export class TypeScriptPlugin
implements
Expand All @@ -70,6 +61,7 @@ export class TypeScriptPlugin
private readonly updateImportsProvider: UpdateImportsProviderImpl;
private readonly diagnosticsProvider: DiagnosticsProviderImpl;
private readonly renameProvider: RenameProviderImpl;
private readonly hoverProvider: HoverProviderImpl;

constructor(
docManager: DocumentManager,
Expand All @@ -86,6 +78,7 @@ export class TypeScriptPlugin
this.updateImportsProvider = new UpdateImportsProviderImpl(this.lsAndTsDocResolver);
this.diagnosticsProvider = new DiagnosticsProviderImpl(this.lsAndTsDocResolver);
this.renameProvider = new RenameProviderImpl(this.lsAndTsDocResolver);
this.hoverProvider = new HoverProviderImpl(this.lsAndTsDocResolver);
}

async getDiagnostics(document: Document): Promise<Diagnostic[]> {
Expand All @@ -101,30 +94,7 @@ export class TypeScriptPlugin
return null;
}

const { lang, tsDoc } = this.getLSAndTSDoc(document);
const fragment = await tsDoc.getFragment();
const info = lang.getQuickInfoAtPosition(
tsDoc.filePath,
fragment.offsetAt(fragment.getGeneratedPosition(position)),
);
if (!info) {
return null;
}
const declaration = ts.displayPartsToString(info.displayParts);
const documentation =
typeof info.documentation === 'string'
? info.documentation
: ts.displayPartsToString(info.documentation);

// https://microsoft.github.io/language-server-protocol/specification#textDocument_hover
const contents = ['```typescript', declaration, '```']
.concat(documentation ? ['---', documentation] : [])
.join('\n');

return mapHoverToParent(fragment, {
range: convertRange(fragment, info.textSpan),
contents,
});
return this.hoverProvider.doHover(document, position);
}

async getDocumentSymbols(document: Document): Promise<SymbolInformation[]> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,29 @@ import {
CompletionContext,
CompletionList,
CompletionTriggerKind,
MarkupContent,
MarkupKind,
Position,
Range,
TextDocumentIdentifier,
TextEdit,
MarkupContent,
MarkupKind,
} from 'vscode-languageserver';
import {
Document,
isInTag,
mapCompletionItemToOriginal,
mapRangeToOriginal,
getNodeIfIsInComponentStartTag,
} from '../../../lib/documents';
import { isNotNullOrUndefined, pathToUrl, getRegExpMatches, flatten } from '../../../utils';
import { flatten, getRegExpMatches, isNotNullOrUndefined, pathToUrl } from '../../../utils';
import { AppCompletionItem, AppCompletionList, CompletionsProvider } from '../../interfaces';
import { SvelteSnapshotFragment, SvelteDocumentSnapshot } from '../DocumentSnapshot';
import { SvelteDocumentSnapshot, SvelteSnapshotFragment } from '../DocumentSnapshot';
import { LSAndTSDocResolver } from '../LSAndTSDocResolver';
import {
convertRange,
getCommitCharactersForScriptElement,
scriptElementKindToCompletionItemKind,
} from '../utils';
import { getComponentAtPosition } from './utils';

export interface CompletionEntryWithIdentifer extends ts.CompletionEntry, TextDocumentIdentifier {
position: Position;
Expand Down Expand Up @@ -135,7 +135,14 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
fragment: SvelteSnapshotFragment,
originalPosition: Position,
): AppCompletionItem<CompletionEntryWithIdentifer>[] {
const snapshot = this.getComponentAtPosition(lang, doc, tsDoc, fragment, originalPosition);
const snapshot = getComponentAtPosition(
this.lsAndTsDocResovler,
lang,
doc,
tsDoc,
fragment,
originalPosition,
);
if (!snapshot) {
return [];
}
Expand All @@ -148,50 +155,6 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
}));
}

/**
* If the completion happens inside the template and within the
* tag of a Svelte component, then retrieve its snapshot.
*/
private getComponentAtPosition(
lang: ts.LanguageService,
doc: Document,
tsDoc: SvelteDocumentSnapshot,
fragment: SvelteSnapshotFragment,
originalPosition: Position,
): SvelteDocumentSnapshot | null {
if (tsDoc.parserError) {
return null;
}

if (
isInTag(originalPosition, doc.scriptInfo) ||
isInTag(originalPosition, doc.moduleScriptInfo)
) {
// Inside script tags -> not a component
return null;
}

const node = getNodeIfIsInComponentStartTag(doc.html, doc.offsetAt(originalPosition));
if (!node) {
return null;
}

const generatedPosition = fragment.getGeneratedPosition(doc.positionAt(node.start + 1));
const def = lang.getDefinitionAtPosition(
tsDoc.filePath,
fragment.offsetAt(generatedPosition),
)?.[0];
if (!def) {
return null;
}

const snapshot = this.lsAndTsDocResovler.getSnapshot(def.fileName);
if (!(snapshot instanceof SvelteDocumentSnapshot)) {
return null;
}
return snapshot;
}

private toCompletionItem(
fragment: SvelteSnapshotFragment,
comp: ts.CompletionEntry,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import ts from 'typescript';
import { Hover, Position } from 'vscode-languageserver';
import { Document, getWordAt, mapHoverToParent } from '../../../lib/documents';
import { HoverProvider } from '../../interfaces';
import { SvelteDocumentSnapshot, SvelteSnapshotFragment } from '../DocumentSnapshot';
import { LSAndTSDocResolver } from '../LSAndTSDocResolver';
import { convertRange } from '../utils';
import { getComponentAtPosition } from './utils';

export class HoverProviderImpl implements HoverProvider {
constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {}

async doHover(document: Document, position: Position): Promise<Hover | null> {
const { lang, tsDoc } = this.getLSAndTSDoc(document);
const fragment = await tsDoc.getFragment();

const eventHoverInfo = this.getEventHoverInfo(lang, document, tsDoc, fragment, position);
if (eventHoverInfo) {
return eventHoverInfo;
}

const info = lang.getQuickInfoAtPosition(
tsDoc.filePath,
fragment.offsetAt(fragment.getGeneratedPosition(position)),
);
if (!info) {
return null;
}

const declaration = ts.displayPartsToString(info.displayParts);
const documentation =
typeof info.documentation === 'string'
? info.documentation
: ts.displayPartsToString(info.documentation);

// https://microsoft.github.io/language-server-protocol/specification#textDocument_hover
const contents = ['```typescript', declaration, '```']
.concat(documentation ? ['---', documentation] : [])
.join('\n');

return mapHoverToParent(fragment, {
range: convertRange(fragment, info.textSpan),
contents,
});
}

private getEventHoverInfo(
lang: ts.LanguageService,
doc: Document,
tsDoc: SvelteDocumentSnapshot,
fragment: SvelteSnapshotFragment,
originalPosition: Position,
): Hover | null {
const possibleEventName = getWordAt(doc.getText(), doc.offsetAt(originalPosition), {
left: /\S+$/,
right: /[\s=]/,
});
if (!possibleEventName.startsWith('on:')) {
return null;
}

const component = getComponentAtPosition(
this.lsAndTsDocResolver,
lang,
doc,
tsDoc,
fragment,
originalPosition,
);
if (!component) {
return null;
}

const eventName = possibleEventName.substr('on:'.length);
const event = component.getEvents().find((event) => event.name === eventName);
if (!event) {
return null;
}

return {
contents: [
'```typescript',
`${event.name}: ${event.type}`,
'```',
event.doc || '',
].join('\n'),
};
}

private getLSAndTSDoc(document: Document) {
return this.lsAndTsDocResolver.getLSAndTSDoc(document);
}
}
50 changes: 50 additions & 0 deletions packages/language-server/src/plugins/typescript/features/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import ts from 'typescript';
import { Position } from 'vscode-languageserver';
import { Document, getNodeIfIsInComponentStartTag, isInTag } from '../../../lib/documents';
import { SvelteDocumentSnapshot, SvelteSnapshotFragment } from '../DocumentSnapshot';
import { LSAndTSDocResolver } from '../LSAndTSDocResolver';

/**
* If the given original position is within a Svelte starting tag,
* return the snapshot of that component.
*/
export function getComponentAtPosition(
lsAndTsDocResovler: LSAndTSDocResolver,
lang: ts.LanguageService,
doc: Document,
tsDoc: SvelteDocumentSnapshot,
fragment: SvelteSnapshotFragment,
originalPosition: Position,
): SvelteDocumentSnapshot | null {
if (tsDoc.parserError) {
return null;
}

if (
isInTag(originalPosition, doc.scriptInfo) ||
isInTag(originalPosition, doc.moduleScriptInfo)
) {
// Inside script tags -> not a component
return null;
}

const node = getNodeIfIsInComponentStartTag(doc.html, doc.offsetAt(originalPosition));
if (!node) {
return null;
}

const generatedPosition = fragment.getGeneratedPosition(doc.positionAt(node.start + 1));
const def = lang.getDefinitionAtPosition(
tsDoc.filePath,
fragment.offsetAt(generatedPosition),
)?.[0];
if (!def) {
return null;
}

const snapshot = lsAndTsDocResovler.getSnapshot(def.fileName);
if (!(snapshot instanceof SvelteDocumentSnapshot)) {
return null;
}
return snapshot;
}
26 changes: 26 additions & 0 deletions packages/language-server/test/lib/documents/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
extractStyleTag,
extractScriptTags,
updateRelativeImport,
getWordAt,
} from '../../../src/lib/documents/utils';
import { Position } from 'vscode-languageserver';

Expand Down Expand Up @@ -328,4 +329,29 @@ describe('document/utils', () => {
assert.deepStrictEqual(newPath, './oldPath/someTsFile');
});
});

describe('#getWordAt', () => {
it('returns word between whitespaces', () => {
assert.equal(getWordAt('qwd asd qwd', 5), 'asd');
});

it('returns word between whitespace and end of string', () => {
assert.equal(getWordAt('qwd asd', 5), 'asd');
});

it('returns word between start of string and whitespace', () => {
assert.equal(getWordAt('asd qwd', 2), 'asd');
});

it('returns word between start of string and end of string', () => {
assert.equal(getWordAt('asd', 2), 'asd');
});

it('returns word with custom delimiters', () => {
assert.equal(
getWordAt('asd on:asd-qwd="asd" ', 10, { left: /\S+$/, right: /[\s=]/ }),
'on:asd-qwd',
);
});
});
});
Loading

0 comments on commit 137b378

Please sign in to comment.