├── .gitignore ├── .vscode ├── extensions.json ├── settings.json ├── launch.json └── tasks.json ├── images ├── icon.png ├── props-hover.gif ├── tag-opening.gif ├── script-getter.gif ├── script-setter.gif ├── component-hover.gif ├── component-methods.gif ├── goto-definition.gif ├── script-computed.gif ├── template-blocks.gif ├── html-tag-attributes.gif ├── component-data-slots.gif ├── component-events-expr.gif ├── goto-field-definition.gif ├── goto-method-definition.gif ├── goto-slot-definition.gif ├── script-path-resolver.gif └── script-component-path-resolver.gif ├── .gitattributes ├── .yo-rc.json ├── .vscodeignore ├── tsconfig.base.json ├── tslint.json ├── client ├── tsconfig.json ├── package.json └── src │ ├── test │ ├── extension.test.ts │ └── index.ts │ └── extension.ts ├── server ├── tsconfig.json ├── package.json └── src │ ├── services │ ├── DocumentService.ts │ ├── script │ │ ├── svelte2 │ │ │ ├── ComputedDependencyService.ts │ │ │ ├── ComponentGetDataService.ts │ │ │ ├── ComputedSectionService.ts │ │ │ ├── ComponentSetDataService.ts │ │ │ ├── ComponentsSectionService.ts │ │ │ └── ComponentPrivateService.ts │ │ ├── ComponentNameService.ts │ │ ├── ScriptService.ts │ │ ├── ImportStatementService.ts │ │ └── ComponentPathService.ts │ ├── markup │ │ ├── MarkupService.ts │ │ ├── html │ │ │ ├── HtmlTagEventService.ts │ │ │ ├── HtmlTagActionService.ts │ │ │ ├── HtmlTagTransitionInService.ts │ │ │ ├── HtmlTagTransitionOutService.ts │ │ │ ├── HtmlTagTransitionService.ts │ │ │ ├── HtmlTagInnerService.ts │ │ │ ├── HtmlTagBindService.ts │ │ │ ├── HtmlTagAttributeAssignService.ts │ │ │ └── HtmlTagDefaultService.ts │ │ ├── block │ │ │ ├── BlockCloseService.ts │ │ │ ├── BlockInnerService.ts │ │ │ ├── BlockOpenService.ts │ │ │ └── BlockHelpers.ts │ │ ├── TagInnerService.ts │ │ ├── component │ │ │ ├── ComponentDataService.ts │ │ │ ├── ComponentInnerService.ts │ │ │ ├── ComponentDefaultSlotParamsService.ts │ │ │ ├── ComponentBindService.ts │ │ │ ├── ComponentEventService.ts │ │ │ ├── ComponentAttributeAssignService.ts │ │ │ └── ComponentDefaultService.ts │ │ ├── BindTargetPropertyService.ts │ │ ├── SlotService.ts │ │ ├── ExpressionService.ts │ │ ├── PlaceholdersService.ts │ │ ├── NamedSlotParamsService.ts │ │ ├── OpenTagService.ts │ │ └── TagHelpers.ts │ ├── style │ │ ├── StyleService.ts │ │ └── svelte2 │ │ │ └── RefsStyleService.ts │ ├── Utils.ts │ ├── Common.ts │ ├── CompositeService.ts │ └── ChoosingService.ts │ ├── DocumentsCache.ts │ ├── importResolvers │ ├── RollupImportResolver.ts │ ├── AliasImportResolver.ts │ ├── WebpackImportResolver.ts │ └── NodeModulesImportResolver.ts │ ├── SvelteDocument.ts │ ├── SvelteItemsHelpers.ts │ ├── interfaces.d.ts │ ├── StringHelpers.ts │ ├── svelteDocUtils.ts │ ├── utils.ts │ ├── svelte2Language.ts │ ├── server.ts │ ├── svelte3Language.ts │ └── svelteLanguage.ts ├── package.json ├── CHANGELOG.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | .vscode-test/ 4 | *.vsix 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "jamesbirtles.svelte-vscode" 4 | ] 5 | } -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArdenIvanov/svelte-intellisense/HEAD/images/icon.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behavior to automatically normalize line endings. 2 | * text=auto 3 | 4 | -------------------------------------------------------------------------------- /images/props-hover.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArdenIvanov/svelte-intellisense/HEAD/images/props-hover.gif -------------------------------------------------------------------------------- /images/tag-opening.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArdenIvanov/svelte-intellisense/HEAD/images/tag-opening.gif -------------------------------------------------------------------------------- /images/script-getter.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArdenIvanov/svelte-intellisense/HEAD/images/script-getter.gif -------------------------------------------------------------------------------- /images/script-setter.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArdenIvanov/svelte-intellisense/HEAD/images/script-setter.gif -------------------------------------------------------------------------------- /images/component-hover.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArdenIvanov/svelte-intellisense/HEAD/images/component-hover.gif -------------------------------------------------------------------------------- /images/component-methods.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArdenIvanov/svelte-intellisense/HEAD/images/component-methods.gif -------------------------------------------------------------------------------- /images/goto-definition.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArdenIvanov/svelte-intellisense/HEAD/images/goto-definition.gif -------------------------------------------------------------------------------- /images/script-computed.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArdenIvanov/svelte-intellisense/HEAD/images/script-computed.gif -------------------------------------------------------------------------------- /images/template-blocks.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArdenIvanov/svelte-intellisense/HEAD/images/template-blocks.gif -------------------------------------------------------------------------------- /.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "generator-code": { 3 | "promptValues": { 4 | "publisher": "ardenivanov" 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /images/html-tag-attributes.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArdenIvanov/svelte-intellisense/HEAD/images/html-tag-attributes.gif -------------------------------------------------------------------------------- /images/component-data-slots.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArdenIvanov/svelte-intellisense/HEAD/images/component-data-slots.gif -------------------------------------------------------------------------------- /images/component-events-expr.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArdenIvanov/svelte-intellisense/HEAD/images/component-events-expr.gif -------------------------------------------------------------------------------- /images/goto-field-definition.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArdenIvanov/svelte-intellisense/HEAD/images/goto-field-definition.gif -------------------------------------------------------------------------------- /images/goto-method-definition.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArdenIvanov/svelte-intellisense/HEAD/images/goto-method-definition.gif -------------------------------------------------------------------------------- /images/goto-slot-definition.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArdenIvanov/svelte-intellisense/HEAD/images/goto-slot-definition.gif -------------------------------------------------------------------------------- /images/script-path-resolver.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArdenIvanov/svelte-intellisense/HEAD/images/script-path-resolver.gif -------------------------------------------------------------------------------- /images/script-component-path-resolver.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArdenIvanov/svelte-intellisense/HEAD/images/script-component-path-resolver.gif -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | client/out/test/** 4 | client/out/**/*.map 5 | client/src/** 6 | server/out/**/*.map 7 | server/src/** 8 | .gitignore 9 | tsconfig.base.json 10 | tslint.json -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noUnusedLocals": false, 4 | "noImplicitReturns": true, 5 | "noFallthroughCasesInSwitch": true, 6 | "noUnusedParameters": true, 7 | } 8 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-string-throw": true, 4 | "no-unused-expression": true, 5 | "no-duplicate-variable": true, 6 | "curly": true, 7 | "class-name": true, 8 | "semicolon": [ 9 | true, 10 | "always" 11 | ], 12 | "triple-equals": true 13 | }, 14 | "defaultSeverity": "warning" 15 | } -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "target": "es6", 6 | "outDir": "out", 7 | "lib": [ "es6" ], 8 | "sourceMap": true, 9 | "rootDir": "src", 10 | }, 11 | "exclude": [ 12 | "node_modules", 13 | ] 14 | } -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "target": "es5", 6 | "outDir": "out", 7 | "lib": [ "es6" ], 8 | "sourceMap": true, 9 | "rootDir": "src", 10 | "moduleResolution": "node", 11 | "esModuleInterop": true 12 | }, 13 | "include": [ 14 | "src" 15 | ], 16 | "exclude": [ 17 | "node_modules", 18 | ] 19 | } -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-intellisense", 3 | "engines": { 4 | "vscode": "^1.30.0" 5 | }, 6 | "repository": { 7 | "url": "https://github.com/ArdenIvanov/svelte-intellisense.git" 8 | }, 9 | "license": "MIT", 10 | "scripts": { 11 | "update-vscode": "yarn vscode-install", 12 | "postinstall": "yarn vscode-install" 13 | }, 14 | "devDependencies": { 15 | "vscode": "^1.1.36" 16 | }, 17 | "dependencies": { 18 | "vscode-languageclient": "^5.2.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-intellisense", 3 | "engines": { 4 | "node": "*" 5 | }, 6 | "repository": { 7 | "url": "https://github.com/ArdenIvanov/svelte-intellisense.git" 8 | }, 9 | "license": "MIT", 10 | "dependencies": { 11 | "vscode-languageserver": "^5.2.1", 12 | "sveltedoc-parser": "^2.3.0", 13 | "cosmiconfig": "^5.2.1", 14 | "@babel/register": "^7.5.5", 15 | "@babel/core": "7.5.5", 16 | "@babel/preset-env": "7.5.5" 17 | }, 18 | "scripts": {} 19 | } 20 | -------------------------------------------------------------------------------- /server/src/services/DocumentService.ts: -------------------------------------------------------------------------------- 1 | import { ChoosingService } from "./ChoosingService"; 2 | import { ScriptService } from "./script/ScriptService"; 3 | import { StyleService } from "./style/StyleService"; 4 | import { MarkupService } from "./markup/MarkupService"; 5 | 6 | export class DocumentService extends ChoosingService { 7 | public constructor() { 8 | super([ 9 | new ScriptService(), 10 | new StyleService(), 11 | new MarkupService() 12 | ]); 13 | } 14 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off", 11 | "npm.exclude": ["**/client", "**/server"], 12 | "npm.enableScriptExplorer": true 13 | } -------------------------------------------------------------------------------- /server/src/DocumentsCache.ts: -------------------------------------------------------------------------------- 1 | import { SvelteDocument } from "./SvelteDocument"; 2 | 3 | export class DocumentsCache { 4 | private cache: Map = new Map(); 5 | 6 | public has(path: string): boolean { 7 | return this.cache.has(path); 8 | } 9 | 10 | public get(path: string): SvelteDocument { 11 | return this.cache.get(path); 12 | } 13 | 14 | public getOrCreateDocumentFromCache(path: string, createIfNotExists = true): SvelteDocument { 15 | if (!this.cache.has(path)) { 16 | if (createIfNotExists) { 17 | this.cache.set(path, new SvelteDocument(path)); 18 | } else { 19 | return null; 20 | } 21 | } 22 | 23 | return this.cache.get(path); 24 | } 25 | } -------------------------------------------------------------------------------- /client/src/test/extension.test.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Note: This example test is leveraging the Mocha test framework. 3 | // Please refer to their documentation on https://mochajs.org/ for help. 4 | // 5 | 6 | // The module 'assert' provides assertion methods from node 7 | import * as assert from 'assert'; 8 | 9 | // You can import and use all API from the 'vscode' module 10 | // as well as import your extension to test it 11 | // import * as vscode from 'vscode'; 12 | // import * as myExtension from '../extension'; 13 | 14 | // Defines a Mocha test suite to group tests of similar kind together 15 | suite("Extension Tests", function () { 16 | 17 | // Defines a Mocha unit test 18 | test("Something 1", function() { 19 | assert.equal(-1, [1, 2, 3].indexOf(5)); 20 | assert.equal(-1, [1, 2, 3].indexOf(0)); 21 | }); 22 | }); -------------------------------------------------------------------------------- /server/src/services/script/svelte2/ComputedDependencyService.ts: -------------------------------------------------------------------------------- 1 | import { BaseService } from "../../Common"; 2 | import { SvelteDocument, SVELTE_VERSION_2 } from "../../../SvelteDocument"; 3 | import { ScopeContext } from "../../../interfaces"; 4 | 5 | export class ComputedDependencyService extends BaseService { 6 | public getSupportedSvelteVersions() { 7 | return [SVELTE_VERSION_2]; 8 | } 9 | 10 | public getCompletitionItems(document: SvelteDocument, context: ScopeContext) { 11 | const content = context.content.substring(0, context.offset); 12 | 13 | 14 | if (/[{,]?\s*[\w_]+[\w\d_]*\s*\:?\s*\(\s*\{\s*[\w\d_,\s]*$/g.test(content)) { 15 | return document.metadata 16 | ? document.metadata.data 17 | : []; 18 | } 19 | 20 | return null; 21 | } 22 | } -------------------------------------------------------------------------------- /server/src/services/markup/MarkupService.ts: -------------------------------------------------------------------------------- 1 | import { ChoosingService } from "../ChoosingService"; 2 | import { BlockInnerService } from "./block/BlockInnerService"; 3 | import { BlockOpenService } from "./block/BlockOpenService"; 4 | import { BlockCloseService } from "./block/BlockCloseService"; 5 | import { OpenTagService } from "./OpenTagService"; 6 | import { PlaceholdersService } from "./PlaceholdersService"; 7 | import { TagInnerService } from "./TagInnerService"; 8 | 9 | export class MarkupService extends ChoosingService { 10 | public constructor() { 11 | super([ 12 | new OpenTagService(), 13 | new TagInnerService(), 14 | 15 | new BlockOpenService(), 16 | new BlockInnerService(), 17 | new BlockCloseService(), 18 | 19 | new PlaceholdersService(), 20 | ]); 21 | } 22 | } -------------------------------------------------------------------------------- /server/src/services/markup/html/HtmlTagEventService.ts: -------------------------------------------------------------------------------- 1 | import { SvelteDocument } from "../../../SvelteDocument"; 2 | import { CompletionItem } from "vscode-languageserver"; 3 | import { findLastDirectiveIndex } from "../TagHelpers"; 4 | import { BaseService } from "../../Common"; 5 | import { TagScopeContext } from "../TagInnerService"; 6 | import { EventModifiers } from "../../../svelteLanguage"; 7 | 8 | export class HtmlTagEventService extends BaseService { 9 | public getCompletitionItems(_document: SvelteDocument, context: TagScopeContext): Array { 10 | const index = findLastDirectiveIndex(context.content, context.offset, 'on'); 11 | if (index < 0) { 12 | return null; 13 | } 14 | 15 | const contentPart = context.content.substring(index, context.offset); 16 | if (/on:[\w\d_|]*\|[\w\d_]*$/g.test(contentPart)) { 17 | return EventModifiers; 18 | } 19 | 20 | return null; 21 | } 22 | } -------------------------------------------------------------------------------- /server/src/services/markup/html/HtmlTagActionService.ts: -------------------------------------------------------------------------------- 1 | import { SvelteDocument } from "../../../SvelteDocument"; 2 | import { CompletionItem } from "vscode-languageserver"; 3 | import { BaseService } from "../../Common"; 4 | import { findLastDirectiveIndex } from "../TagHelpers"; 5 | import { TagScopeContext } from "../TagInnerService"; 6 | 7 | export class HtmlTagActionService extends BaseService { 8 | public getCompletitionItems(document: SvelteDocument, context: TagScopeContext): Array { 9 | const index = findLastDirectiveIndex(context.content, context.offset, 'use'); 10 | if (index < 0) { 11 | return null; 12 | } 13 | 14 | const contentPart = context.content.substring(index, context.offset); 15 | if (/use:[\w\d_]*$/g.test(contentPart)) { 16 | return document.metadata 17 | ? document.metadata.actions 18 | : []; 19 | } 20 | 21 | return null; 22 | } 23 | } -------------------------------------------------------------------------------- /server/src/services/markup/html/HtmlTagTransitionInService.ts: -------------------------------------------------------------------------------- 1 | import { SvelteDocument } from "../../../SvelteDocument"; 2 | import { CompletionItem } from "vscode-languageserver"; 3 | import { BaseService } from "../../Common"; 4 | import { findLastDirectiveIndex } from "../TagHelpers"; 5 | import { TagScopeContext } from "../TagInnerService"; 6 | 7 | export class HtmlTagTransionInService extends BaseService { 8 | 9 | public getCompletitionItems(document: SvelteDocument, context: TagScopeContext): Array { 10 | const index = findLastDirectiveIndex(context.content, context.offset, 'in'); 11 | if (index < 0) { 12 | return null; 13 | } 14 | 15 | const contentPart = context.content.substring(index, context.offset); 16 | if (/in:[\w\d_]*$/g.test(contentPart)) { 17 | return document.metadata 18 | ? document.metadata.transitions 19 | : []; 20 | } 21 | 22 | return null; 23 | } 24 | } -------------------------------------------------------------------------------- /server/src/services/markup/html/HtmlTagTransitionOutService.ts: -------------------------------------------------------------------------------- 1 | import { SvelteDocument } from "../../../SvelteDocument"; 2 | import { CompletionItem } from "vscode-languageserver"; 3 | import { BaseService } from "../../Common"; 4 | import { findLastDirectiveIndex } from "../TagHelpers"; 5 | import { TagScopeContext } from "../TagInnerService"; 6 | 7 | export class HtmlTagTransionOutService extends BaseService { 8 | 9 | public getCompletitionItems(document: SvelteDocument, context: TagScopeContext): Array { 10 | const index = findLastDirectiveIndex(context.content, context.offset, 'out'); 11 | if (index < 0) { 12 | return null; 13 | } 14 | 15 | const contentPart = context.content.substring(index, context.offset); 16 | if (/out:[\w\d_]*$/g.test(contentPart)) { 17 | return document.metadata 18 | ? document.metadata.transitions 19 | : []; 20 | } 21 | 22 | return null; 23 | } 24 | } -------------------------------------------------------------------------------- /server/src/services/markup/html/HtmlTagTransitionService.ts: -------------------------------------------------------------------------------- 1 | import { SvelteDocument } from "../../../SvelteDocument"; 2 | import { CompletionItem } from "vscode-languageserver"; 3 | import { BaseService } from "../../Common"; 4 | import { findLastDirectiveIndex } from "../TagHelpers"; 5 | import { TagScopeContext } from "../TagInnerService"; 6 | 7 | export class HtmlTagTransionService extends BaseService { 8 | 9 | public getCompletitionItems(document: SvelteDocument, context: TagScopeContext): Array { 10 | const index = findLastDirectiveIndex(context.content, context.offset, 'transition'); 11 | if (index < 0) { 12 | return null; 13 | } 14 | 15 | const contentPart = context.content.substring(index, context.offset); 16 | if (/transition:[\w\d_]*$/g.test(contentPart)) { 17 | return document.metadata 18 | ? document.metadata.transitions 19 | : []; 20 | } 21 | 22 | return null; 23 | } 24 | } -------------------------------------------------------------------------------- /server/src/services/script/svelte2/ComponentGetDataService.ts: -------------------------------------------------------------------------------- 1 | import { BaseService } from "../../Common"; 2 | import { SvelteDocument, SVELTE_VERSION_2 } from "../../../SvelteDocument"; 3 | import { CompletionItem } from "vscode-languageserver"; 4 | import { ScopeContext } from "../../../interfaces"; 5 | 6 | export class ComponentGetDataService extends BaseService { 7 | public getSupportedSvelteVersions() { 8 | return [SVELTE_VERSION_2]; 9 | } 10 | 11 | public getCompletitionItems(document: SvelteDocument, context: ScopeContext): Array { 12 | const contentPart = context.content.substring(0, context.offset); 13 | if (/\b(const|var|let)\s*{\s*[\w\d_,\s]*$/g.test(contentPart) 14 | || /\bthis\s*\.\s*get\s*\(\s*\)\s*\.\s*[\w\d_]*$/g.test(contentPart) 15 | ) { 16 | return document.metadata ? [ 17 | ...document.metadata.data, 18 | ...document.metadata.computed 19 | ] : []; 20 | } 21 | 22 | return null; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /server/src/importResolvers/RollupImportResolver.ts: -------------------------------------------------------------------------------- 1 | import { DocumentsCache } from "../DocumentsCache"; 2 | import { AliasImportResolver } from "./AliasImportResolver"; 3 | 4 | export class RollupImportResolver extends AliasImportResolver { 5 | private resolvePlugins: Array; 6 | 7 | constructor(documentsCache: DocumentsCache, documentPath: string, plugins: Array) { 8 | super(documentsCache, documentPath); 9 | this.resolvePlugins = plugins.filter(x => x.hasOwnProperty('resolveId')); 10 | } 11 | 12 | protected findFileWithAlias(partialPath: string) : string { 13 | let importFilePath = null; 14 | 15 | this.resolvePlugins.forEach(plugin => { 16 | try { 17 | const resolvedId = plugin.resolveId(partialPath, this.documentPath); 18 | if (resolvedId && (typeof resolvedId === 'string')) { 19 | importFilePath = resolvedId; 20 | } 21 | } catch {} 22 | }); 23 | 24 | return importFilePath; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /client/src/test/index.ts: -------------------------------------------------------------------------------- 1 | // 2 | // PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING 3 | // 4 | // This file is providing the test runner to use when running extension tests. 5 | // By default the test runner in use is Mocha based. 6 | // 7 | // You can provide your own test runner if you want to override it by exporting 8 | // a function run(testRoot: string, clb: (error:Error) => void) that the extension 9 | // host can call to run the tests. The test runner is expected to use console.log 10 | // to report the results back to the caller. When the tests are finished, return 11 | // a possible error to the callback or null if none. 12 | 13 | import * as testRunner from 'vscode/lib/testrunner'; 14 | 15 | // You can directly control Mocha options by uncommenting the following lines 16 | // See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info 17 | testRunner.configure({ 18 | ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) 19 | useColors: true // colored output from test results 20 | }); 21 | 22 | module.exports = testRunner; -------------------------------------------------------------------------------- /server/src/SvelteDocument.ts: -------------------------------------------------------------------------------- 1 | import { ImportedComponent, ComponentMetadata, ImportResolver } from './interfaces'; 2 | import { SvelteComponentDoc } from 'sveltedoc-parser/typings'; 3 | import { Position, TextDocument } from 'vscode-languageserver'; 4 | 5 | export const SVELTE_VERSION_2 = 2; 6 | export const SVELTE_VERSION_3 = 3; 7 | 8 | export class SvelteDocument { 9 | 10 | constructor(path: string) { 11 | this.path = path; 12 | this.importedComponents = []; 13 | this.importResolver = null; 14 | } 15 | 16 | path: string; 17 | sveltedoc: SvelteComponentDoc; 18 | metadata: ComponentMetadata; 19 | importedComponents: ImportedComponent[]; 20 | content: string; 21 | importResolver: ImportResolver; 22 | document: TextDocument; 23 | 24 | public svelteVersion(): number { 25 | return this.sveltedoc ? this.sveltedoc.version | SVELTE_VERSION_3 : SVELTE_VERSION_3; 26 | } 27 | 28 | public offsetAt(position: Position): number { 29 | return this.document.offsetAt(position); 30 | } 31 | 32 | public positionAt(offset: number): Position { 33 | return this.document.positionAt(offset); 34 | } 35 | } -------------------------------------------------------------------------------- /server/src/services/style/StyleService.ts: -------------------------------------------------------------------------------- 1 | import { ChoosingService } from "../ChoosingService"; 2 | import { ScopeContext } from "../../interfaces"; 3 | import { RefsStyleService } from "./svelte2/RefsStyleService"; 4 | 5 | export class StyleService extends ChoosingService { 6 | public constructor() { 7 | super([ 8 | new RefsStyleService() 9 | ], { 10 | exclusive: true 11 | }); 12 | } 13 | 14 | protected reduceContext(context: ScopeContext): ScopeContext { 15 | const openTagIndex = context.content.lastIndexOf("", context.offset); 21 | if (closeTagIndex < 0) { 22 | return null; 23 | } 24 | 25 | const tagContentIndex = context.content.indexOf(">", openTagIndex); 26 | if (tagContentIndex < 0) { 27 | return null; 28 | } 29 | 30 | const startPositionIndex = tagContentIndex + 1; 31 | return { 32 | documentOffset: context.documentOffset, 33 | content: context.content.substring(startPositionIndex, closeTagIndex), 34 | offset: context.offset - startPositionIndex 35 | }; 36 | } 37 | } -------------------------------------------------------------------------------- /server/src/services/style/svelte2/RefsStyleService.ts: -------------------------------------------------------------------------------- 1 | import { BaseService } from "../../Common"; 2 | import { SvelteDocument, SVELTE_VERSION_2 } from "../../../SvelteDocument"; 3 | import { CompletionItem } from "vscode-languageserver"; 4 | import { cloneCompletionItem } from "../../Utils"; 5 | import { svelte2DefaultRefCompletionItem } from "../../../svelte2Language"; 6 | 7 | export class RefsStyleService extends BaseService { 8 | public getSupportedSvelteVersions() { 9 | return [SVELTE_VERSION_2]; 10 | } 11 | 12 | public getCompletitionItems(document: SvelteDocument): Array { 13 | let result = [ 14 | svelte2DefaultRefCompletionItem 15 | ]; 16 | 17 | if (document.metadata) { 18 | result.push( 19 | ...document.metadata.refs 20 | .map(cloneCompletionItem) 21 | .map(item => { 22 | item.filterText = `ref:${item.label}`; 23 | item.sortText = `ref:${item.label}`; 24 | item.insertText = `ref:${item.label}`; 25 | item.detail = `[Svelte] ref:${item.label}`; 26 | item.commitCharacters = [' ']; 27 | return item; 28 | }) 29 | ); 30 | } 31 | 32 | return result; 33 | } 34 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "Launch Client", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": [ "--extensionDevelopmentPath=${workspaceFolder}" ], 11 | "outFiles": [ "${workspaceFolder}/client/out/**/*.js" ] 12 | }, 13 | { 14 | "type": "node", 15 | "request": "attach", 16 | "name": "Attach to Server", 17 | "port": 6010, 18 | "restart": true, 19 | "outFiles": ["${workspaceRoot}/server/out/**/*.js"] 20 | }, 21 | { 22 | "name": "Extension Tests", 23 | "type": "extensionHost", 24 | "request": "launch", 25 | "runtimeExecutable": "${execPath}", 26 | "args": [ 27 | "--extensionDevelopmentPath=${workspaceFolder}", 28 | "--extensionTestsPath=${workspaceFolder}/client/out/test" 29 | ], 30 | "outFiles": [ 31 | "${workspaceFolder}/client/out/test/**/*.js" 32 | ] 33 | } 34 | ], 35 | "compounds": [ 36 | { 37 | "name": "Client + Server", 38 | "configurations": ["Launch Client", "Attach to Server"] 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /server/src/services/markup/block/BlockCloseService.ts: -------------------------------------------------------------------------------- 1 | import { BaseService } from "../../Common"; 2 | import { SvelteDocument } from "../../../SvelteDocument"; 3 | import { CompletionItem, CompletionItemKind } from "vscode-languageserver"; 4 | import { findNearestNotClosedBlock, findLastCloseBlockIndex } from "./BlockHelpers"; 5 | import { ScopeContext } from "../../../interfaces"; 6 | 7 | export class BlockCloseService extends BaseService { 8 | public getCompletitionItems(document: SvelteDocument, context: ScopeContext): Array { 9 | const notClosedBlock = findNearestNotClosedBlock(context.content, context.offset); 10 | if (notClosedBlock == null) { 11 | return null; 12 | } 13 | 14 | const startIndex = findLastCloseBlockIndex(context.content, context.offset); 15 | if (startIndex < 0) { 16 | return null; 17 | } 18 | 19 | const contentPart = document.content.substring(startIndex, context.offset); 20 | if (/{\/[\w\d_]*$/g.test(contentPart)) { 21 | return [ 22 | { 23 | label: notClosedBlock.blockName, 24 | kind: CompletionItemKind.Keyword, 25 | preselect: true, 26 | commitCharacters: ['}'] 27 | } 28 | ]; 29 | } 30 | 31 | return null; 32 | } 33 | } -------------------------------------------------------------------------------- /server/src/services/markup/html/HtmlTagInnerService.ts: -------------------------------------------------------------------------------- 1 | import { ChoosingService } from "../../ChoosingService"; 2 | import { HtmlTagBindService } from "./HtmlTagBindService"; 3 | import { HtmlTagDefaultService } from "./HtmlTagDefaultService"; 4 | import { HtmlTagActionService } from "./HtmlTagActionService"; 5 | import { HtmlTagTransionService } from "./HtmlTagTransitionService"; 6 | import { HtmlTagTransionOutService } from "./HtmlTagTransitionOutService"; 7 | import { HtmlTagTransionInService } from "./HtmlTagTransitionInService"; 8 | import { ExpressionService } from "../ExpressionService"; 9 | import { BindTargetPropertyService } from "../BindTargetPropertyService"; 10 | import { HtmlTagAttributeAssignService } from "./HtmlTagAttributeAssignService"; 11 | import { HtmlTagEventService } from "./HtmlTagEventService"; 12 | 13 | export class HtmlTagInnerService extends ChoosingService { 14 | public constructor() { 15 | super([ 16 | new ExpressionService(), 17 | new HtmlTagBindService(), 18 | new HtmlTagActionService(), 19 | new HtmlTagTransionService(), 20 | new HtmlTagTransionInService(), 21 | new HtmlTagTransionOutService(), 22 | 23 | new BindTargetPropertyService(), 24 | new HtmlTagAttributeAssignService(), 25 | new HtmlTagEventService(), 26 | 27 | // Fallback service 28 | new HtmlTagDefaultService() 29 | ]); 30 | } 31 | } -------------------------------------------------------------------------------- /server/src/services/markup/TagInnerService.ts: -------------------------------------------------------------------------------- 1 | import { ChoosingService } from "../ChoosingService"; 2 | import { ScopeContext, GenericScopeContext } from "../../interfaces"; 3 | import { findLastOpenTag } from "./TagHelpers"; 4 | import { ComponentInnerService } from "./component/ComponentInnerService"; 5 | import { HtmlTagInnerService } from "./html/HtmlTagInnerService"; 6 | import { SlotService } from "./SlotService"; 7 | import { NamedSlotParamsService } from "./NamedSlotParamsService"; 8 | 9 | export interface TagData { 10 | name: string; 11 | namespace: string; 12 | } 13 | 14 | export interface TagScopeContext extends GenericScopeContext {} 15 | 16 | export class TagInnerService extends ChoosingService { 17 | public constructor() { 18 | super([ 19 | new SlotService(), 20 | new NamedSlotParamsService(), 21 | new ComponentInnerService(), 22 | new HtmlTagInnerService 23 | ]); 24 | } 25 | 26 | protected reduceContext(context: ScopeContext): TagScopeContext { 27 | const openTag = findLastOpenTag(context.content, context.offset); 28 | 29 | if (openTag === null) { 30 | return null; 31 | } 32 | 33 | return { 34 | documentOffset: context.documentOffset, 35 | content: openTag.content, 36 | offset: context.offset - openTag.startIndex, 37 | data: { 38 | name: openTag.tagName, 39 | namespace: openTag.tagNamespace 40 | } 41 | }; 42 | } 43 | } -------------------------------------------------------------------------------- /server/src/services/markup/html/HtmlTagBindService.ts: -------------------------------------------------------------------------------- 1 | import { SvelteDocument, SVELTE_VERSION_2, SVELTE_VERSION_3 } from "../../../SvelteDocument"; 2 | import { CompletionItem } from "vscode-languageserver"; 3 | import { BaseService } from "../../Common"; 4 | import { findLastDirectiveIndex } from "../TagHelpers"; 5 | import { getHtmlTagDefaultBindCompletionItems, getVersionSpecificSelection } from "../../../svelteLanguage"; 6 | import { svelte2DefaultHtmlTagBindCompletionItems } from "../../../svelte2Language"; 7 | import { svelte3DefaultHtmlTagBindCompletionItems } from "../../../svelte3Language"; 8 | import { TagScopeContext } from "../TagInnerService"; 9 | 10 | export class HtmlTagBindService extends BaseService { 11 | 12 | public getCompletitionItems(document: SvelteDocument, context: TagScopeContext): Array { 13 | const index = findLastDirectiveIndex(context.content, context.offset, 'bind'); 14 | if (index < 0) { 15 | return null; 16 | } 17 | 18 | const versionsSpecific = [ 19 | { version: SVELTE_VERSION_2, specific: svelte2DefaultHtmlTagBindCompletionItems }, 20 | { version: SVELTE_VERSION_3, specific: svelte3DefaultHtmlTagBindCompletionItems} 21 | ]; 22 | 23 | const contentPart = context.content.substring(index, context.offset); 24 | if (/bind:[\w\d_]*$/g.test(contentPart)) { 25 | return getHtmlTagDefaultBindCompletionItems(context.data.name, getVersionSpecificSelection(document, versionsSpecific)); 26 | } 27 | 28 | return null; 29 | } 30 | } -------------------------------------------------------------------------------- /server/src/importResolvers/AliasImportResolver.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as utils from '../utils'; 3 | import { NodeModulesImportResolver } from "./NodeModulesImportResolver"; 4 | import { DocumentsCache } from "../DocumentsCache"; 5 | import { SvelteDocument } from "../SvelteDocument"; 6 | 7 | export class AliasImportResolver extends NodeModulesImportResolver { 8 | constructor(documentsCache: DocumentsCache, documentPath: string) { 9 | super(documentsCache, documentPath); 10 | } 11 | 12 | protected findFileWithAlias(_partialPath: string) : string { 13 | return null; 14 | } 15 | 16 | public resolve(importee: string) : SvelteDocument { 17 | const result = super.resolve(importee); 18 | 19 | if (result !== null) { 20 | return result; 21 | } 22 | 23 | let importFilePath = this.findFileWithAlias(importee); 24 | 25 | importFilePath = utils.findSvelteFile(importFilePath); 26 | if (importFilePath !== null) { 27 | return this.documentsCache.getOrCreateDocumentFromCache(importFilePath); 28 | } 29 | 30 | return null; 31 | } 32 | 33 | public resolvePath(partialPath: string): string { 34 | const result = super.resolvePath(partialPath); 35 | 36 | if (result !== null) { 37 | return result; 38 | } 39 | 40 | const importFilePath = this.findFileWithAlias(partialPath); 41 | 42 | if (fs.existsSync(importFilePath)) { 43 | return importFilePath; 44 | } 45 | 46 | return null; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /server/src/services/script/ComponentNameService.ts: -------------------------------------------------------------------------------- 1 | import { BaseService } from "../Common"; 2 | import { ScopeContext, WorkspaceContext } from "../../interfaces"; 3 | import { Hover, Definition } from "vscode-languageserver"; 4 | import { SvelteDocument } from "../../SvelteDocument"; 5 | 6 | import { getImportedComponentDocumentation, getImportedComponentDefinition } from "../Utils"; 7 | 8 | export class ComponentNameService extends BaseService { 9 | public getHover(document: SvelteDocument, context: ScopeContext, workspace: WorkspaceContext): Hover { 10 | return getImportedComponentDocumentation(this.getComponentName(context), document, workspace); 11 | } 12 | 13 | public getDefinitions(document: SvelteDocument, context: ScopeContext, workspace: WorkspaceContext): Definition[] { 14 | return getImportedComponentDefinition(this.getComponentName(context), document, workspace); 15 | } 16 | 17 | private getComponentName(context: ScopeContext) { 18 | const prevContent = context.content.substring(0, context.offset); 19 | const nextContent = context.content.substring(context.offset); 20 | 21 | const componentNameStartSearchResult = /\b([\w\d_]+)$/g.exec(prevContent); 22 | const componentNameEndSearchResult = /^([\w\d_]+)\s*[:, }]/g.exec(nextContent); 23 | 24 | if (componentNameStartSearchResult !== null && componentNameEndSearchResult !== null) { 25 | const componentName = componentNameStartSearchResult[1] + componentNameEndSearchResult[1]; 26 | return componentName; 27 | } 28 | 29 | return null; 30 | } 31 | } -------------------------------------------------------------------------------- /server/src/SvelteItemsHelpers.ts: -------------------------------------------------------------------------------- 1 | import { MarkupKind, Hover, Definition } from "vscode-languageserver"; 2 | import { SvelteDocument } from "./SvelteDocument"; 3 | import { ISvelteItem } from "sveltedoc-parser/typings"; 4 | 5 | export interface IItemsWithHandlers { 6 | items: Array; 7 | handler(item: ISvelteItem): string; 8 | } 9 | 10 | export function findItemInSvelteDoc(itemsWithHandlers: Array, name: string) : Hover { 11 | if (!name) { 12 | return null; 13 | } 14 | 15 | for (let index = 0; index < itemsWithHandlers.length; index++) { 16 | const itemTypeAndHandler = itemsWithHandlers[index]; 17 | let foundItem = itemTypeAndHandler.items.find(item => item.name === name); 18 | if (foundItem) { 19 | return { 20 | contents: { kind: MarkupKind.Markdown, value: itemTypeAndHandler.handler(foundItem)} 21 | }; 22 | } 23 | } 24 | 25 | return null; 26 | } 27 | 28 | export function findLocationForItemInSvelteDoc(document: SvelteDocument, items: Array, name: string) : Definition[] { 29 | if (!name) { 30 | return null; 31 | } 32 | 33 | let item = items.find(item => item.name === name); 34 | if (item && item.locations && item.locations.length > 0) { 35 | return item.locations.map((loc) => { 36 | return { 37 | uri: document.document.uri, 38 | range: { 39 | start: document.positionAt(loc.start), 40 | end: document.positionAt(loc.end) 41 | } 42 | }; 43 | }); 44 | } 45 | 46 | return null; 47 | } -------------------------------------------------------------------------------- /server/src/importResolvers/WebpackImportResolver.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import { DocumentsCache } from "../DocumentsCache"; 3 | import { AliasImportResolver } from "./AliasImportResolver"; 4 | 5 | export class WebpackImportResolver extends AliasImportResolver { 6 | constructor(documentsCache: DocumentsCache, documentPath: string, private alias: Object) { 7 | super(documentsCache, documentPath); 8 | } 9 | 10 | private isAlias(file: string, alias: string) { 11 | const trueAlias = alias.endsWith('$') ? alias.substring(0, alias.length - 1) : alias; 12 | 13 | if (trueAlias === file) { 14 | return true; 15 | } 16 | if (!file.startsWith(trueAlias)) { 17 | return false; 18 | } 19 | return file[trueAlias.length] === '/'; 20 | } 21 | 22 | private getAlias(file: string, aliases: Object) { 23 | for (const p in aliases) { 24 | if (aliases.hasOwnProperty(p) && this.isAlias(file, p)) { 25 | return p; 26 | } 27 | } 28 | return null; 29 | } 30 | 31 | protected findFileWithAlias(partialPath: string) : string { 32 | let importFilePath = null; 33 | 34 | let alias = this.getAlias(partialPath, this.alias); 35 | if (alias === null) { 36 | return null; 37 | } 38 | importFilePath = partialPath.substr(alias.length - (alias.endsWith('$') ? 1 : 0)); 39 | if (importFilePath !== '') { 40 | importFilePath = '.' + importFilePath; 41 | } 42 | importFilePath = path.resolve(this.alias[alias], importFilePath); 43 | 44 | return importFilePath; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "compile", 6 | "dependsOn": [ 7 | { 8 | "type": "npm", 9 | "script": "compile:client" 10 | }, 11 | { 12 | "type": "npm", 13 | "script": "compile:server" 14 | } 15 | ], 16 | "problemMatcher": [] 17 | }, 18 | { 19 | "type": "npm", 20 | "script": "compile:client", 21 | "group": "build", 22 | "presentation": { 23 | "panel": "dedicated", 24 | "reveal": "never" 25 | }, 26 | "problemMatcher": [ 27 | "$tsc" 28 | ] 29 | }, 30 | { 31 | "type": "npm", 32 | "script": "compile:server", 33 | "group": "build", 34 | "presentation": { 35 | "panel": "dedicated", 36 | "reveal": "never" 37 | }, 38 | "problemMatcher": [ 39 | "$tsc" 40 | ] 41 | }, 42 | { 43 | "label": "watch", 44 | "dependsOn": [ 45 | { 46 | "type": "npm", 47 | "script": "watch:client" 48 | }, 49 | { 50 | "type": "npm", 51 | "script": "watch:server" 52 | } 53 | ], 54 | "group": { 55 | "kind": "build", 56 | "isDefault": true 57 | }, 58 | "problemMatcher": [] 59 | }, 60 | { 61 | "type": "npm", 62 | "script": "watch:client", 63 | "isBackground": true, 64 | "group": "build", 65 | "presentation": { 66 | "panel": "dedicated", 67 | "reveal": "never" 68 | }, 69 | "problemMatcher": [ 70 | "$tsc-watch" 71 | ] 72 | }, 73 | { 74 | "type": "npm", 75 | "script": "watch:server", 76 | "isBackground": true, 77 | "group": "build", 78 | "presentation": { 79 | "panel": "dedicated", 80 | "reveal": "never" 81 | }, 82 | "problemMatcher": [ 83 | "$tsc-watch" 84 | ] 85 | } 86 | ] 87 | } -------------------------------------------------------------------------------- /server/src/services/script/svelte2/ComputedSectionService.ts: -------------------------------------------------------------------------------- 1 | import { ChoosingService } from "../../ChoosingService"; 2 | import { ScopeContext } from "../../../interfaces"; 3 | import { ComputedDependencyService } from "./ComputedDependencyService"; 4 | import { SVELTE_VERSION_2 } from "../../../SvelteDocument"; 5 | 6 | export class ComputedSectionService extends ChoosingService { 7 | public constructor() { 8 | super([ 9 | new ComputedDependencyService() 10 | ]); 11 | } 12 | 13 | public getSupportedSvelteVersions() { 14 | return [SVELTE_VERSION_2]; 15 | } 16 | 17 | protected reduceContext(context: ScopeContext): ScopeContext { 18 | const openBlockIndex = context.content.lastIndexOf('computed', context.offset); 19 | 20 | if (openBlockIndex < 0) { 21 | return null; 22 | } 23 | 24 | const closeBlockIndex = context.content.indexOf('}', openBlockIndex); 25 | 26 | if (closeBlockIndex > 0 && closeBlockIndex < context.offset) { 27 | return null; 28 | } 29 | 30 | const innerContent = closeBlockIndex < 0 31 | ? context.content.substring(openBlockIndex) 32 | : context.content.substring(openBlockIndex, closeBlockIndex); 33 | 34 | const match = /computed\s*:\s*\{/gi.exec(innerContent); 35 | if (match) { 36 | const matchOffset = match.index + match[0].length; 37 | return { 38 | documentOffset: context.documentOffset, 39 | content: innerContent.substring(matchOffset), 40 | offset: context.offset - openBlockIndex - matchOffset, 41 | data: context.data 42 | }; 43 | } 44 | 45 | return null; 46 | } 47 | } -------------------------------------------------------------------------------- /server/src/services/script/svelte2/ComponentSetDataService.ts: -------------------------------------------------------------------------------- 1 | import { BaseService } from "../../Common"; 2 | import { SvelteDocument, SVELTE_VERSION_2 } from "../../../SvelteDocument"; 3 | import { CompletionItem } from "vscode-languageserver"; 4 | import { ScopeContext } from "../../../interfaces"; 5 | 6 | export class ComponentSetDataService extends BaseService { 7 | public getSupportedSvelteVersions() { 8 | return [SVELTE_VERSION_2]; 9 | } 10 | 11 | public getCompletitionItems(document: SvelteDocument, context: ScopeContext): Array { 12 | const indexOfLastOpenBrace = this.findNearestOpenBraceIndex(context.content, context.offset); 13 | if (indexOfLastOpenBrace < 0) { 14 | return null; 15 | } 16 | 17 | if (/\s*this\s*\.\s*set\s*\(\s*$/g.test(context.content.substring(0, indexOfLastOpenBrace))) { 18 | return document.metadata 19 | ? document.metadata.data 20 | : []; 21 | } 22 | 23 | return null; 24 | } 25 | 26 | private findNearestOpenBraceIndex(content: string, offset: number) { 27 | let currentPosition = offset - 1; 28 | let countOfUnclosedBraces = 0; 29 | 30 | while (currentPosition >= 0) { 31 | const currentChar = content.charAt(currentPosition); 32 | if (currentChar === '}') { 33 | countOfUnclosedBraces++; 34 | } else if (currentChar === '{') { 35 | if (countOfUnclosedBraces === 0) { 36 | return currentPosition; 37 | } else { 38 | countOfUnclosedBraces--; 39 | } 40 | } 41 | currentPosition--; 42 | } 43 | 44 | return -1; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /server/src/services/script/svelte2/ComponentsSectionService.ts: -------------------------------------------------------------------------------- 1 | import { ChoosingService } from "../../ChoosingService"; 2 | import { ScopeContext } from "../../../interfaces"; 3 | import { ComponentPathService } from "../ComponentPathService"; 4 | import { ComponentNameService } from "../ComponentNameService"; 5 | import { SVELTE_VERSION_2 } from "../../../SvelteDocument"; 6 | 7 | export class ComponentsSectionService extends ChoosingService { 8 | public constructor() { 9 | super([ 10 | new ComponentNameService(), 11 | new ComponentPathService() 12 | ]); 13 | } 14 | 15 | public getSupportedSvelteVersions() { 16 | return [SVELTE_VERSION_2]; 17 | } 18 | 19 | protected reduceContext(context: ScopeContext): ScopeContext { 20 | const openBlockIndex = context.content.lastIndexOf('components', context.offset); 21 | 22 | if (openBlockIndex < 0) { 23 | return null; 24 | } 25 | 26 | const closeBlockIndex = context.content.indexOf('}', openBlockIndex); 27 | 28 | if (closeBlockIndex > 0 && closeBlockIndex < context.offset) { 29 | return null; 30 | } 31 | 32 | const innerContent = closeBlockIndex < 0 33 | ? context.content.substring(openBlockIndex) 34 | : context.content.substring(openBlockIndex, closeBlockIndex); 35 | 36 | const match = /components\s*:\s*\{/gi.exec(innerContent); 37 | if (match) { 38 | const matchOffset = match.index + match[0].length; 39 | return { 40 | documentOffset: context.documentOffset, 41 | content: innerContent.substring(matchOffset), 42 | offset: context.offset - openBlockIndex - matchOffset, 43 | data: context.data 44 | }; 45 | } 46 | 47 | return null; 48 | } 49 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-intellisense", 3 | "displayName": "Svelte Intellisense", 4 | "description": "Provides intellisense for data, events, slots etc. in components.", 5 | "icon": "images/icon.png", 6 | "repository": { 7 | "url": "https://github.com/ArdenIvanov/svelte-intellisense.git" 8 | }, 9 | "version": "0.7.1", 10 | "publisher": "ardenivanov", 11 | "keywords": [ 12 | "svelte", 13 | "vscode" 14 | ], 15 | "author": "Aleksandr Ivanov", 16 | "license": "MIT", 17 | "engines": { 18 | "vscode": "^1.30.0" 19 | }, 20 | "categories": [ 21 | "Programming Languages", 22 | "Formatters" 23 | ], 24 | "activationEvents": [ 25 | "onLanguage:svelte" 26 | ], 27 | "main": "./client/out/extension", 28 | "contributes": { 29 | "configuration": { 30 | "type": "object", 31 | "title": "Svelte Intellisense configuration", 32 | "properties": { 33 | "svelte-intellisense.trace.server": { 34 | "scope": "window", 35 | "type": "string", 36 | "enum": [ 37 | "off", 38 | "messages", 39 | "verbose" 40 | ], 41 | "default": "off", 42 | "description": "Traces the communication between VS Code and the language server." 43 | } 44 | } 45 | } 46 | }, 47 | "scripts": { 48 | "vscode:prepublish": "cd client && yarn update-vscode && cd .. && yarn compile", 49 | "compile:client": "tsc -p ./client/tsconfig.json", 50 | "compile:server": "tsc -p ./server/tsconfig.json", 51 | "watch:client": "tsc -w -p ./client/tsconfig.json", 52 | "watch:server": "tsc -w -p ./server/tsconfig.json", 53 | "compile": "yarn compile:client && yarn compile:server", 54 | "postinstall": "cd client && yarn install && cd ../server && yarn install && cd .." 55 | }, 56 | "devDependencies": { 57 | "@types/mocha": "^2.2.42", 58 | "@types/node": "^8.10.25", 59 | "tslint": "^5.8.0", 60 | "typescript": "^2.6.1", 61 | "vscode": "^1.1.36" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /server/src/interfaces.d.ts: -------------------------------------------------------------------------------- 1 | import { CompletionItem, CompletionItemKind, Position } from 'vscode-languageserver'; 2 | import { DocumentsCache } from './DocumentsCache'; 3 | import { SvelteDocument } from './SvelteDocument'; 4 | 5 | export interface ConfigurationItem { 6 | completionItemKind: CompletionItemKind; 7 | metadataName: string; 8 | hasPublic: boolean; 9 | } 10 | 11 | export interface ImportedComponent { 12 | name: string; 13 | filePath: string; 14 | } 15 | 16 | export interface SlotMetadata { 17 | name: string; 18 | parameters: CompletionItem[]; 19 | } 20 | 21 | export interface ComponentMetadata { 22 | data: CompletionItem[]; 23 | public_data: CompletionItem[]; 24 | 25 | events: CompletionItem[]; 26 | public_events: CompletionItem[]; 27 | 28 | methods: CompletionItem[]; 29 | public_methods: CompletionItem[]; 30 | 31 | slots: CompletionItem[]; 32 | public_slots: CompletionItem[]; 33 | 34 | transitions: CompletionItem[]; 35 | 36 | actions: CompletionItem[]; 37 | refs: CompletionItem[]; 38 | computed: CompletionItem[]; 39 | helpers: CompletionItem[]; 40 | components: CompletionItem[]; 41 | 42 | slotsMetadata: SlotMetadata[]; 43 | } 44 | 45 | export interface DocumentPosition extends Position { 46 | /** 47 | * Index of cursor offset in document. 48 | * Handful to use with `String.substr` like methods. 49 | */ 50 | offset: number; 51 | } 52 | 53 | export interface GenericScopeContext { 54 | content: string; 55 | offset: number; 56 | data?: TData; 57 | 58 | documentOffset: number; 59 | } 60 | 61 | export interface ScopeContext extends GenericScopeContext { 62 | 63 | } 64 | 65 | export interface WorkspaceContext { 66 | documentsCache: DocumentsCache; 67 | } 68 | 69 | export interface ImportResolver { 70 | resolve(importee: string): SvelteDocument; 71 | resolvePath(partialPath: string): string; 72 | } -------------------------------------------------------------------------------- /server/src/services/script/ScriptService.ts: -------------------------------------------------------------------------------- 1 | import { ChoosingService } from "../ChoosingService"; 2 | import { ScopeContext } from "../../interfaces"; 3 | import { ComponentsSectionService } from "./svelte2/ComponentsSectionService"; 4 | import { ImportStatementService } from "./ImportStatementService"; 5 | import { ComponentPrivateService } from "./svelte2/ComponentPrivateService"; 6 | import { ComponentGetDataService } from "./svelte2/ComponentGetDataService"; 7 | import { ComponentSetDataService } from "./svelte2/ComponentSetDataService"; 8 | import { ComputedSectionService } from "./svelte2/ComputedSectionService"; 9 | 10 | export class ScriptService extends ChoosingService { 11 | public constructor() { 12 | super([ 13 | new ImportStatementService(), 14 | new ComponentsSectionService(), 15 | new ComputedSectionService(), 16 | new ComponentPrivateService(), 17 | new ComponentGetDataService(), 18 | new ComponentSetDataService() 19 | ], { 20 | exclusive: true 21 | }); 22 | } 23 | 24 | protected reduceContext(context: ScopeContext): ScopeContext { 25 | const openTagIndex = context.content.lastIndexOf("", context.offset); 31 | if (closeTagIndex < 0) { 32 | return null; 33 | } 34 | 35 | const tagContentIndex = context.content.indexOf(">", openTagIndex); 36 | if (tagContentIndex < 0) { 37 | return null; 38 | } 39 | 40 | const startPositionIndex = tagContentIndex + 1; 41 | return { 42 | documentOffset: context.documentOffset, 43 | content: context.content.substring(startPositionIndex, closeTagIndex), 44 | offset: context.offset - startPositionIndex 45 | }; 46 | } 47 | } -------------------------------------------------------------------------------- /server/src/services/markup/block/BlockInnerService.ts: -------------------------------------------------------------------------------- 1 | import { BaseService } from "../../Common"; 2 | import { SvelteDocument, SVELTE_VERSION_2, SVELTE_VERSION_3 } from "../../../SvelteDocument"; 3 | import { CompletionItem } from "vscode-languageserver"; 4 | import { markupBlockInnerCompletitionItems, getVersionSpecificSelection } from "../../../svelteLanguage"; 5 | import { svelte2MarkupBlockInnerCompletitionItems } from "../../../svelte2Language"; 6 | import { svelte3MarkupBlockInnerCompletitionItems } from "../../../svelte3Language"; 7 | import { findNearestNotClosedBlock, findLastInnerBlockIndex } from "./BlockHelpers"; 8 | import { ScopeContext } from "../../../interfaces"; 9 | 10 | export class BlockInnerService extends BaseService { 11 | 12 | public getCompletitionItems(document: SvelteDocument, context: ScopeContext): Array { 13 | const versionsSpecific = [ 14 | { version: SVELTE_VERSION_2, specific: svelte2MarkupBlockInnerCompletitionItems }, 15 | { version: SVELTE_VERSION_3, specific: svelte3MarkupBlockInnerCompletitionItems} 16 | ]; 17 | 18 | var aggregatedItems = Object.assign({}, markupBlockInnerCompletitionItems, getVersionSpecificSelection(document, versionsSpecific)); 19 | const nearestBlock = findNearestNotClosedBlock(context.content, context.offset); 20 | if (nearestBlock == null) { 21 | return null; 22 | } 23 | 24 | const openIndex = findLastInnerBlockIndex(context.content, context.offset); 25 | if (openIndex < 0) { 26 | return null; 27 | } 28 | 29 | if (!aggregatedItems.hasOwnProperty(nearestBlock.blockName)) { 30 | return null; 31 | }; 32 | 33 | const contentPart = document.content.substring(openIndex, context.offset); 34 | if (/{:[\w\d_]*$/g.test(contentPart)) { 35 | return aggregatedItems[nearestBlock.blockName]; 36 | } 37 | 38 | return null; 39 | } 40 | } -------------------------------------------------------------------------------- /server/src/services/script/ImportStatementService.ts: -------------------------------------------------------------------------------- 1 | import { ChoosingService } from "../ChoosingService"; 2 | import { ComponentPathService, SupportedComponentFileExtensions } from "./ComponentPathService"; 3 | import { ScopeContext } from "../../interfaces"; 4 | import { ComponentNameService } from "./ComponentNameService"; 5 | 6 | const SupportedImportFileExtensions = [ 7 | '.js', 8 | '.ts' 9 | ]; 10 | 11 | const ExcludedFileExtensions = [ 12 | '.spec.js' 13 | ]; 14 | 15 | export class ImportStatementService extends ChoosingService { 16 | constructor() { 17 | super([ 18 | new ComponentNameService(), 19 | new ComponentPathService({ 20 | extensionsToSearch: [ 21 | ...SupportedComponentFileExtensions, 22 | ...SupportedImportFileExtensions 23 | ], 24 | extensionsToExclude: ExcludedFileExtensions, 25 | includeFileExtensionToInsert: false 26 | }) 27 | ]); 28 | } 29 | 30 | protected reduceContext(context: ScopeContext): ScopeContext { 31 | const startIndex = context.content.lastIndexOf('import ', context.offset); 32 | 33 | if (startIndex < 0) { 34 | return null; 35 | } 36 | 37 | const endIndex = context.content.indexOf(';', startIndex); 38 | 39 | const importStatementContent = endIndex < 0 40 | ? context.content.substring(startIndex) 41 | : context.content.substring(startIndex, endIndex); 42 | 43 | const _importStatementRegex = /^import\s+(({[^}]*}|[\w_][\w\d_]*|\*)\s+(as\s+[\w_][\w\d_]*\s+)?from\s+)/i; 44 | const match = _importStatementRegex.exec(importStatementContent); 45 | 46 | if (match) { 47 | return { 48 | content: importStatementContent, 49 | offset: context.offset - startIndex, 50 | documentOffset: context.documentOffset 51 | }; 52 | } 53 | 54 | return null; 55 | } 56 | } -------------------------------------------------------------------------------- /server/src/services/markup/component/ComponentDataService.ts: -------------------------------------------------------------------------------- 1 | import { BaseService } from "../../Common"; 2 | import { SvelteDocument } from "../../../SvelteDocument"; 3 | import { ComponentScopeContext } from "./ComponentInnerService"; 4 | import { CompletionItem, Hover, Definition } from "vscode-languageserver"; 5 | import { buildPropertyDocumentation } from "../../../svelteDocUtils"; 6 | import { regexIndexOf, regexLastIndexOf } from "../../../StringHelpers"; 7 | import { findItemInSvelteDoc, findLocationForItemInSvelteDoc } from "../../../SvelteItemsHelpers"; 8 | 9 | export class ComponentDataService extends BaseService { 10 | 11 | public getCompletitionItems(_document: SvelteDocument, context: ComponentScopeContext): Array { 12 | return context.data.component.metadata.public_data; 13 | } 14 | 15 | public getHover(_document: SvelteDocument, context: ComponentScopeContext): Hover { 16 | return findItemInSvelteDoc([ 17 | {items: context.data.component.sveltedoc.data, handler: buildPropertyDocumentation} 18 | ], this.getAttributeNameAtOffset(context)); 19 | } 20 | 21 | public getDefinitions(_document: SvelteDocument, context: ComponentScopeContext): Definition[] { 22 | return findLocationForItemInSvelteDoc( 23 | context.data.component, 24 | [ 25 | ...context.data.component.sveltedoc.data 26 | ], 27 | this.getAttributeNameAtOffset(context)); 28 | } 29 | 30 | private getAttributeNameAtOffset(context: ComponentScopeContext): string { 31 | const startIndex = regexLastIndexOf(context.content, /\s/, context.offset); 32 | const endIndex = regexIndexOf(context.content, /[\s=]/, context.offset); 33 | if (startIndex < 0 || endIndex < 0 || endIndex < startIndex) { 34 | return null; 35 | } 36 | 37 | const name = context.content.substring(startIndex, endIndex); 38 | const match = /^([\w\d_]+)$/.exec(name); 39 | 40 | if (match) { 41 | return match[1]; 42 | } 43 | 44 | return null; 45 | } 46 | } -------------------------------------------------------------------------------- /server/src/services/markup/BindTargetPropertyService.ts: -------------------------------------------------------------------------------- 1 | import { BaseService } from "../Common"; 2 | import { SvelteDocument } from "../../SvelteDocument"; 3 | import { ScopeContext } from "../../interfaces"; 4 | import { getIdentifierAtOffset } from "../../StringHelpers"; 5 | import { findLocationForItemInSvelteDoc, findItemInSvelteDoc } from "../../SvelteItemsHelpers"; 6 | import { buildPropertyDocumentation } from "../../svelteDocUtils"; 7 | 8 | export class BindTargetPropertyService extends BaseService { 9 | public getCompletitionItems(document: SvelteDocument, context: ScopeContext) { 10 | const contentPart = context.content.substring(0, context.offset); 11 | if (/\bbind:[\w\d_]*=[\'\"]?[\{]?[\w\d_]*$/g.test(contentPart)) { 12 | return document.metadata 13 | ? document.metadata.data 14 | : []; 15 | } 16 | 17 | return null; 18 | } 19 | 20 | public getHover(document: SvelteDocument, context: ScopeContext) { 21 | if (!this.isInsideBindTarget(context.content, context.offset)) { 22 | return null; 23 | } 24 | 25 | return findItemInSvelteDoc([ 26 | {items: document.sveltedoc.data, handler: buildPropertyDocumentation} 27 | ], getIdentifierAtOffset(context.content, context.offset)); 28 | } 29 | 30 | public getDefinitions(document: SvelteDocument, context: ScopeContext) 31 | { 32 | if (!this.isInsideBindTarget(context.content, context.offset)) { 33 | return null; 34 | } 35 | 36 | return findLocationForItemInSvelteDoc( 37 | document, 38 | [ 39 | ...document.sveltedoc.data 40 | ], 41 | getIdentifierAtOffset(context.content, context.offset)); 42 | } 43 | 44 | isInsideBindTarget(content: string, offset: number) { 45 | if (!/\bbind:[\w\d_]*=[\'\"]?[\w\d_$]*$/.test(content.substring(0, offset))) { 46 | return false; 47 | } 48 | if (!/^[\w\d_$]*/.test(content.substring(offset))) { 49 | return false; 50 | } 51 | return true; 52 | } 53 | } -------------------------------------------------------------------------------- /client/src/extension.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import * as path from 'path'; 3 | import { workspace, ExtensionContext } from 'vscode'; 4 | 5 | import { 6 | LanguageClient, 7 | LanguageClientOptions, 8 | ServerOptions, 9 | TransportKind 10 | } from 'vscode-languageclient'; 11 | 12 | let client: LanguageClient; 13 | 14 | export function activate(context: ExtensionContext) { 15 | // The server is implemented in node 16 | let serverModule = context.asAbsolutePath( 17 | path.join('server', 'out', 'server.js') 18 | ); 19 | // The debug options for the server 20 | // --inspect=6010: runs the server in Node's Inspector mode so VS Code can attach to the server for debugging 21 | let debugOptions = { execArgv: ['--nolazy', '--inspect=6010'] }; 22 | 23 | // If the extension is launched in debug mode then the debug server options are used 24 | // Otherwise the run options are used 25 | let serverOptions: ServerOptions = { 26 | run: { module: serverModule, transport: TransportKind.ipc }, 27 | debug: { 28 | module: serverModule, 29 | transport: TransportKind.ipc, 30 | options: debugOptions 31 | } 32 | }; 33 | 34 | const lsConfig = workspace.getConfiguration('svelte.language-server'); 35 | const serverRuntime = lsConfig.get('runtime'); 36 | if (serverRuntime) { 37 | serverOptions.run.runtime = serverRuntime; 38 | serverOptions.debug.runtime = serverRuntime; 39 | console.log('setting server runtime to', serverRuntime); 40 | } 41 | 42 | let clientOptions: LanguageClientOptions = { 43 | // Options to control the language client 44 | // Register the server for svelte files 45 | documentSelector: ['svelte'], 46 | synchronize: { 47 | // Notify the server about file changes to '.clientrc files contained in the workspace 48 | fileEvents: workspace.createFileSystemWatcher('**/.clientrc') 49 | } 50 | }; 51 | 52 | // Create the language client and start the client. 53 | client = new LanguageClient( 54 | 'languageServerSvelteIntellisense', 55 | 'Svelte Intellisense Language Server', 56 | serverOptions, 57 | clientOptions 58 | ); 59 | 60 | // Start the client. This will also launch the server 61 | client.start(); 62 | } 63 | 64 | // this method is called when your extension is deactivated 65 | export function deactivate() { 66 | if (!client) { 67 | return undefined; 68 | } 69 | return client.stop(); 70 | } -------------------------------------------------------------------------------- /server/src/StringHelpers.ts: -------------------------------------------------------------------------------- 1 | export function regexLastIndexOf(content: string, regex: RegExp, startPos?: number): number { 2 | let index = content.substring(0, startPos || content.length).search(regex); 3 | while (index >= 0) { 4 | let nextIndex = content.substring(index + 1, startPos || content.length).search(regex); 5 | if (nextIndex < 0) { 6 | return index + 1; 7 | } 8 | 9 | index += nextIndex + 1; 10 | } 11 | 12 | return -1; 13 | } 14 | 15 | export function regexIndexOf(content: string, regex: RegExp, startPos?: number): number { 16 | var indexOf = content.substring(startPos || 0).search(regex); 17 | return (indexOf >= 0) ? (indexOf + (startPos || 0)) : indexOf; 18 | } 19 | 20 | export function getIdentifierAtOffset(text: string, offset: number) { 21 | let result = ''; 22 | let position = offset; 23 | 24 | while (text.length > position) { 25 | const char = text[position]; 26 | if (char && /^[\w\d_$]$/.test(char)) { 27 | result += char; 28 | position++; 29 | } else { 30 | break; 31 | } 32 | } 33 | 34 | position = offset - 1; 35 | while (position >= 0) { 36 | const char = text[position]; 37 | if (char && /^[\w\d_$]$/.test(char)) { 38 | result = char + result; 39 | position--; 40 | } else { 41 | break; 42 | } 43 | } 44 | 45 | return result; 46 | } 47 | 48 | export function isInsideAttributeAssign(content: string, position: number) { 49 | const openMustacheIndex = content.lastIndexOf('{', position); 50 | if (openMustacheIndex >= 2) { 51 | let quotesUsed = ''; 52 | let prevChar = content.charAt(openMustacheIndex - 1); 53 | 54 | if (prevChar === '\'') { 55 | quotesUsed = '\''; 56 | prevChar = content.charAt(openMustacheIndex - 2); 57 | } else if (prevChar === '\"') { 58 | quotesUsed = '\"'; 59 | prevChar = content.charAt(openMustacheIndex - 2); 60 | } 61 | 62 | if (prevChar !== '=') { 63 | return false; 64 | } 65 | 66 | const closeMustacheIndex = content.indexOf('}' + quotesUsed, openMustacheIndex); 67 | return closeMustacheIndex > 0 && closeMustacheIndex >= position; 68 | } 69 | 70 | return false; 71 | } -------------------------------------------------------------------------------- /server/src/services/script/svelte2/ComponentPrivateService.ts: -------------------------------------------------------------------------------- 1 | import { BaseService } from "../../Common"; 2 | import { SvelteDocument, SVELTE_VERSION_2 } from "../../../SvelteDocument"; 3 | import { CompletionItem } from "vscode-languageserver"; 4 | import { ScopeContext } from "../../../interfaces"; 5 | import { svelte2DefaultComponentMethods, svelte2DefaultComponentGetMethodCompletionItem, svelte2DefaultScriptRefsCompletionItem } from "../../../svelte2Language"; 6 | import { cloneCompletionItem } from "../../Utils"; 7 | 8 | export class ComponentPrivateService extends BaseService { 9 | public getSupportedSvelteVersions() { 10 | return [SVELTE_VERSION_2]; 11 | } 12 | 13 | public getCompletitionItems(document: SvelteDocument, context: ScopeContext): Array { 14 | if (/\bthis(\s)*.(\s)*[\w\d_]*$/g.test(context.content.substring(0, context.offset))) { 15 | const result = [ 16 | ...svelte2DefaultComponentMethods, 17 | svelte2DefaultComponentGetMethodCompletionItem, 18 | svelte2DefaultScriptRefsCompletionItem, 19 | ]; 20 | 21 | if (document.metadata) { 22 | result.push(...[ 23 | ...document.metadata.methods, 24 | ...document.metadata.refs 25 | .map(cloneCompletionItem) 26 | .map(item => { 27 | item.detail = `[Svelte] Reference to element/component`; 28 | item.filterText = `refs.${item.label}`; 29 | item.sortText = `refs.${item.label}`; 30 | item.insertText = `refs.${item.label}`; 31 | 32 | return item; 33 | }) 34 | ]); 35 | } 36 | 37 | return result; 38 | } 39 | 40 | if (/\bthis(\s)*.(\s)*refs(\s)*.(\s)*[\w\d_]*$/g.test(context.content.substring(0, context.offset))) { 41 | return document.metadata ? [ 42 | ...document.metadata.refs 43 | .map(cloneCompletionItem) 44 | .map(item => { 45 | item.detail = `[Svelte] Reference to element/component`; 46 | return item; 47 | }) 48 | ] : []; 49 | } 50 | 51 | return null; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /server/src/services/markup/component/ComponentInnerService.ts: -------------------------------------------------------------------------------- 1 | import { SvelteDocument } from "../../../SvelteDocument"; 2 | import { WorkspaceContext, GenericScopeContext } from "../../../interfaces"; 3 | import { ComponentEventService } from "./ComponentEventService"; 4 | import { ComponentDataService } from "./ComponentDataService"; 5 | import { ComponentBindService } from "./ComponentBindService"; 6 | import { ComponentDefaultService } from "./ComponentDefaultService"; 7 | import { CompositeCompletionService } from "../../CompositeService"; 8 | import { ExpressionService } from "../ExpressionService"; 9 | import { ChoosingService } from "../../ChoosingService"; 10 | import { TagData, TagScopeContext } from "../TagInnerService"; 11 | import { findImportedComponent } from "../TagHelpers"; 12 | import { BindTargetPropertyService } from "../BindTargetPropertyService"; 13 | import { ComponentAttributeAssignService } from "./ComponentAttributeAssignService"; 14 | import { ComponentDefaultSlotParamsService } from "./ComponentDefaultSlotParamsService"; 15 | 16 | export interface ComponentTagData extends TagData { 17 | component: SvelteDocument; 18 | } 19 | 20 | export interface ComponentScopeContext extends GenericScopeContext {} 21 | 22 | export class ComponentInnerService extends ChoosingService { 23 | public constructor() { 24 | super([ 25 | new ExpressionService(), 26 | new ComponentEventService(), 27 | new ComponentBindService(), 28 | new ComponentDefaultSlotParamsService(), 29 | new BindTargetPropertyService(), 30 | new ComponentAttributeAssignService(), 31 | 32 | // Fallback 33 | new CompositeCompletionService([ 34 | new ComponentDataService(), 35 | new ComponentDefaultService() 36 | ]) 37 | ]); 38 | } 39 | 40 | protected reduceContext(context: TagScopeContext, document: SvelteDocument, workspace: WorkspaceContext): ComponentScopeContext { 41 | const component = findImportedComponent(context.data.name, document, workspace.documentsCache); 42 | if (component === null || !component.metadata) { 43 | return null; 44 | } 45 | 46 | return { 47 | documentOffset: context.documentOffset, 48 | content: context.content, 49 | offset: context.offset, 50 | data: Object.assign({}, context.data, { component: component }) 51 | }; 52 | } 53 | } -------------------------------------------------------------------------------- /server/src/services/markup/component/ComponentDefaultSlotParamsService.ts: -------------------------------------------------------------------------------- 1 | import { BaseService } from "../../Common"; 2 | import { SvelteDocument } from "../../../SvelteDocument"; 3 | import { CompletionItem, Hover, Definition } from "vscode-languageserver"; 4 | import { findLastDirectiveIndex, getAttributeLetNameAtOffset } from "../TagHelpers"; 5 | import { ComponentScopeContext } from "./ComponentInnerService"; 6 | import { buildSlotPerameterDocumentation } from "../../../svelteDocUtils"; 7 | import { findItemInSvelteDoc, findLocationForItemInSvelteDoc } from "../../../SvelteItemsHelpers"; 8 | 9 | export class ComponentDefaultSlotParamsService extends BaseService { 10 | public getCompletitionItems(_document: SvelteDocument, context: ComponentScopeContext): Array { 11 | if (!context.data.component.metadata.slotsMetadata) { 12 | return null; 13 | } 14 | 15 | const defaultSlotMetadata = context.data.component.metadata.slotsMetadata.find(s => s.name === 'default'); 16 | if (!defaultSlotMetadata) { 17 | return null; 18 | } 19 | 20 | const index = findLastDirectiveIndex(context.content, context.offset, 'let'); 21 | if (index < 0) { 22 | return null; 23 | } 24 | 25 | const contentPart = context.content.substring(index, context.offset); 26 | if (/let:[\w\d_]*$/g.test(contentPart)) { 27 | return defaultSlotMetadata.parameters; 28 | } 29 | 30 | return null; 31 | } 32 | 33 | public getHover(_document: SvelteDocument, context: ComponentScopeContext): Hover { 34 | const defaultSlotDoc = this.getDefaultSlotDocumentation(context); 35 | if (!defaultSlotDoc) { 36 | return null; 37 | } 38 | 39 | return findItemInSvelteDoc([ 40 | {items: defaultSlotDoc.parameters, handler: buildSlotPerameterDocumentation} 41 | ], getAttributeLetNameAtOffset(context)); 42 | } 43 | 44 | public getDefinitions(_document: SvelteDocument, context: ComponentScopeContext): Definition[] { 45 | const defaultSlotDoc = this.getDefaultSlotDocumentation(context); 46 | if (!defaultSlotDoc) { 47 | return null; 48 | } 49 | 50 | return findLocationForItemInSvelteDoc(context.data.component, defaultSlotDoc.parameters, getAttributeLetNameAtOffset(context)); 51 | } 52 | 53 | private getDefaultSlotDocumentation(context: ComponentScopeContext) { 54 | return context.data.component.sveltedoc.slots.find(s => s.name === 'default'); 55 | } 56 | } -------------------------------------------------------------------------------- /server/src/services/Utils.ts: -------------------------------------------------------------------------------- 1 | import { CompletionItem, MarkupKind, Location, Range } from "vscode-languageserver"; 2 | import { SvelteDocument } from "../SvelteDocument"; 3 | import { WorkspaceContext } from "../interfaces"; 4 | import { EmptyHoverContent } from "./Common"; 5 | import * as docUtils from "../svelteDocUtils"; 6 | import { pathToFileUri } from "../utils"; 7 | 8 | export function cloneCompletionItem(item: CompletionItem): CompletionItem { 9 | return { 10 | additionalTextEdits: item.additionalTextEdits, 11 | command: item.command, 12 | commitCharacters: item.commitCharacters, 13 | data: item.data, 14 | deprecated: item.deprecated, 15 | detail: item.detail, 16 | documentation: item.documentation, 17 | filterText: item.filterText, 18 | insertText: item.insertText, 19 | insertTextFormat: item.insertTextFormat, 20 | kind: item.kind, 21 | label: item.label, 22 | preselect: item.preselect, 23 | sortText: item.sortText, 24 | textEdit: item.textEdit 25 | }; 26 | } 27 | 28 | export function getImportedComponentDocumentation(componentName: string, document: SvelteDocument, workspace: WorkspaceContext) { 29 | if (componentName === null) { 30 | return null; 31 | } 32 | 33 | const componentDocument = findImportedComponent(componentName, document, workspace); 34 | 35 | if (componentDocument === null) { 36 | return EmptyHoverContent; 37 | } 38 | 39 | return { 40 | contents: { 41 | kind: MarkupKind.Markdown, 42 | value: docUtils.buildDocumentation(componentDocument.sveltedoc) 43 | } 44 | }; 45 | } 46 | 47 | export function getImportedComponentDefinition(componentName: string, document: SvelteDocument, workspace: WorkspaceContext) { 48 | const componentDocument = findImportedComponent(componentName, document, workspace); 49 | 50 | if (componentDocument === null) { 51 | return null; 52 | } 53 | 54 | return [Location.create(pathToFileUri(componentDocument.path), Range.create(0, 0, 0, 0))]; 55 | } 56 | 57 | function findImportedComponent(componentName: string, document: SvelteDocument, workspace: WorkspaceContext) { 58 | if (componentName === null) { 59 | return null; 60 | } 61 | 62 | const component = document.importedComponents.find(c => c.name === componentName); 63 | if (component === undefined || !workspace.documentsCache.has(component.filePath)) { 64 | return null; 65 | } 66 | return workspace.documentsCache.get(component.filePath); 67 | } -------------------------------------------------------------------------------- /server/src/importResolvers/NodeModulesImportResolver.ts: -------------------------------------------------------------------------------- 1 | import { ImportResolver } from "../interfaces"; 2 | import * as fs from "fs"; 3 | import * as path from "path"; 4 | import * as utils from '../utils'; 5 | import { DocumentsCache } from "../DocumentsCache"; 6 | import { SvelteDocument } from "../SvelteDocument"; 7 | 8 | export class NodeModulesImportResolver implements ImportResolver { 9 | private baseDocumentPath: string; 10 | private nodeModulesPath: string; 11 | 12 | constructor(protected documentsCache: DocumentsCache, protected documentPath: string) { 13 | this.baseDocumentPath = path.dirname(documentPath) 14 | this.nodeModulesPath = utils.findNodeModules(this.baseDocumentPath); 15 | } 16 | 17 | public resolve(importee: string): SvelteDocument { 18 | let importFilePath = path.resolve(this.baseDocumentPath, importee); 19 | let importedDocument = utils.findSvelteDocumentInCache(importFilePath, this.documentsCache); 20 | 21 | if (importedDocument === null) { 22 | importFilePath = utils.findSvelteFile(importFilePath); 23 | if (importFilePath !== null) { 24 | importedDocument = this.documentsCache.getOrCreateDocumentFromCache(importFilePath); 25 | } else if (this.nodeModulesPath) { 26 | const moduleFilePath = utils.findSvelteFile(path.resolve(this.nodeModulesPath, importee)); 27 | if (moduleFilePath !== null) { 28 | importedDocument = this.documentsCache.getOrCreateDocumentFromCache(moduleFilePath); 29 | } 30 | } 31 | } 32 | 33 | return importedDocument; 34 | } 35 | 36 | public resolvePath(partialPath: string): string { 37 | if (partialPath.startsWith('./') || partialPath.startsWith('../')) { 38 | const searchFolderPath = path.resolve(this.baseDocumentPath, partialPath.endsWith('/') ? partialPath : path.dirname(partialPath)); 39 | 40 | if (fs.existsSync(searchFolderPath)) { 41 | return searchFolderPath; 42 | } 43 | } else if (!partialPath.startsWith('.')) { 44 | // Search in node modules folder 45 | if (this.nodeModulesPath) { 46 | const searchFolderPath = path.resolve(this.nodeModulesPath, partialPath.endsWith('/') ? partialPath : path.dirname(partialPath)); 47 | 48 | if (fs.existsSync(searchFolderPath)) { 49 | return searchFolderPath; 50 | } 51 | } 52 | } 53 | 54 | return null; 55 | } 56 | } 57 | 58 | -------------------------------------------------------------------------------- /server/src/services/markup/component/ComponentBindService.ts: -------------------------------------------------------------------------------- 1 | import { BaseService } from "../../Common"; 2 | import { SvelteDocument } from "../../../SvelteDocument"; 3 | import { CompletionItem, Hover, Definition } from "vscode-languageserver"; 4 | import { findLastDirectiveIndex } from "../TagHelpers"; 5 | import { ComponentScopeContext } from "./ComponentInnerService"; 6 | import { regexLastIndexOf, regexIndexOf } from "../../../StringHelpers"; 7 | import { buildPropertyDocumentation } from "../../../svelteDocUtils"; 8 | import { findItemInSvelteDoc, findLocationForItemInSvelteDoc } from "../../../SvelteItemsHelpers"; 9 | 10 | export class ComponentBindService extends BaseService { 11 | 12 | public getCompletitionItems(_document: SvelteDocument, context: ComponentScopeContext): Array { 13 | const index = findLastDirectiveIndex(context.content, context.offset, 'bind'); 14 | if (index < 0) { 15 | return null; 16 | } 17 | 18 | const contentPart = context.content.substring(index, context.offset); 19 | if (/bind:[\w\d_]*$/g.test(contentPart)) { 20 | return context.data.component.metadata.public_data; 21 | } 22 | 23 | return null; 24 | } 25 | 26 | public getHover(_document: SvelteDocument, context: ComponentScopeContext): Hover { 27 | return findItemInSvelteDoc([ 28 | {items: context.data.component.sveltedoc.data, handler: buildPropertyDocumentation} 29 | ], this.getAttributeBindNameAtOffset(context)); 30 | } 31 | 32 | public getDefinitions(_document: SvelteDocument, context: ComponentScopeContext): Definition[] { 33 | return findLocationForItemInSvelteDoc( 34 | context.data.component, 35 | [ 36 | ...context.data.component.sveltedoc.data 37 | ], 38 | this.getAttributeBindNameAtOffset(context)); 39 | } 40 | 41 | private getAttributeBindNameAtOffset(context: ComponentScopeContext): string { 42 | const startIndex = regexLastIndexOf(context.content, /\sbind:/, context.offset); 43 | let endIndex = regexIndexOf(context.content, /[\s=]/, context.offset); 44 | if (endIndex < 0) { 45 | endIndex = context.content.length; 46 | } 47 | 48 | if (startIndex < 0 || endIndex < 0 || endIndex < startIndex) { 49 | return null; 50 | } 51 | 52 | const name = context.content.substring(startIndex, endIndex); 53 | const match = /^bind:([\w\d_]+)$/.exec(name); 54 | 55 | if (match) { 56 | return match[1]; 57 | } 58 | 59 | return null; 60 | } 61 | } -------------------------------------------------------------------------------- /server/src/services/markup/component/ComponentEventService.ts: -------------------------------------------------------------------------------- 1 | import { SvelteDocument } from "../../../SvelteDocument"; 2 | import { CompletionItem, Hover, Definition } from "vscode-languageserver"; 3 | import { findLastDirectiveIndex } from "../TagHelpers"; 4 | import { BaseService } from "../../Common"; 5 | import { ComponentScopeContext } from "./ComponentInnerService"; 6 | import { regexLastIndexOf, regexIndexOf } from "../../../StringHelpers"; 7 | import { buildPropertyDocumentation } from "../../../svelteDocUtils"; 8 | import { findItemInSvelteDoc, findLocationForItemInSvelteDoc } from "../../../SvelteItemsHelpers"; 9 | 10 | export class ComponentEventService extends BaseService { 11 | 12 | public getCompletitionItems(_document: SvelteDocument, context: ComponentScopeContext): Array { 13 | const index = findLastDirectiveIndex(context.content, context.offset, 'on'); 14 | if (index < 0) { 15 | return null; 16 | } 17 | 18 | const contentPart = context.content.substring(index, context.offset); 19 | if (/on:[\w\d_]*$/g.test(contentPart)) { 20 | return context.data.component.metadata.public_events; 21 | } 22 | 23 | return null; 24 | } 25 | 26 | public getHover(_document: SvelteDocument, context: ComponentScopeContext): Hover { 27 | if (!context.data.component.sveltedoc) { 28 | return null; 29 | } 30 | 31 | return findItemInSvelteDoc([ 32 | {items: context.data.component.sveltedoc.events, handler: buildPropertyDocumentation} 33 | ], this.getAttributeEventNameAtOffset(context)); 34 | } 35 | 36 | public getDefinitions(_document: SvelteDocument, context: ComponentScopeContext): Definition[] { 37 | return findLocationForItemInSvelteDoc( 38 | context.data.component, 39 | [ 40 | ...context.data.component.sveltedoc.events 41 | ], 42 | this.getAttributeEventNameAtOffset(context)); 43 | } 44 | 45 | private getAttributeEventNameAtOffset(context: ComponentScopeContext): string { 46 | const startIndex = regexLastIndexOf(context.content, /\son:/, context.offset); 47 | let endIndex = regexIndexOf(context.content, /[\s=]/, context.offset); 48 | if (endIndex < 0) { 49 | endIndex = context.content.length; 50 | } 51 | 52 | if (startIndex < 0 || endIndex < 0 || endIndex < startIndex) { 53 | return null; 54 | } 55 | 56 | const name = context.content.substring(startIndex, endIndex); 57 | const match = /^on:([\w\d_]+)$/.exec(name); 58 | 59 | if (match) { 60 | return match[1]; 61 | } 62 | 63 | return null; 64 | } 65 | } -------------------------------------------------------------------------------- /server/src/services/Common.ts: -------------------------------------------------------------------------------- 1 | import { CompletionItem, Hover, Definition } from 'vscode-languageserver'; 2 | import { SvelteDocument, SVELTE_VERSION_2, SVELTE_VERSION_3 } from '../SvelteDocument'; 3 | import { WorkspaceContext, ScopeContext } from '../interfaces'; 4 | 5 | /** 6 | * Provide methods to implementing a completions HFSM. 7 | */ 8 | export interface IService { 9 | /** 10 | * Returns all applyable completition items for required context. 11 | * @param document The svelte document. 12 | * @param context The current document position context. 13 | * @param workspace The workspace context data. 14 | * @returns {CompletionItem|null} Returns null, when scope context is not correct for this service compatibility. 15 | */ 16 | getCompletitionItems(document: SvelteDocument, context: ScopeContext, workspace: WorkspaceContext): Array; 17 | 18 | /** 19 | * Returns hover details for required context. 20 | * @param document The svelte document. 21 | * @param position The current cursor position in specified document. 22 | * @param context The workspace context data. 23 | * @returns {CompletionItem|null} Returns null, when scope context is not correct for this service compatibility. 24 | */ 25 | getHover(document: SvelteDocument, context: ScopeContext, workspace: WorkspaceContext): Hover; 26 | 27 | /** 28 | * Returns definitions for required context. 29 | * @param document The svelte document. 30 | * @param position The current cursor position in specified document. 31 | * @param context The workspace context data. 32 | * @returns {Definition[]|null} Returns null, when scope context is not correct for this service compatibility. 33 | */ 34 | getDefinitions(document: SvelteDocument, context: ScopeContext, workspace: WorkspaceContext): Definition[]; 35 | 36 | /** 37 | * Returns list of supported svelte versions. 38 | */ 39 | getSupportedSvelteVersions(): number[]; 40 | } 41 | 42 | export abstract class BaseService implements IService { 43 | public getCompletitionItems(_document: SvelteDocument, _context: ScopeContext, _workspace: WorkspaceContext): Array { 44 | return null; 45 | } 46 | 47 | public getHover(_document: SvelteDocument, _context: ScopeContext, _workspace: WorkspaceContext): Hover { 48 | return null; 49 | } 50 | 51 | public getDefinitions(_document: SvelteDocument, _context: ScopeContext, _workspace: WorkspaceContext): Definition[] { 52 | return null; 53 | } 54 | 55 | public getSupportedSvelteVersions(): number[] { 56 | return [SVELTE_VERSION_2, SVELTE_VERSION_3]; 57 | } 58 | } 59 | 60 | export const EmptyHoverContent: Hover = { 61 | contents: null 62 | }; -------------------------------------------------------------------------------- /server/src/services/markup/component/ComponentAttributeAssignService.ts: -------------------------------------------------------------------------------- 1 | import { BaseService } from "../../Common"; 2 | import { SvelteDocument } from "../../../SvelteDocument"; 3 | import { ComponentScopeContext } from "./ComponentInnerService"; 4 | import { CompletionItem, CompletionItemKind } from "vscode-languageserver"; 5 | import { JSDocType } from "sveltedoc-parser/typings"; 6 | import { getVersionSpecificMetadataForMarkup } from "../../../svelteLanguage"; 7 | 8 | export class ComponentAttributeAssignService extends BaseService { 9 | public getCompletitionItems(document: SvelteDocument, context: ComponentScopeContext) { 10 | const contentPart = context.content.substring(0, context.offset); 11 | 12 | const match = /\s+(([\w\d_:]+)=)?(['"]?\{[^}]*|'[^']*|"[^"]*)$/.exec(contentPart); 13 | 14 | if (match) { 15 | // When source name are provided we can use 16 | // any valid evaluatable expression with using helpers, data and computed properties 17 | if (match[1]) { 18 | const sourcePropertyName = match[2]; 19 | 20 | if (match[3].startsWith('"{') || match[3].startsWith('\'{') || match[3].startsWith('{')) { 21 | return document.metadata ? [ 22 | ...getVersionSpecificMetadataForMarkup(document), 23 | ...document.metadata.data, 24 | ...document.metadata.computed 25 | ] : []; 26 | } 27 | 28 | const property = context.data.component.sveltedoc.data.find(p => p.name === sourcePropertyName && p.visibility === 'public'); 29 | if (property.hasOwnProperty(sourcePropertyName)) { 30 | 31 | if (property.type.kind === 'union') { 32 | const types = property.type.type; 33 | 34 | return types 35 | .filter(t => t.kind === 'const') 36 | .map(t => { 37 | label: t.value, 38 | kind: CompletionItemKind.Constant, 39 | detail: property.description 40 | }); 41 | } 42 | } 43 | 44 | return []; 45 | } 46 | 47 | // When source property is not specified we can use only data or computed with same names 48 | const items = document.metadata ? [ 49 | ...document.metadata.data, 50 | ...document.metadata.computed 51 | ] : []; 52 | 53 | return items.filter(item => context.data.component.metadata.public_data.some(child_item => child_item.label === item.label)); 54 | } 55 | 56 | return null; 57 | } 58 | } -------------------------------------------------------------------------------- /server/src/services/markup/html/HtmlTagAttributeAssignService.ts: -------------------------------------------------------------------------------- 1 | import { BaseService } from "../../Common"; 2 | import { SvelteDocument } from "../../../SvelteDocument"; 3 | import { TagScopeContext } from "../TagInnerService"; 4 | import { findItemInSvelteDoc, findLocationForItemInSvelteDoc } from "../../../SvelteItemsHelpers"; 5 | import { getIdentifierAtOffset, isInsideAttributeAssign } from "../../../StringHelpers"; 6 | import { buildMethodDocumentation, buildComputedDocumentation, buildPropertyDocumentation } from "../../../svelteDocUtils"; 7 | import { getVersionSpecificMetadataForMarkup, getVersionSpecificDocForMarkup } from "../../../svelteLanguage"; 8 | 9 | export class HtmlTagAttributeAssignService extends BaseService { 10 | public getCompletitionItems(document: SvelteDocument, context: TagScopeContext) { 11 | const contentPart = context.content.substring(0, context.offset); 12 | 13 | const match = /\s+(([\w\d_:]+)=)?(['"]?\{[^}]*|'[^']*|"[^"]*)$/.exec(contentPart); 14 | if (match) { 15 | // When source name are provided we can use 16 | // any valid evaluatable expression with using helpers, data and computed properties 17 | if (match[1]) { 18 | const sourcePropertyName = match[3]; 19 | 20 | if (sourcePropertyName.startsWith('"{') || sourcePropertyName.startsWith('\'{') || sourcePropertyName.startsWith('{')) { 21 | return document.metadata ? [ 22 | ...getVersionSpecificMetadataForMarkup(document), 23 | ...document.metadata.computed, 24 | ...document.metadata.data 25 | ] : []; 26 | } 27 | } 28 | } 29 | 30 | return null; 31 | } 32 | 33 | public getHover(document: SvelteDocument, context: TagScopeContext) { 34 | if (!isInsideAttributeAssign(context.content, context.offset)) { 35 | return null; 36 | } 37 | 38 | return findItemInSvelteDoc([ 39 | {items: getVersionSpecificDocForMarkup(document), handler: buildMethodDocumentation}, 40 | {items: document.sveltedoc.computed, handler: buildComputedDocumentation}, 41 | {items: document.sveltedoc.data, handler: buildPropertyDocumentation} 42 | ], getIdentifierAtOffset(context.content, context.offset)); 43 | } 44 | 45 | public getDefinitions(document: SvelteDocument, context: TagScopeContext) 46 | { 47 | if (!isInsideAttributeAssign(context.content, context.offset)) { 48 | return null; 49 | } 50 | 51 | return findLocationForItemInSvelteDoc( 52 | document, 53 | [ 54 | ...getVersionSpecificDocForMarkup(document), 55 | ...document.sveltedoc.computed, 56 | ...document.sveltedoc.data 57 | ], 58 | getIdentifierAtOffset(context.content, context.offset)); 59 | } 60 | } -------------------------------------------------------------------------------- /server/src/services/markup/SlotService.ts: -------------------------------------------------------------------------------- 1 | import { BaseService } from "../Common"; 2 | import { SvelteDocument } from "../../SvelteDocument"; 3 | import { CompletionItem } from "vscode-languageserver"; 4 | import { TagScopeContext } from "./TagInnerService"; 5 | import { cloneCompletionItem } from "../Utils"; 6 | import { findNearestOpenComponent } from "./TagHelpers"; 7 | import { WorkspaceContext } from "../../interfaces"; 8 | import { findItemInSvelteDoc, findLocationForItemInSvelteDoc } from "../../SvelteItemsHelpers"; 9 | import { buildPropertyDocumentation } from "../../svelteDocUtils"; 10 | import { getIdentifierAtOffset } from "../../StringHelpers"; 11 | 12 | export class SlotService extends BaseService { 13 | public getCompletitionItems(document: SvelteDocument, context: TagScopeContext, workspace: WorkspaceContext): Array { 14 | if (/\sslot(\s)*=?(\s)*[\'|\"][\w\d_]*$/g.test(context.content.substring(0, context.offset))) { 15 | const component = findNearestOpenComponent(context.documentOffset - context.offset - 1, document, workspace.documentsCache); 16 | if (component === null) { 17 | return null; 18 | } 19 | 20 | return [ 21 | ...component.metadata.slots 22 | .map(cloneCompletionItem) 23 | .map(item => { 24 | item.detail = `[Svelte] Slot of ${component.sveltedoc.name}`; 25 | return item; 26 | }) 27 | ]; 28 | } 29 | 30 | return null; 31 | } 32 | 33 | public getHover(document: SvelteDocument, context: TagScopeContext, workspace: WorkspaceContext) { 34 | if (!this.isInsideSlot(context.content, context.offset)) { 35 | return null; 36 | } 37 | 38 | const component = findNearestOpenComponent(context.documentOffset - context.offset - 1, document, workspace.documentsCache); 39 | if (component === null) { 40 | return null; 41 | } 42 | 43 | return findItemInSvelteDoc([ 44 | {items: component.sveltedoc.slots, handler: buildPropertyDocumentation} 45 | ], getIdentifierAtOffset(context.content, context.offset)); 46 | } 47 | 48 | public getDefinitions(document: SvelteDocument, context: TagScopeContext, workspace: WorkspaceContext) 49 | { 50 | if (!this.isInsideSlot(context.content, context.offset)) { 51 | return null; 52 | } 53 | 54 | const component = findNearestOpenComponent(context.documentOffset - context.offset - 1, document, workspace.documentsCache); 55 | if (component === null) { 56 | return null; 57 | } 58 | 59 | return findLocationForItemInSvelteDoc( 60 | component, 61 | [ 62 | ...component.sveltedoc.slots 63 | ], 64 | getIdentifierAtOffset(context.content, context.offset)); 65 | } 66 | 67 | private isInsideSlot(content: string, offset: number) { 68 | if (!/\sslot(\s)*=?(\s)*[\'|\"][\w\d_$]*$/.test(content.substring(0, offset))) { 69 | return false; 70 | } 71 | if (!/^[\w\d_$]*/.test(content.substring(offset))) { 72 | return false; 73 | } 74 | return true; 75 | } 76 | } -------------------------------------------------------------------------------- /server/src/services/markup/block/BlockOpenService.ts: -------------------------------------------------------------------------------- 1 | import { BaseService } from "../../Common"; 2 | import { SvelteDocument, SVELTE_VERSION_2, SVELTE_VERSION_3 } from "../../../SvelteDocument"; 3 | import { CompletionItem, Definition } from "vscode-languageserver"; 4 | import { markupBlockCompletitionItems, getVersionSpecificSelection, getVersionSpecificMetadataForMarkup, getVersionSpecificDocForMarkup } from "../../../svelteLanguage"; 5 | import { svelte2MarkupBlockCompletitionItems } from "../../../svelte2Language"; 6 | import { svelte3MarkupBlockCompletitionItems } from "../../../svelte3Language"; 7 | import { findLastOpenBlockIndex, isInsideOpenBlock } from "./BlockHelpers"; 8 | import { ScopeContext } from "../../../interfaces"; 9 | import { buildPropertyDocumentation, buildComputedDocumentation, buildMethodDocumentation } from "../../../svelteDocUtils"; 10 | import { getIdentifierAtOffset } from "../../../StringHelpers"; 11 | import { findItemInSvelteDoc, findLocationForItemInSvelteDoc } from "../../../SvelteItemsHelpers"; 12 | 13 | export class BlockOpenService extends BaseService { 14 | public getCompletitionItems(document: SvelteDocument, context: ScopeContext): Array { 15 | const openBlockIndex = findLastOpenBlockIndex(context.content, context.offset); 16 | if (openBlockIndex < 0) { 17 | return null; 18 | } 19 | 20 | const versionsSpecific = [ 21 | { version: SVELTE_VERSION_2, specific: svelte2MarkupBlockCompletitionItems }, 22 | { version: SVELTE_VERSION_3, specific: svelte3MarkupBlockCompletitionItems} 23 | ]; 24 | 25 | const blockContent = document.content.substring(openBlockIndex, context.offset); 26 | if (/^{#([\w\d_]*)$/g.test(blockContent)) { 27 | return [...markupBlockCompletitionItems, ...getVersionSpecificSelection(document, versionsSpecific)]; 28 | } 29 | 30 | const match = /^{([#:][\w\d_]*)\s*[^}]*/g.exec(blockContent); 31 | if (match) { 32 | const blockName = match[1]; 33 | if (blockName === '#if' || blockName === ':elseif' || blockName === '#await' || blockName === '#each') { 34 | return document.metadata ? [ 35 | ...document.metadata.data, 36 | ...document.metadata.computed, 37 | ...getVersionSpecificMetadataForMarkup(document) 38 | ] : []; 39 | } 40 | } 41 | 42 | return null; 43 | } 44 | 45 | public getHover(document: SvelteDocument, context: ScopeContext) { 46 | if (!isInsideOpenBlock(context.content, context.offset)) { 47 | return null; 48 | } 49 | 50 | return findItemInSvelteDoc([ 51 | {items: getVersionSpecificDocForMarkup(document), handler: buildMethodDocumentation}, 52 | {items: document.sveltedoc.computed, handler: buildComputedDocumentation}, 53 | {items: document.sveltedoc.data, handler: buildPropertyDocumentation} 54 | ], getIdentifierAtOffset(context.content, context.offset)); 55 | } 56 | 57 | public getDefinitions(document: SvelteDocument, context: ScopeContext): Definition[] 58 | { 59 | if (!isInsideOpenBlock(context.content, context.offset)) { 60 | return null; 61 | } 62 | 63 | return findLocationForItemInSvelteDoc( 64 | document, 65 | [ 66 | ...getVersionSpecificDocForMarkup(document), 67 | ...document.sveltedoc.computed, 68 | ...document.sveltedoc.data 69 | ], 70 | getIdentifierAtOffset(context.content, context.offset)); 71 | } 72 | } -------------------------------------------------------------------------------- /server/src/services/markup/ExpressionService.ts: -------------------------------------------------------------------------------- 1 | import { CompletionItem } from "vscode-languageserver"; 2 | import { BaseService } from "../Common"; 3 | import { ScopeContext } from "../../interfaces"; 4 | import { SvelteDocument, SVELTE_VERSION_2 } from "../../SvelteDocument"; 5 | import { svelte2DefaultComponentMethods } from "../../svelte2Language"; 6 | import { findItemInSvelteDoc, findLocationForItemInSvelteDoc } from "../../SvelteItemsHelpers"; 7 | import { buildPropertyDocumentation, buildComputedDocumentation, buildMethodDocumentation } from "../../svelteDocUtils"; 8 | import { getIdentifierAtOffset } from "../../StringHelpers"; 9 | 10 | export class ExpressionService extends BaseService { 11 | public getSupportedSvelteVersions(): number[] { 12 | return [SVELTE_VERSION_2]; 13 | } 14 | 15 | public getCompletitionItems(document: SvelteDocument, context: ScopeContext): Array { 16 | const index = this.findLastOpenExpressionIndex(context.content, context.offset); 17 | if (index < 0) { 18 | return null; 19 | } 20 | 21 | const result = []; 22 | 23 | if (document.metadata) { 24 | result.push(...[ 25 | ...svelte2DefaultComponentMethods, 26 | ...document.metadata.data, 27 | ...document.metadata.computed, 28 | ...document.metadata.methods, 29 | ...document.metadata.helpers 30 | ]); 31 | } 32 | 33 | 34 | return result; 35 | } 36 | 37 | public getHover(document: SvelteDocument, context: ScopeContext) { 38 | if (!this.isInsideExpression(context.content, context.offset)) { 39 | return null; 40 | } 41 | 42 | return findItemInSvelteDoc([ 43 | {items: document.sveltedoc.helpers, handler: buildMethodDocumentation}, 44 | {items: document.sveltedoc.computed, handler: buildComputedDocumentation}, 45 | {items: document.sveltedoc.data, handler: buildPropertyDocumentation}, 46 | {items: document.sveltedoc.methods, handler: buildMethodDocumentation}, 47 | ], getIdentifierAtOffset(context.content, context.offset)); 48 | } 49 | 50 | public getDefinitions(document: SvelteDocument, context: ScopeContext) 51 | { 52 | if (!this.isInsideExpression(context.content, context.offset)) { 53 | return null; 54 | } 55 | 56 | return findLocationForItemInSvelteDoc( 57 | document, 58 | [ 59 | ...document.sveltedoc.helpers, 60 | ...document.sveltedoc.computed, 61 | ...document.sveltedoc.data, 62 | ...document.sveltedoc.methods 63 | ], 64 | getIdentifierAtOffset(context.content, context.offset)); 65 | } 66 | 67 | private isInsideExpression(content: string, offset: number) { 68 | return this.findLastOpenExpressionIndex(content, offset) >= 0; 69 | } 70 | 71 | private findLastOpenExpressionIndex(content: string, offset: number): number { 72 | const openIndex = content.lastIndexOf('"', offset); 73 | if (openIndex < 0) { 74 | return -1; 75 | } 76 | 77 | const endIndex = content.indexOf('"', openIndex + 1); 78 | if (endIndex > 0 && endIndex < offset) { 79 | return -1; 80 | } 81 | 82 | const spaceIndex = content.lastIndexOf(' ', offset); 83 | if (spaceIndex < 0 || spaceIndex > openIndex) { 84 | return -1; 85 | } 86 | 87 | if (content.startsWith('on:', spaceIndex + 1)) { 88 | return openIndex + 1; 89 | } 90 | 91 | return -1; 92 | } 93 | } -------------------------------------------------------------------------------- /server/src/services/markup/block/BlockHelpers.ts: -------------------------------------------------------------------------------- 1 | export function findLastOpenBlockIndex(content: string, position: number) { 2 | const openBlockIndex = content.lastIndexOf('{#', position); 3 | if (openBlockIndex >= 0) { 4 | const closeBlockIndex = content.indexOf('}', openBlockIndex); 5 | if (closeBlockIndex > 0 && closeBlockIndex < position) { 6 | return -1; 7 | } 8 | } 9 | 10 | return openBlockIndex; 11 | } 12 | 13 | export function isInsideOpenBlock(content: string, position: number) { 14 | let openBlockIndex = content.lastIndexOf('{#', position); 15 | if (openBlockIndex < 0) { 16 | openBlockIndex = content.lastIndexOf('{:elseif', position); 17 | } 18 | if (openBlockIndex < 0) { 19 | openBlockIndex = content.lastIndexOf('{:else if', position); 20 | } 21 | if (openBlockIndex >= 0) { 22 | const closeBlockIndex = content.indexOf('}', openBlockIndex); 23 | return closeBlockIndex > 0 && closeBlockIndex >= position; 24 | } 25 | return false; 26 | } 27 | 28 | export function findLastInnerBlockIndex(content: string, position: number): number { 29 | const openIndex = content.lastIndexOf('{:', position); 30 | if (openIndex < 0) { 31 | return -1; 32 | } 33 | 34 | const closeIndex = content.indexOf('}', openIndex); 35 | if (closeIndex > 0 && closeIndex < position) { 36 | return -1; 37 | } 38 | 39 | return openIndex; 40 | } 41 | 42 | export function findLastCloseBlockIndex(content: string, position: number) { 43 | const openIndex = content.lastIndexOf('{/', position); 44 | if (openIndex < 0) { 45 | return -1; 46 | } 47 | 48 | const endIndex = content.indexOf('}', openIndex); 49 | if (endIndex > 0 && endIndex < position) { 50 | return -1; 51 | } 52 | 53 | return openIndex; 54 | } 55 | 56 | export function findNearestOpenBlockRange(content: string, position: number) { 57 | const blockStartIndex = content.lastIndexOf('{#', position); 58 | if (blockStartIndex < 0) { 59 | return null; 60 | } 61 | 62 | const blockEndTagIndex = content.indexOf('}', blockStartIndex); 63 | if (blockEndTagIndex < 0) { 64 | return null; 65 | } 66 | 67 | return { 68 | startIndex: blockStartIndex, 69 | endIndex: blockEndTagIndex + 1 70 | }; 71 | } 72 | 73 | export function findNearestCloseBlockIndex(content: string, blockName: string, blockStartIndex: number) { 74 | return content.indexOf(`{/${blockName}}`, blockStartIndex); 75 | } 76 | 77 | export function findNearestNotClosedBlock(content: string, offset: number) { 78 | let positionToSearch = offset; 79 | 80 | while (positionToSearch > 0) { 81 | const openBlockRange = this.findNearestOpenBlockRange(content, positionToSearch); 82 | if (openBlockRange == null) { 83 | return null; 84 | } 85 | 86 | const blockContent = content.substring(openBlockRange.startIndex, openBlockRange.endIndex); 87 | const match = /^{#([\w\d_]+).*}$/g.exec(blockContent); 88 | if (match) { 89 | const blockName = match[1]; 90 | 91 | const closeBlockIndex = this.findNearestCloseBlockIndex(content, blockName, openBlockRange.endIndex); 92 | if (closeBlockIndex < 0 || closeBlockIndex >= offset) { 93 | return { 94 | blockName, 95 | range: openBlockRange 96 | } 97 | } 98 | } 99 | 100 | positionToSearch = openBlockRange.startIndex - 1; 101 | } 102 | 103 | return null; 104 | } -------------------------------------------------------------------------------- /server/src/services/CompositeService.ts: -------------------------------------------------------------------------------- 1 | import { IService } from "./Common"; 2 | import { SvelteDocument, SVELTE_VERSION_2, SVELTE_VERSION_3 } from "../SvelteDocument"; 3 | import { WorkspaceContext, ScopeContext } from "../interfaces"; 4 | import { CompletionItem, Hover, MarkupContent, MarkedString, MarkupKind, Definition } from "vscode-languageserver"; 5 | import { isArray } from "util"; 6 | 7 | /** 8 | * Implements a composite completion services that find all appliable services 9 | * and merge it completion items to one resulting list. 10 | */ 11 | export class CompositeCompletionService implements IService { 12 | private _services: Array; 13 | 14 | public constructor(services: Array) { 15 | this._services = services; 16 | } 17 | 18 | public getCompletitionItems(document: SvelteDocument, context: ScopeContext, workspace: WorkspaceContext): Array { 19 | const reducedContext = this.reduceContext(context); 20 | if (reducedContext === null) { 21 | return null; 22 | } 23 | 24 | return this.findServiceResults( 25 | document, 26 | service => service.getCompletitionItems(document, reducedContext, workspace) 27 | ); 28 | } 29 | 30 | public getHover(document: SvelteDocument, context: ScopeContext, workspace: WorkspaceContext): Hover { 31 | const reducedContext = this.reduceContext(context); 32 | if (reducedContext === null) { 33 | return null; 34 | } 35 | 36 | const results = this.findServiceResults( 37 | document, 38 | service => service.getHover(document, reducedContext, workspace) 39 | ); 40 | if (results && results.length > 0) { 41 | let aggregatedHover = { contents: { kind: MarkupKind.Markdown, value: '' } }; 42 | results.forEach((element: Hover) => { 43 | (aggregatedHover.contents).value += (element.contents).value; 44 | }); 45 | return aggregatedHover; 46 | } else { 47 | return null; 48 | } 49 | } 50 | 51 | public getDefinitions(document: SvelteDocument, context: ScopeContext, workspace: WorkspaceContext): Definition[] { 52 | const reducedContext = this.reduceContext(context); 53 | if (reducedContext === null) { 54 | return null; 55 | } 56 | 57 | return this.findServiceResults( 58 | document, 59 | service => service.getDefinitions(document, reducedContext, workspace) 60 | ); 61 | } 62 | 63 | public getSupportedSvelteVersions(): number[] { 64 | return [SVELTE_VERSION_2, SVELTE_VERSION_3]; 65 | } 66 | 67 | protected reduceContext(context: ScopeContext): ScopeContext { 68 | return context; 69 | } 70 | 71 | private findServiceResults(document: SvelteDocument, callback: (service: IService) => any|null) { 72 | let result = null; 73 | 74 | this._services.forEach(service => { 75 | if (service.getSupportedSvelteVersions().indexOf(document.svelteVersion()) < 0) { 76 | return; 77 | } 78 | 79 | const serviceResult = callback(service); 80 | 81 | if (serviceResult !== null) { 82 | if (result === null) { 83 | result = []; 84 | } 85 | 86 | if (isArray(serviceResult)) { 87 | result.push(...serviceResult); 88 | } else { 89 | result.push(serviceResult); 90 | } 91 | } 92 | }); 93 | 94 | return result; 95 | } 96 | } -------------------------------------------------------------------------------- /server/src/services/markup/PlaceholdersService.ts: -------------------------------------------------------------------------------- 1 | import { SvelteDocument } from "../../SvelteDocument"; 2 | import { ScopeContext } from "../../interfaces"; 3 | import { BaseService } from "../Common"; 4 | import { CompletionItem } from "vscode-languageserver"; 5 | import { PlaceholderModifiers, getVersionSpecificMetadataForMarkup, getVersionSpecificDocForMarkup } from "../../svelteLanguage"; 6 | import { cloneCompletionItem } from "../Utils"; 7 | import { findItemInSvelteDoc, findLocationForItemInSvelteDoc } from "../../SvelteItemsHelpers"; 8 | import { buildMethodDocumentation, buildComputedDocumentation, buildPropertyDocumentation } from "../../svelteDocUtils"; 9 | import { getIdentifierAtOffset } from "../../StringHelpers"; 10 | 11 | export class PlaceholdersService extends BaseService { 12 | public getCompletitionItems(document: SvelteDocument, context: ScopeContext): Array { 13 | const openIndex = this.findLastOpenPlaceholderIndex(context.content, context.offset); 14 | 15 | if (openIndex < 0) { 16 | return null; 17 | } 18 | 19 | const contentPart = document.content.substring(openIndex, context.offset); 20 | if (/{@[\w\d_]*$/.test(contentPart)) { 21 | return PlaceholderModifiers; 22 | } 23 | 24 | const result = []; 25 | 26 | if (document.metadata) { 27 | result.push(...[ 28 | ...document.metadata.data, 29 | ...document.metadata.computed, 30 | ...getVersionSpecificMetadataForMarkup(document), 31 | ]); 32 | } 33 | 34 | if (openIndex + 1 === context.offset) { 35 | result.push(...PlaceholderModifiers 36 | .map(cloneCompletionItem) 37 | .map(item => { 38 | item.insertText = `@${item.label}`; 39 | item.filterText = `@${item.label}`; 40 | return item; 41 | })); 42 | } 43 | 44 | return result; 45 | } 46 | 47 | public getHover(document: SvelteDocument, context: ScopeContext) { 48 | if (!this.isInsidePlaceholder(context.content, context.offset)) { 49 | return null; 50 | } 51 | 52 | return findItemInSvelteDoc([ 53 | {items: getVersionSpecificDocForMarkup(document), handler: buildMethodDocumentation}, 54 | {items: document.sveltedoc.computed, handler: buildComputedDocumentation}, 55 | {items: document.sveltedoc.data, handler: buildPropertyDocumentation}, 56 | ], getIdentifierAtOffset(context.content, context.offset)); 57 | } 58 | 59 | public getDefinitions(document: SvelteDocument, context: ScopeContext) 60 | { 61 | if (!this.isInsidePlaceholder(context.content, context.offset)) { 62 | return null; 63 | } 64 | 65 | return findLocationForItemInSvelteDoc( 66 | document, 67 | [ 68 | ...getVersionSpecificDocForMarkup(document), 69 | ...document.sveltedoc.computed, 70 | ...document.sveltedoc.data 71 | ], 72 | getIdentifierAtOffset(context.content, context.offset)); 73 | } 74 | 75 | private isInsidePlaceholder(content: string, offset: number) { 76 | return this.findLastOpenPlaceholderIndex(content, offset) >= 0; 77 | } 78 | 79 | private findLastOpenPlaceholderIndex(content: string, offset: number): number { 80 | const openIndex = content.lastIndexOf('{', offset); 81 | if (openIndex < 0) { 82 | return -1; 83 | } 84 | 85 | const endIndex = content.indexOf('}', openIndex); 86 | if (endIndex > 0 && endIndex < offset) { 87 | return -1; 88 | } 89 | 90 | return openIndex; 91 | } 92 | } -------------------------------------------------------------------------------- /server/src/services/ChoosingService.ts: -------------------------------------------------------------------------------- 1 | import { IService, EmptyHoverContent } from './Common'; 2 | import { SvelteDocument, SVELTE_VERSION_2, SVELTE_VERSION_3 } from '../SvelteDocument'; 3 | import { CompletionItem, Hover, Definition } from 'vscode-languageserver'; 4 | import { WorkspaceContext, ScopeContext } from '../interfaces'; 5 | 6 | export interface ChoosingServiceOptions { 7 | exclusive?: boolean; 8 | } 9 | 10 | const __defaultServiceOptions: ChoosingServiceOptions = { 11 | exclusive: false 12 | }; 13 | 14 | /** 15 | * Implements a choosing completition services, find first applyable services 16 | * and use it to getting completion items. 17 | */ 18 | export class ChoosingService implements IService { 19 | private _services: Array; 20 | private _options: ChoosingServiceOptions; 21 | 22 | public constructor(services: Array, options?: ChoosingServiceOptions) { 23 | this._services = services; 24 | this._options = Object.assign({}, __defaultServiceOptions, options); 25 | } 26 | 27 | public getCompletitionItems(document: SvelteDocument, context: ScopeContext, workspace: WorkspaceContext): Array { 28 | const reducedContext = this.reduceContext(context, document, workspace); 29 | if (reducedContext === null) { 30 | return null; 31 | } 32 | 33 | return this.findServiceResults( 34 | document, 35 | service => service.getCompletitionItems(document, reducedContext, workspace), 36 | [] 37 | ); 38 | } 39 | 40 | public getHover(document: SvelteDocument, context: ScopeContext, workspace: WorkspaceContext): Hover { 41 | const reducedContext = this.reduceContext(context, document, workspace); 42 | if (reducedContext === null) { 43 | return null; 44 | } 45 | 46 | return this.findServiceResults( 47 | document, 48 | service => service.getHover(document, reducedContext, workspace), 49 | EmptyHoverContent 50 | ); 51 | } 52 | 53 | public getDefinitions(document: SvelteDocument, context: ScopeContext, workspace: WorkspaceContext): Definition[] { 54 | const reducedContext = this.reduceContext(context, document, workspace); 55 | if (reducedContext === null) { 56 | return null; 57 | } 58 | 59 | return this.findServiceResults( 60 | document, 61 | service => service.getDefinitions(document, reducedContext, workspace), 62 | null 63 | ); 64 | } 65 | 66 | public getSupportedSvelteVersions(): number[] { 67 | return [SVELTE_VERSION_2, SVELTE_VERSION_3]; 68 | } 69 | 70 | protected reduceContext(context: ScopeContext, _document: SvelteDocument, _workspace: WorkspaceContext): ScopeContext { 71 | return context; 72 | } 73 | 74 | private findServiceResults( 75 | document: SvelteDocument, 76 | callback: (service: IService) => any|null, 77 | emptyValue: any 78 | ) { 79 | let result = null; 80 | 81 | this._services.some(service => { 82 | if (service.getSupportedSvelteVersions().indexOf(document.svelteVersion()) < 0) { 83 | return false; 84 | } 85 | 86 | const serviceResult = callback(service); 87 | 88 | if (serviceResult) { 89 | result = serviceResult; 90 | return true; 91 | } 92 | 93 | return false; 94 | }); 95 | 96 | if (result === null && this._options.exclusive) { 97 | return emptyValue; 98 | } 99 | 100 | return result; 101 | } 102 | } -------------------------------------------------------------------------------- /server/src/services/markup/NamedSlotParamsService.ts: -------------------------------------------------------------------------------- 1 | import { BaseService } from "../Common"; 2 | import { SvelteDocument } from "../../SvelteDocument"; 3 | import { CompletionItem } from "vscode-languageserver"; 4 | import { TagScopeContext } from "./TagInnerService"; 5 | import { cloneCompletionItem } from "../Utils"; 6 | import { findNearestOpenComponent, findLastDirectiveIndex, getAttributeLetNameAtOffset, getNamedSlotName } from "./TagHelpers"; 7 | import { WorkspaceContext } from "../../interfaces"; 8 | import { findItemInSvelteDoc, findLocationForItemInSvelteDoc } from "../../SvelteItemsHelpers"; 9 | import { buildSlotPerameterDocumentation } from "../../svelteDocUtils"; 10 | 11 | export class NamedSlotParamsService extends BaseService { 12 | public getCompletitionItems(document: SvelteDocument, context: TagScopeContext, workspace: WorkspaceContext): Array { 13 | const index = findLastDirectiveIndex(context.content, context.offset, 'let'); 14 | if (index < 0) { 15 | return null; 16 | } 17 | const contentPart = context.content.substring(index, context.offset); 18 | if (/let:[\w\d_]*$/g.test(contentPart)) { 19 | const slotName = getNamedSlotName(context.content); 20 | if (!slotName) { 21 | return null; 22 | } 23 | 24 | const component = findNearestOpenComponent(context.documentOffset - context.offset - 1, document, workspace.documentsCache); 25 | if (component === null) { 26 | return null; 27 | } 28 | 29 | const namedSlotMetadata = component.metadata.slotsMetadata.find(s => s.name === slotName); 30 | if (!namedSlotMetadata) { 31 | return null; 32 | } 33 | 34 | return [ 35 | ...namedSlotMetadata.parameters 36 | .map(cloneCompletionItem) 37 | .map(item => { 38 | item.detail = `[Svelte] Prop of "${slotName}" slot for ${component.sveltedoc.name}`; 39 | return item; 40 | }) 41 | ]; 42 | } 43 | 44 | return null; 45 | } 46 | 47 | public getHover(document: SvelteDocument, context: TagScopeContext, workspace: WorkspaceContext) { 48 | const slotName = getNamedSlotName(context.content); 49 | if (!slotName) { 50 | return null; 51 | } 52 | 53 | const component = findNearestOpenComponent(context.documentOffset - context.offset - 1, document, workspace.documentsCache); 54 | if (component === null) { 55 | return null; 56 | } 57 | 58 | const slotDoc = this.getNamedSlotDocumentation(component, slotName); 59 | if (!slotDoc) { 60 | return null; 61 | } 62 | 63 | return findItemInSvelteDoc([ 64 | {items: slotDoc.parameters, handler: buildSlotPerameterDocumentation} 65 | ], getAttributeLetNameAtOffset(context)); 66 | } 67 | 68 | public getDefinitions(document: SvelteDocument, context: TagScopeContext, workspace: WorkspaceContext) 69 | { 70 | const slotName = getNamedSlotName(context.content); 71 | if (!slotName) { 72 | return null; 73 | } 74 | 75 | const component = findNearestOpenComponent(context.documentOffset - context.offset - 1, document, workspace.documentsCache); 76 | if (component === null) { 77 | return null; 78 | } 79 | 80 | const slotDoc = this.getNamedSlotDocumentation(component, slotName); 81 | if (!slotDoc) { 82 | return null; 83 | } 84 | 85 | return findLocationForItemInSvelteDoc(component, slotDoc.parameters, getAttributeLetNameAtOffset(context)); 86 | } 87 | 88 | private getNamedSlotDocumentation(component: SvelteDocument, name: string) { 89 | return component.sveltedoc.slots.find(s => s.name === name); 90 | } 91 | } -------------------------------------------------------------------------------- /server/src/services/markup/OpenTagService.ts: -------------------------------------------------------------------------------- 1 | import { BaseService } from "../Common"; 2 | import { SvelteDocument, SVELTE_VERSION_2, SVELTE_VERSION_3 } from "../../SvelteDocument"; 3 | import { findLastOpenTagIndex } from "./TagHelpers"; 4 | import { CompletionItem, Hover, MarkupContent, Definition } from "vscode-languageserver"; 5 | import { ScopeContext, WorkspaceContext } from "../../interfaces"; 6 | import { SpecialComponentNamespace, getVersionSpecificSelection } from "../../svelteLanguage"; 7 | import { svelte2SpecialComponents } from "../../svelte2Language"; 8 | import { svelte3SpecialComponents } from "../../svelte3Language"; 9 | import { cloneCompletionItem, getImportedComponentDocumentation, getImportedComponentDefinition } from "../Utils"; 10 | import { regexIndexOf } from "../../StringHelpers"; 11 | 12 | export class OpenTagService extends BaseService { 13 | public getCompletitionItems(document: SvelteDocument, context: ScopeContext, workspace: WorkspaceContext): Array { 14 | const openIndex = findLastOpenTagIndex(context.content, context.offset); 15 | if (openIndex < 0) { 16 | return null; 17 | } 18 | 19 | const spaceIndex = regexIndexOf(context.content, /\s/, openIndex); 20 | if (spaceIndex > 0 && spaceIndex < context.offset) { 21 | return null; 22 | } 23 | 24 | const tagContent = context.content.substring(openIndex, context.offset); 25 | 26 | const match = /<([\w\d_]+:)?[\w\d_]*$/g.exec(tagContent); 27 | 28 | const versionsSpecific = [ 29 | { version: SVELTE_VERSION_2, specific: svelte2SpecialComponents }, 30 | { version: SVELTE_VERSION_3, specific: svelte3SpecialComponents} 31 | ]; 32 | 33 | if (match) { 34 | if (!document.metadata || match[1] === `${SpecialComponentNamespace}:`) { 35 | return [ 36 | ...getVersionSpecificSelection(document, versionsSpecific) 37 | ]; 38 | } 39 | 40 | if (!match[1]) { 41 | return [ 42 | ...document.metadata.components 43 | .map(cloneCompletionItem) 44 | .map(item => { 45 | item.documentation = getImportedComponentDocumentation(item.label, document, workspace).contents; 46 | 47 | return item; 48 | }), 49 | ...svelte2SpecialComponents 50 | .map(cloneCompletionItem) 51 | .map(item => { 52 | item.filterText = `${SpecialComponentNamespace}:${item.label}`; 53 | item.sortText = `${SpecialComponentNamespace}:${item.label}`; 54 | item.insertText = `${SpecialComponentNamespace}:${item.label}`; 55 | 56 | return item; 57 | }) 58 | ]; 59 | } 60 | } 61 | 62 | return null; 63 | } 64 | 65 | public getHover(document: SvelteDocument, context: ScopeContext, workspace: WorkspaceContext): Hover { 66 | return getImportedComponentDocumentation(this.getTagContent(context), document, workspace); 67 | } 68 | 69 | public getDefinitions(document: SvelteDocument, context: ScopeContext, workspace: WorkspaceContext): Definition[] { 70 | return getImportedComponentDefinition(this.getTagContent(context), document, workspace); 71 | } 72 | 73 | private getTagContent(context: ScopeContext) { 74 | const openIndex = findLastOpenTagIndex(context.content, context.offset); 75 | if (openIndex < 0) { 76 | return null; 77 | } 78 | 79 | const spaceIndex = regexIndexOf(context.content, /\s/, openIndex); 80 | if (spaceIndex > 0 && spaceIndex < context.offset) { 81 | return null; 82 | } 83 | 84 | return context.content.substring(openIndex + 1, spaceIndex); 85 | } 86 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to the "svelte-intellisense" extension will be documented in this file. 3 | 4 | Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. 5 | 6 | ## [0.7.1] 7 | - [Fixed] Issue with "Request textDocument/hover failed with message: Cannot read property 'helpers' of undefined" and similar issues 8 | 9 | ## [0.7.0] 10 | - [Added] Support for locations in Svelte v3. 11 | - [Added] Support for events in Svelte v3. 12 | 13 | ## [0.6.0] 14 | - [Added] Partial support for Svelte v3 - syntax, data, methods, components, slots. 15 | 16 | ## [0.5.0] 17 | - [Added] Support for aliases in rollup and webpack 18 | - [Added] Hover and definition (go to source file) for different self and imported components metadata 19 | 20 | ## [0.4.1] 21 | - [Fixed] Fixed some crashes of language server completion requests when medatadata of current document is not provided in some cases 22 | - [Fixed] Update [sveltedoc-parser](https://github.com/alexprey/sveltedoc-parser) library to 1.1.5 version with crash-fixes 23 | 24 | ## [0.4.0] 25 | - [Added] Auto-completion for component assign properties 26 | - [Added] Auto-completion for HTML tags attribute assign 27 | - [Added] Sugestions for data properties that have `@type` attribute with union constant types, like `{('plain'|'primary'|'secondary')}` 28 | - [Added] Definition (go to source file) for component tag in template 29 | - [Added] Definition (go to source file) for imported component and import section in script 30 | 31 | ## [0.3.1] 32 | - [Fixed] Issue #8: Your extension is affected by event stream and have been blocked 33 | 34 | ## [0.3.0] 35 | - [Fixed] Issue #2 with `:catch` and `:then` svelte syntax auto-insertion 36 | - [Fixed] Issue #3 with `ref:`, `transition:`, `in:`, `out:`, `use:` auto-insertion when user press : 37 | - [Fixed] Improved auto-completion for `this.` 38 | - [Fixed] Issue with auto-completion in `this.refs.<>` 39 | - [Fixed] Issue with providing completion item of `ref:` from markup into script block, now provided a correct completion item `refs.` with proper description 40 | - [Added] Auto-completion for single property getter `this.get().<>` 41 | - [Added] Auto-completion for data and computed properties and helper methods in `{#if _}`, `{:elseif _}`, `{#each _}`, `{#await _}` statements 42 | - [Added] Auto-completion for bind target property in `bind:...=<>` syntax in markup 43 | 44 | ## [0.2.0] 45 | - [Fixed] Auto-completion for nested blocks 46 | - [Added] Hover for component tag documentation in template 47 | - [Added] Hover for imported component documentation in script 48 | - [Added] Auto-completions for template: 49 | - Special svelte components syntax - `svelte:window`, `svelte:document`, `svelte:head`, `svelte:component`. 50 | - Special svelte tags syntax - `@debug`, `@html`. 51 | - Reference syntax for html elements - `ref:...`. 52 | - Standard bindings for html elements - `bind:...`. 53 | - Class syntax for html elements - `class:...`. 54 | - Accessible data, computed, helpers in `{}`. 55 | - Accessible data, computed, methods in attribute values like `on:event="..."`. 56 | - Accessible transitions for html elements - `transition:...`, `in:...`, `out:...`. 57 | - Accessible actions for html elements - `use:...`. 58 | - Accessible bindings for component tags - ` 117 | 118 | 155 | ``` 156 | 157 | ## TODO 158 | - Signature help for component tags 159 | - Workspace symbol search 160 | - Find all references 161 | - Rename symbol -------------------------------------------------------------------------------- /server/src/utils.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as fs from 'fs'; 3 | import { DocumentsCache } from './DocumentsCache'; 4 | import { SvelteDocument } from './SvelteDocument'; 5 | import { TextDocument } from 'vscode-languageserver'; 6 | 7 | const sep = path.sep || '/'; 8 | const svelteFileExtensions = [ '', '.svelte', '.html' ]; 9 | 10 | /** 11 | * File URI to Path function. Taken from https://github.com/TooTallNate/file-uri-to-path. 12 | * 13 | * @param {String} uri 14 | * @return {String} path 15 | */ 16 | export function fileUriToPath (uri) { 17 | if ('string' !== typeof uri || 18 | uri.length <= 7 || 19 | 'file://' !== uri.substring(0, 7)) { 20 | throw new TypeError('must pass in a file:// URI to convert to a file path'); 21 | } 22 | 23 | var rest = decodeURIComponent(uri.substring(7)); 24 | var firstSlash = rest.indexOf('/'); 25 | var host = rest.substring(0, firstSlash); 26 | var path = rest.substring(firstSlash + 1); 27 | 28 | // 2. Scheme Definition 29 | // As a special case, can be the string "localhost" or the empty 30 | // string; this is interpreted as "the machine from which the URL is 31 | // being interpreted". 32 | if ('localhost' === host) { 33 | host = ''; 34 | } 35 | 36 | if (host) { 37 | host = sep + sep + host; 38 | } 39 | 40 | // 3.2 Drives, drive letters, mount points, file system root 41 | // Drive letters are mapped into the top of a file URI in various ways, 42 | // depending on the implementation; some applications substitute 43 | // vertical bar ("|") for the colon after the drive letter, yielding 44 | // "file:///c|/tmp/test.txt". In some cases, the colon is left 45 | // unchanged, as in "file:///c:/tmp/test.txt". In other cases, the 46 | // colon is simply omitted, as in "file:///c/tmp/test.txt". 47 | path = path.replace(/^(.+)\|/, '$1:'); 48 | 49 | // for Windows, we need to invert the path separators from what a URI uses 50 | if (sep === '\\') { 51 | path = path.replace(/\//g, '\\'); 52 | } 53 | 54 | if (/^.+\:/.test(path)) { 55 | // has Windows drive at beginning of path 56 | } else { 57 | // unix path… 58 | path = sep + path; 59 | } 60 | 61 | return host + path; 62 | } 63 | 64 | /** 65 | * Path to File URI function. 66 | * 67 | * @param {String} filePath 68 | * @return {String} uri 69 | */ 70 | export function pathToFileUri (filePath) { 71 | // Require a leading slash, on windows prefixed with drive letter 72 | if (!/^(?:[a-z]:)?[\\\/]/i.test(filePath)) { 73 | throw new Error(`${filePath} is not an absolute path`); 74 | } 75 | 76 | const parts = filePath.split(/[\\\/]/); 77 | 78 | // If the first segment is a Windows drive letter, prefix with a slash and skip encoding 79 | let head = parts.shift()!; 80 | if (head !== '') { 81 | head = '/' + head; 82 | } else { 83 | head = encodeURIComponent(head); 84 | } 85 | 86 | return `file://${head}/${parts.map(encodeURIComponent).join('/')}`; 87 | } 88 | 89 | /** 90 | * Checks if svelte file (with .svelte or .html extension) exists based on a given file path and returns its real path. 91 | * @param {String} filepath File path with or without extension. 92 | * @returns {String} Full file path with extension. null if file not found. 93 | */ 94 | export function findSvelteFile(filepath: string) { 95 | for (let index = 0; index < svelteFileExtensions.length; index++) { 96 | const extension = svelteFileExtensions[index]; 97 | if (extension === '' || !filepath.endsWith(extension)) { 98 | const svelteFilePath = filepath + extension; 99 | if (fs.existsSync(svelteFilePath)) { 100 | return svelteFilePath; 101 | } 102 | } 103 | } 104 | 105 | return null; 106 | } 107 | 108 | /** 109 | * Checks if svelte file (with .svelte or .html extension) exists based on a given file path and returns document from cache. 110 | * @param {String} filepath File path with or without extension. 111 | * @param {DocumentsCache} documentsCache Documents cache to search in. 112 | * @returns {SvelteDocument} Document from cache, null if not found. 113 | */ 114 | export function findSvelteDocumentInCache(filepath: string, documentsCache: DocumentsCache) { 115 | for (let index = 0; index < svelteFileExtensions.length; index++) { 116 | const extension = svelteFileExtensions[index]; 117 | if (extension === '' || !filepath.endsWith(extension)) { 118 | const svelteFilePath = filepath + extension; 119 | if (documentsCache.has(svelteFilePath)) { 120 | return documentsCache.get(svelteFilePath); 121 | } 122 | } 123 | } 124 | return null; 125 | } 126 | 127 | /** 128 | * Finds node_modules directory going up from the given directory. 129 | * @param startDir Directory to start search from. 130 | */ 131 | export function findNodeModules(startDir: string) { 132 | let currentDir = startDir; 133 | const nodeModulesDir = 'node_modules'; 134 | while (currentDir !== '.') { 135 | const candidate = path.join(currentDir, nodeModulesDir); 136 | if (fs.existsSync(candidate)) { 137 | return candidate; 138 | } 139 | const newCurrentDir = path.dirname(currentDir); 140 | if (newCurrentDir === currentDir) { 141 | break; 142 | } 143 | currentDir = newCurrentDir; 144 | } 145 | return null; 146 | } 147 | 148 | /** 149 | * Creates TextDocument for the given file. 150 | * @param path Path to the document. 151 | */ 152 | export function createTextDocument(path: string, uri?: string) { 153 | const buffer = fs.readFileSync(path); 154 | return TextDocument.create( 155 | uri || pathToFileUri(path), 156 | 'svelte', 157 | 0, 158 | buffer.toString() 159 | ); 160 | } -------------------------------------------------------------------------------- /server/src/services/markup/TagHelpers.ts: -------------------------------------------------------------------------------- 1 | import { SvelteDocument } from "../../SvelteDocument"; 2 | import { DocumentsCache } from "../../DocumentsCache"; 3 | 4 | import { createTextDocument } from "../../utils"; 5 | import { regexLastIndexOf, regexIndexOf } from "../../StringHelpers"; 6 | import { ScopeContext } from "../../interfaces"; 7 | 8 | export function findLastOpenTagIndex(content: string, offset: number): number { 9 | const startIndex = content.lastIndexOf('<', offset); 10 | if (startIndex < 0) { 11 | return -1; 12 | } 13 | 14 | const endIndex = findTagInnerEndIndex(content, startIndex); 15 | if (endIndex > 0 && endIndex < offset) { 16 | return -1; 17 | } 18 | 19 | return startIndex; 20 | } 21 | 22 | export function findNearestOpenTag(content: string, offset: number) { 23 | let positionToSearch = offset; 24 | let countOfUnclosedTags = 0; 25 | 26 | while (positionToSearch > 0) { 27 | const startIndex = content.lastIndexOf('<', positionToSearch); 28 | if (startIndex < 0) { 29 | return null; 30 | } 31 | 32 | const endIndex = findTagInnerEndIndex(content, startIndex); 33 | if (endIndex < 0 || endIndex > positionToSearch) { 34 | return null; 35 | } 36 | 37 | if (content.charAt(endIndex - 1) !== '/') { 38 | if (content.charAt(startIndex + 1) === '/') { 39 | countOfUnclosedTags++; 40 | } else { 41 | if (countOfUnclosedTags > 0) { 42 | countOfUnclosedTags--; 43 | } else { 44 | const tagContent = endIndex < 0 45 | ? content.substring(startIndex) 46 | : content.substring(startIndex, endIndex); 47 | 48 | const match = /<(([\w\d_]+:)?[\w_]+[\w\d_]*)\s*/g.exec(tagContent); 49 | if (match) { 50 | return { 51 | tagName: match[1], 52 | tagNamespace: match[2], 53 | startIndex: startIndex, 54 | endIndex: endIndex, 55 | content: tagContent, 56 | }; 57 | } 58 | } 59 | } 60 | } 61 | 62 | positionToSearch = startIndex - 1; 63 | } 64 | 65 | return null; 66 | } 67 | 68 | export function findLastOpenTag(content: string, offset: number) { 69 | const startIndex = content.lastIndexOf('<', offset); 70 | if (startIndex < 0) { 71 | return null; 72 | } 73 | 74 | const endIndex = findTagInnerEndIndex(content, startIndex); 75 | if (endIndex > 0 && endIndex < offset) { 76 | return null; 77 | } 78 | 79 | const tagContent = endIndex < 0 80 | ? content.substring(startIndex) 81 | : content.substring(startIndex, endIndex); 82 | 83 | const match = /<(([\w\d_]+:)?[\w_]+[\w\d_]*)\s*/g.exec(tagContent); 84 | if (match) { 85 | return { 86 | tagName: match[1], 87 | tagNamespace: match[2], 88 | startIndex: startIndex, 89 | endIndex: endIndex, 90 | content: tagContent, 91 | }; 92 | } 93 | 94 | return null; 95 | } 96 | 97 | export function findTagInnerEndIndex(content: string, offset: number) { 98 | const tagCloseIndex = content.indexOf('>', offset); 99 | 100 | if (tagCloseIndex >= 0) { 101 | // Check if it is inside expression 102 | const expressionStartIndex = content.lastIndexOf('{', tagCloseIndex); 103 | const expressionEndIndex = content.lastIndexOf('}', tagCloseIndex); 104 | if (expressionStartIndex > offset && expressionStartIndex > expressionEndIndex) { 105 | return findTagInnerEndIndex(content, tagCloseIndex + 1); 106 | } 107 | } 108 | 109 | return tagCloseIndex; 110 | } 111 | 112 | export function findLastDirectiveIndex(content: string, offset: number, directiveName: string) { 113 | const index = content.lastIndexOf(`${directiveName}:`, offset); 114 | if (index < 0) { 115 | return -1; 116 | } 117 | 118 | const equalIndex = content.indexOf('=', index); 119 | if (equalIndex > 0 && equalIndex < offset) { 120 | return -1; 121 | } 122 | 123 | const spaceIndex = content.indexOf(' ', index); 124 | if (spaceIndex > 0 && spaceIndex < offset) { 125 | return -1; 126 | } 127 | 128 | return index; 129 | } 130 | 131 | export function findImportedComponent(componentName: string, document: SvelteDocument, documentsCache: DocumentsCache) { 132 | const component = document.importedComponents.find(c => c.name === componentName); 133 | 134 | if (component === undefined) { 135 | return null; 136 | } 137 | 138 | const externalDocument = documentsCache.has(component.filePath) ? documentsCache.get(component.filePath) : null; 139 | if (externalDocument && !externalDocument.document) { 140 | externalDocument.document = createTextDocument(component.filePath); 141 | } 142 | 143 | return externalDocument; 144 | } 145 | 146 | export function findNearestOpenComponent(offset: number, document: SvelteDocument, documentsCache: DocumentsCache) { 147 | const prevTag = findNearestOpenTag(document.content, offset); 148 | return prevTag === null ? null : findImportedComponent(prevTag.tagName, document, documentsCache); 149 | } 150 | 151 | export function getAttributeLetNameAtOffset(context: ScopeContext): string { 152 | const startIndex = regexLastIndexOf(context.content, /\slet:/, context.offset); 153 | let endIndex = regexIndexOf(context.content, /[\s=]/, context.offset); 154 | if (endIndex < 0) { 155 | endIndex = context.content.length; 156 | } 157 | 158 | if (startIndex < 0 || endIndex < 0 || endIndex < startIndex) { 159 | return null; 160 | } 161 | 162 | const name = context.content.substring(startIndex, endIndex); 163 | const match = /^let:([\w\d_]+)$/.exec(name); 164 | 165 | if (match) { 166 | return match[1]; 167 | } 168 | 169 | return null; 170 | } 171 | 172 | export function getNamedSlotName(content: string) { 173 | const match = content.match(/\sslot\s*=?\s*[\'|\"]([\w\d_$]*)[\'|\"]/) 174 | if (!match) { 175 | return null; 176 | } else { 177 | return match[1]; 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /server/src/services/script/ComponentPathService.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as fs from 'fs'; 3 | 4 | import { BaseService } from "../Common"; 5 | import { SvelteDocument } from "../../SvelteDocument"; 6 | import { CompletionItem, CompletionItemKind, Definition } from "vscode-languageserver"; 7 | import { WorkspaceContext, ScopeContext } from '../../interfaces'; 8 | import { getImportedComponentDefinition } from '../Utils'; 9 | 10 | export const SupportedComponentFileExtensions = [ 11 | '.svelte', 12 | '.html' 13 | ]; 14 | 15 | export interface ComponentPathServiceOptions { 16 | extensionsToSearch?: string[]; 17 | extensionsToExclude?: string[]; 18 | includeFileExtensionToInsert?: boolean; 19 | } 20 | 21 | const __defaultOptions: ComponentPathServiceOptions = { 22 | extensionsToSearch: SupportedComponentFileExtensions, 23 | extensionsToExclude: [], 24 | includeFileExtensionToInsert: true 25 | } 26 | 27 | export class ComponentPathService extends BaseService { 28 | 29 | private _options: ComponentPathServiceOptions; 30 | 31 | constructor(options?: ComponentPathServiceOptions) { 32 | super(); 33 | 34 | this._options = Object.assign({}, __defaultOptions, options); 35 | } 36 | 37 | public getCompletitionItems(document: SvelteDocument, context: ScopeContext, _workspace: WorkspaceContext): Array { 38 | const prevContent = context.content.substring(0, context.offset); 39 | 40 | // Find open quote for component path 41 | let quote = '\''; 42 | let openQuoteIndex = prevContent.lastIndexOf(quote); 43 | if (openQuoteIndex < 0) { 44 | quote = '"'; 45 | openQuoteIndex = prevContent.lastIndexOf(quote); 46 | } 47 | if (openQuoteIndex < 0) { 48 | quote = '`'; 49 | openQuoteIndex = prevContent.lastIndexOf(quote); 50 | } 51 | 52 | if (openQuoteIndex <= 0) { 53 | return null; 54 | } 55 | 56 | // Check that cursor positioned in component path string 57 | 58 | if (prevContent.indexOf(quote, openQuoteIndex + 1) < 0 59 | && prevContent.lastIndexOf(quote, openQuoteIndex - 1) <= prevContent.lastIndexOf(':', openQuoteIndex - 1) 60 | ) { 61 | const partialPath = prevContent.substring(openQuoteIndex + 1); 62 | 63 | // Do nothing if partial path started from root folder 64 | if (partialPath.startsWith('/')) { 65 | return []; 66 | } 67 | 68 | // Don't show auto-completion for hidden items 69 | if (/[\\\/]\.+$/g.test(partialPath)) { 70 | return []; 71 | } 72 | 73 | const result = []; 74 | 75 | if (document.importResolver !== null) { 76 | const resolvedPath = document.importResolver.resolvePath(partialPath); 77 | if (resolvedPath !== null) { 78 | result.push(...this.searchFolderItems(resolvedPath, partialPath, resolvedPath.indexOf('node_modules') >= 0)); 79 | } 80 | } 81 | 82 | return result; 83 | } 84 | 85 | return null; 86 | } 87 | 88 | public getDefinitions(document: SvelteDocument, context: ScopeContext, workspace: WorkspaceContext): Definition[] { 89 | const prevContent = context.content.substring(0, context.offset); 90 | const nextContent = context.content.substring(context.offset); 91 | 92 | const componentFileNameStartSearchResult = /\b([\w\d_.]+)$/g.exec(prevContent); 93 | const componentFileNameEndSearchResult = /^([\w\d_.]+)\s*['"]/g.exec(nextContent); 94 | 95 | if (componentFileNameStartSearchResult !== null && componentFileNameEndSearchResult !== null) { 96 | const componentNameSearchResult = /[^,{]\s*([\w\d_]+)\s*:.+$/g.exec(prevContent); 97 | if (componentNameSearchResult !== null) { 98 | const componentName = componentNameSearchResult[1]; 99 | return getImportedComponentDefinition(componentName, document, workspace); 100 | } 101 | 102 | const componentImportSearchResult = /import\s{\s*([\w\d_]+)\s}\s*from\s*.+$/g.exec(prevContent); 103 | if (componentImportSearchResult !== null) { 104 | const componentName = componentImportSearchResult[1]; 105 | return getImportedComponentDefinition(componentName, document, workspace); 106 | } 107 | } 108 | 109 | return null; 110 | } 111 | 112 | private searchFolderItems(searchFolderPath, partialPath, isFromNodeModules) { 113 | return fs.readdirSync(searchFolderPath) 114 | .map((foundPath) => { 115 | const basename = path.basename(foundPath); 116 | 117 | // Don't include hidden items 118 | if (basename.startsWith('.')) { 119 | return null; 120 | } 121 | 122 | const partialBaseName = path.basename(partialPath); 123 | 124 | const itemStats = fs.lstatSync(path.resolve(searchFolderPath, foundPath)); 125 | 126 | if (itemStats.isDirectory() || itemStats.isSymbolicLink()) { 127 | return { 128 | label: basename, 129 | kind: CompletionItemKind.Folder, 130 | detail: isFromNodeModules ? 'from node_modules' : null, 131 | commitCharacters: ['/'], 132 | insertText: basename.startsWith('@') && partialBaseName.startsWith('@') 133 | ? basename.substring(1) 134 | : basename, 135 | filterText: basename.startsWith('@') 136 | ? basename.substring(1) 137 | : basename, 138 | sortText: `1.${basename}` 139 | } 140 | } 141 | 142 | if (itemStats.isFile()) { 143 | const extname = path.extname(foundPath); 144 | 145 | if (this.isIncludedFileName(basename)) { 146 | const fileNameToInsert = this._options.includeFileExtensionToInsert 147 | ? basename 148 | : path.basename(basename, extname); 149 | 150 | // Check that file is a Svelte component file or not? 151 | if (SupportedComponentFileExtensions.indexOf(extname) >= 0) { 152 | return { 153 | label: basename, 154 | kind: CompletionItemKind.Class, 155 | detail: '[Svelte] component' + (isFromNodeModules ? ' from node_modules' : ''), 156 | commitCharacters: ['\''], 157 | sortText: `2.${basename}`, 158 | insertText: fileNameToInsert 159 | } 160 | } 161 | 162 | // Return a default file item statement 163 | return { 164 | label: basename, 165 | kind: CompletionItemKind.File, 166 | detail: isFromNodeModules ? 'from node_modules' : null, 167 | commitCharacters: ['\''], 168 | sortText: `2.${basename}`, 169 | insertText: fileNameToInsert 170 | } 171 | } 172 | } 173 | 174 | return null; 175 | }) 176 | .filter(item => item != null); 177 | } 178 | 179 | private isIncludedFileName(basename: string): boolean { 180 | return this._options.extensionsToSearch.some(ext => basename.endsWith(ext)) 181 | && !this._options.extensionsToExclude.some(ext => basename.endsWith(ext)); 182 | } 183 | } -------------------------------------------------------------------------------- /server/src/svelte2Language.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CompletionItem, CompletionItemKind, 3 | MarkupContent, MarkupKind, InsertTextFormat 4 | } from 'vscode-languageserver'; 5 | 6 | export const svelte2DefaultHtmlTagBindCompletionItems = [ 7 | { 8 | allowedHtmlTags: null, 9 | restrictedHtmlTags: ['svelte:window'], 10 | items: [ 11 | { 12 | label: 'offsetWidth', 13 | kind: CompletionItemKind.Constant, 14 | detail: '[Svelte] bind:offsetWidth={data} (One-Way)', 15 | }, 16 | { 17 | label: 'offsetHeight', 18 | kind: CompletionItemKind.Constant, 19 | detail: '[Svelte] bind:offsetHeight={data} (One-Way)', 20 | }, 21 | { 22 | label: 'clientWidth', 23 | kind: CompletionItemKind.Constant, 24 | detail: '[Svelte] bind:clientWidth={data} (One-Way)', 25 | }, 26 | { 27 | label: 'clientHeight', 28 | kind: CompletionItemKind.Constant, 29 | detail: '[Svelte] bind:clientHeight={data} (One-Way)', 30 | } 31 | ] 32 | }, 33 | ]; 34 | 35 | export const svelte2DefaultScriptRefsCompletionItem: CompletionItem = { 36 | label: 'refs', 37 | kind: CompletionItemKind.Property, 38 | detail: '[Svelte] refs', 39 | documentation: { 40 | kind: MarkupKind.Markdown, 41 | value: 42 | ` 43 | Refs are a convenient way to store a reference to particular DOM nodes or components. 44 | Declare a ref with \`ref:[name]\`, and access it inside your component's methods with \`this.refs.[name]\`. 45 | \`\`\` 46 | ` 47 | }, 48 | insertText: 'refs' 49 | }; 50 | 51 | export const svelte2DefaultRefCompletionItem: CompletionItem = { 52 | label: 'ref:...', 53 | kind: CompletionItemKind.Keyword, 54 | detail: '[Svelte] ref:', 55 | documentation: { 56 | kind: MarkupKind.Markdown, 57 | value: 58 | ` 59 | Refs are a convenient way to store a reference to particular DOM nodes or components. 60 | Declare a ref with \`ref:[name]\`, and access it inside your component's methods with \`this.refs.[name]\`. 61 | 62 | **Keep Attention!** 63 | Since only one element or component can occupy a given ref, don't use them in \`{#each ...}\` blocks. 64 | It's fine to use them in \`{#if ...}\` blocks however. 65 | 66 | Note that you can use refs in your \`