diff --git a/src/builtin-addons/core/template-completion-provider.ts b/src/builtin-addons/core/template-completion-provider.ts index 9aee97db..06cac921 100644 --- a/src/builtin-addons/core/template-completion-provider.ts +++ b/src/builtin-addons/core/template-completion-provider.ts @@ -8,8 +8,10 @@ import { emberBlockItems, emberMustacheItems, emberSubExpressionItems, emberModi import { templateContextLookup } from './template-context-provider'; import { provideComponentTemplatePaths } from './template-definition-provider'; -import { log } from '../../utils/logger'; +import { log, logInfo, logError } from '../../utils/logger'; import ASTPath, { getLocalScope } from '../../glimmer-utils'; +import Server from '../../server'; +import { Project } from '../../project-roots'; import { isLinkToTarget, isComponentArgumentName, @@ -80,6 +82,19 @@ function isArgumentName(name: string) { export default class TemplateCompletionProvider { constructor() {} + async initRegistry(_: Server, project: Project) { + try { + let initStartTime = Date.now(); + mListHelpers(project.root); + mListModifiers(project.root); + mListRoutes(project.root); + mListComponents(project.root); + mGetProjectAddonsInfo(project.root); + logInfo(project.root + ': registry initialized in ' + (Date.now() - initStartTime) + 'ms'); + } catch (e) { + logError(e); + } + } getAllAngleBracketComponents(root: string, uri: string) { const items: CompletionItem[] = []; return uniqBy( diff --git a/src/builtin-addons/core/template-definition-provider.ts b/src/builtin-addons/core/template-definition-provider.ts index ad604e73..cb72c1ca 100644 --- a/src/builtin-addons/core/template-definition-provider.ts +++ b/src/builtin-addons/core/template-definition-provider.ts @@ -5,6 +5,7 @@ import { Definition, Location } from 'vscode-languageserver'; import { DefinitionFunctionParams } from './../../utils/addon-api'; import { isLinkToTarget, isLinkComponentRouteTarget } from './../../utils/ast-helpers'; import ASTPath from './../../glimmer-utils'; +import { getGlobalRegistry } from './../../utils/layout-helpers'; import { isTemplatePath, getComponentNameFromURI, isModuleUnificationApp, getPodModulePrefix } from './../../utils/layout-helpers'; @@ -29,8 +30,23 @@ function normalizeAngleTagName(tagName: string) { .join('/'); } +export function getPathsFromRegistry(type: 'helper' | 'modifier' | 'component', name: string, root: string): string[] { + const absRoot = path.normalize(root); + const registry = getGlobalRegistry(); + const bucket: any = registry[type].get(name) || new Set(); + return Array.from(bucket).filter((el: string) => path.normalize(el).includes(absRoot) && fs.existsSync(el)) as string[]; +} + export function provideComponentTemplatePaths(root: string, rawComponentName: string) { const maybeComponentName = normalizeAngleTagName(rawComponentName); + const items = getPathsFromRegistry('component', maybeComponentName, root); + if (items.length) { + const results = items.filter((el) => el.endsWith('.hbs')); + if (results.length) { + return results; + } + } + let paths = [...getPathsForComponentTemplates(root, maybeComponentName)].filter(fs.existsSync); if (!paths.length) { @@ -142,8 +158,10 @@ export default class TemplateDefinitionProvider { } _provideLikelyRawComponentTemplatePaths(root: string, rawComponentName: string) { const maybeComponentName = normalizeAngleTagName(rawComponentName); - let paths = [...getPathsForComponentScripts(root, maybeComponentName), ...getPathsForComponentTemplates(root, maybeComponentName)].filter(fs.existsSync); - + let paths = getPathsFromRegistry('component', maybeComponentName, root); + if (!paths.length) { + paths = [...getPathsForComponentScripts(root, maybeComponentName), ...getPathsForComponentTemplates(root, maybeComponentName)].filter(fs.existsSync); + } if (!paths.length) { paths = mAddonPathsForComponentTemplates(root, maybeComponentName); } diff --git a/src/project-roots.ts b/src/project-roots.ts index 39b42f21..fd11baf8 100644 --- a/src/project-roots.ts +++ b/src/project-roots.ts @@ -7,21 +7,40 @@ import * as walkSync from 'walk-sync'; import { isGlimmerNativeProject, isGlimmerXProject } from './utils/layout-helpers'; import { ProjectProviders, collectProjectProviders, initBuiltinProviders } from './utils/addon-api'; import Server from './server'; +import { TextDocument, Diagnostic } from 'vscode-languageserver'; +export type Eexcutor = (server: Server, command: string, args: any[]) => any; +export type Linter = (document: TextDocument) => Diagnostic[]; export interface Executors { - [key: string]: (server: Server, command: string, args: any[]) => any; + [key: string]: Eexcutor; } export class Project { providers!: ProjectProviders; builtinProviders!: ProjectProviders; executors: Executors = {}; + linters: Linter[] = []; + addCommandExecutor(key: string, cb: Eexcutor) { + this.executors[key] = cb; + } + addLinter(cb: Linter) { + this.linters.push(cb); + } constructor(public readonly root: string) { this.providers = collectProjectProviders(root); this.builtinProviders = initBuiltinProviders(); } init(server: Server) { + this.builtinProviders.initFunctions.forEach((initFn) => initFn(server, this)); this.providers.initFunctions.forEach((initFn) => initFn(server, this)); + if (this.providers.info.length) { + logInfo('--------------------'); + logInfo('loded language server addons:'); + this.providers.info.forEach((addonName) => { + logInfo(' ' + addonName); + }); + logInfo('--------------------'); + } } } @@ -68,8 +87,8 @@ export default class ProjectRoots { try { const project = new Project(path); this.projects.set(path, project); - project.init(this.server); logInfo(`Ember CLI project added at ${path}`); + project.init(this.server); } catch (e) { logError(e); } diff --git a/src/server.ts b/src/server.ts index 7e3e915d..aaf2fd94 100644 --- a/src/server.ts +++ b/src/server.ts @@ -21,12 +21,14 @@ import { TextDocumentPositionParams, CompletionItem, StreamMessageReader, + WorkspaceFoldersChangeEvent, StreamMessageWriter, ReferenceParams, - Location + Location, + TextDocument } from 'vscode-languageserver'; -import ProjectRoots, { Executors } from './project-roots'; +import ProjectRoots, { Project, Executors } from './project-roots'; import DefinitionProvider from './definition-providers/entry'; import TemplateLinter from './template-linter'; import DocumentSymbolProvider from './symbols/document-symbol-provider'; @@ -92,7 +94,9 @@ export default class Server { templateLinter: TemplateLinter = new TemplateLinter(this); referenceProvider: ReferenceProvider = new ReferenceProvider(this); - + private onInitilized() { + this.connection.workspace.onDidChangeWorkspaceFolders(this.onDidChangeWorkspaceFolders.bind(this)); + } constructor() { // Make the text document manager listen on the connection // for open, change and close text document events @@ -103,6 +107,7 @@ export default class Server { // Bind event handlers this.connection.onInitialize(this.onInitialize.bind(this)); + this.connection.onInitialized(this.onInitilized.bind(this)); this.documents.onDidChangeContent(this.onDidChangeContent.bind(this)); this.connection.onDidChangeWatchedFiles(this.onDidChangeWatchedFiles.bind(this)); this.connection.onDocumentSymbol(this.onDocumentSymbol.bind(this)); @@ -172,6 +177,13 @@ export default class Server { this.connection.listen(); } + private onDidChangeWorkspaceFolders(event: WorkspaceFoldersChangeEvent) { + if (event.added.length) { + event.added.forEach((folder) => { + this.projectRoots.findProjectsInsideRoot(folder.uri); + }); + } + } // After the server has started the client sends an initilize request. The server receives // in the passed params the rootPath of the workspace plus the client capabilites. private onInitialize({ rootUri, rootPath, workspaceFolders }: InitializeParams): InitializeResult { @@ -205,6 +217,12 @@ export default class Server { }, documentSymbolProvider: true, referencesProvider: true, + workspace: { + workspaceFolders: { + supported: true, + changeNotifications: true + } + }, completionProvider: { resolveProvider: true, triggerCharacters: ['.', '::', '=', '/', '{{', '(', '<', '@', 'this.'] @@ -213,7 +231,6 @@ export default class Server { }; } - linters: any[] = []; executors: Executors = {}; private async onDidChangeContent(change: any) { @@ -226,17 +243,20 @@ export default class Server { results.push(result); }); } - for (let linter of this.linters) { - try { - let tempResult = await linter(change.document); - // API must return array - if (Array.isArray(tempResult)) { - tempResult.forEach((el) => { - results.push(el as Diagnostic); - }); + const project: Project | undefined = this.projectRoots.projectForUri(change.document.uri); + if (project) { + for (let linter of project.linters) { + try { + let tempResults = await linter(change.document as TextDocument); + // API must return array + if (Array.isArray(tempResults)) { + tempResults.forEach((el) => { + results.push(el as Diagnostic); + }); + } + } catch (e) { + logError(e); } - } catch (e) { - logError(e); } } diff --git a/src/utils/addon-api.ts b/src/utils/addon-api.ts index fcb411e6..239be69c 100644 --- a/src/utils/addon-api.ts +++ b/src/utils/addon-api.ts @@ -1,5 +1,12 @@ import { Location, TextDocumentIdentifier, Position, CompletionItem } from 'vscode-languageserver'; -import { getProjectAddonsRoots, getPackageJSON, getProjectInRepoAddonsRoots } from './layout-helpers'; +import { + getProjectAddonsRoots, + getPackageJSON, + getProjectInRepoAddonsRoots, + PackageInfo, + ADDON_CONFIG_KEY, + hasEmberLanguageServerExtension +} from './layout-helpers'; import * as path from 'path'; import { log, logInfo, logError } from './logger'; import Server from '../server'; @@ -11,7 +18,6 @@ import ScriptCompletionProvider from './../builtin-addons/core/script-completion import TemplateCompletionProvider from './../builtin-addons/core/template-completion-provider'; import { Project } from '../project-roots'; -const ADDON_CONFIG_KEY = 'ember-language-server'; interface BaseAPIParams { server: Server; textDocument: TextDocumentIdentifier; @@ -53,7 +59,7 @@ interface HandlerObject { updateHandler: () => void; packageRoot: string; debug: boolean; - packageJSON: any; + packageJSON: PackageInfo; capabilities: NormalizedCapabilities; } @@ -82,7 +88,8 @@ export function initBuiltinProviders(): ProjectProviders { return { definitionProviders: [scriptDefinition.onDefinition.bind(scriptDefinition), templateDefinition.onDefinition.bind(templateDefinition)], referencesProviders: [], - initFunctions: [], + initFunctions: [templateCompletion.initRegistry.bind(this)], + info: [], completionProviders: [scriptCompletion.onComplete.bind(scriptCompletion), templateCompletion.onComplete.bind(templateCompletion)] }; } @@ -127,11 +134,13 @@ export function collectProjectProviders(root: string): ProjectProviders { referencesProviders: ReferenceResolveFunction[]; completionProviders: CompletionResolveFunction[]; initFunctions: InitFunction[]; + info: string[]; } = { definitionProviders: [], referencesProviders: [], completionProviders: [], - initFunctions: [] + initFunctions: [], + info: [] }; // onReference, onComplete, onDefinition @@ -143,6 +152,7 @@ export function collectProjectProviders(root: string): ProjectProviders { // let's reload files in case of debug mode for each request if (handlerObject.debug) { + result.info.push('addon-in-debug-mode: ' + _); logInfo(`els-addon-api: debug mode enabled for ${handlerObject.packageRoot}, for all requests resolvers will be reloaded.`); result.completionProviders.push(function(root: string, params: CompletionFunctionParams) { handlerObject.updateHandler(); @@ -177,6 +187,7 @@ export function collectProjectProviders(root: string): ProjectProviders { } } as InitFunction); } else { + result.info.push('addon: ' + _); if (handlerObject.capabilities.completionProvider && typeof handlerObject.handler.onComplete === 'function') { result.completionProviders.push(handlerObject.handler.onComplete); } @@ -200,6 +211,7 @@ export interface ProjectProviders { referencesProviders: ReferenceResolveFunction[]; completionProviders: CompletionResolveFunction[]; initFunctions: InitFunction[]; + info: string[]; } interface ExtensionCapabilities { @@ -236,6 +248,3 @@ export function languageServerHandler(info: any): string { export function isDebugModeEnabled(info: any): boolean { return info[ADDON_CONFIG_KEY].debug === true; } -export function hasEmberLanguageServerExtension(info: any) { - return ADDON_CONFIG_KEY in info; -} diff --git a/src/utils/layout-helpers.ts b/src/utils/layout-helpers.ts index 69865f0a..2c62e4f5 100644 --- a/src/utils/layout-helpers.ts +++ b/src/utils/layout-helpers.ts @@ -8,6 +8,8 @@ import { CompletionItem, CompletionItemKind } from 'vscode-languageserver'; type GLOBAL_REGISTRY_ITEM = Map>; export type REGISTRY_KIND = 'transform' | 'helper' | 'component' | 'routePath' | 'model' | 'service' | 'modifier'; +export const ADDON_CONFIG_KEY = 'ember-language-server'; + const GLOBAL_REGISTRY: { transform: GLOBAL_REGISTRY_ITEM; helper: GLOBAL_REGISTRY_ITEM; @@ -30,6 +32,10 @@ export function getGlobalRegistry() { return GLOBAL_REGISTRY; } +export function hasEmberLanguageServerExtension(info: PackageInfo) { + return info[ADDON_CONFIG_KEY] !== undefined; +} + export function addToRegistry(normalizedName: string, kind: REGISTRY_KIND, files: string[]) { if (!GLOBAL_REGISTRY[kind].has(normalizedName)) { GLOBAL_REGISTRY[kind].set(normalizedName, new Set()); @@ -62,11 +68,17 @@ export const isAddonRoot = memoize(isProjectAddonRoot, { maxAge: 600000 }); -interface PackageInfo { +export interface PackageInfo { keywords?: string[]; + name?: string; 'ember-language-server'?: {}; + peerDependencies?: {}; + devDependencies?: {}; + dependencies?: {}; 'ember-addon'?: { version?: number; + before?: string | string[]; + after?: string | string[]; }; } @@ -223,7 +235,6 @@ export function isGlimmerXProject(root: string) { } export function getProjectAddonsRoots(root: string, resolvedItems: string[] = [], packageFolderName = 'node_modules') { - // log('getProjectAddonsInfo', root); const pack = getPackageJSON(root); if (resolvedItems.length) { if (!isEmberAddon(pack)) { @@ -245,6 +256,11 @@ export function getProjectAddonsRoots(root: string, resolvedItems: string[] = [] }); const recursiveRoots: string[] = resolvedItems.slice(0); roots.forEach((rootItem: string) => { + let packInfo = getPackageJSON(rootItem); + // we don't need to go deeper if package itself not an ember-addon or els-extension + if (!isEmberAddon(packInfo) && !hasEmberLanguageServerExtension(packInfo)) { + return; + } if (!recursiveRoots.includes(rootItem)) { recursiveRoots.push(rootItem); getProjectAddonsRoots(rootItem, recursiveRoots, packageFolderName).forEach((item: string) => { @@ -257,7 +273,7 @@ export function getProjectAddonsRoots(root: string, resolvedItems: string[] = [] return recursiveRoots; } -export function getPackageJSON(file: string) { +export function getPackageJSON(file: string): PackageInfo { try { const result = JSON.parse(fs.readFileSync(path.join(file, 'package.json'), 'utf8')); return result; diff --git a/test/__snapshots__/integration-test.ts.snap b/test/__snapshots__/integration-test.ts.snap index 69b81fa6..8d44e26b 100644 --- a/test/__snapshots__/integration-test.ts.snap +++ b/test/__snapshots__/integration-test.ts.snap @@ -2683,6 +2683,12 @@ Object { }, "referencesProvider": true, "textDocumentSync": 1, + "workspace": Object { + "workspaceFolders": Object { + "changeNotifications": true, + "supported": true, + }, + }, }, } `;