From 77ce0b9f3f458b59b7385299d59f0020a094df3f Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Sat, 24 Feb 2024 11:38:26 +0100 Subject: [PATCH] Show unregistered controller identifiers in Tree View --- client/src/controller_tree_view.ts | 98 +++++++++++++++++++++++------- client/src/requests.ts | 23 ++++++- server/src/requests.ts | 23 ++++++- server/src/server.ts | 63 +++++++++++++++++-- 4 files changed, 174 insertions(+), 33 deletions(-) diff --git a/client/src/controller_tree_view.ts b/client/src/controller_tree_view.ts index 5c3a828..217ce9e 100644 --- a/client/src/controller_tree_view.ts +++ b/client/src/controller_tree_view.ts @@ -14,26 +14,13 @@ import * as vscode from "vscode" import { Client } from "./client" -class ControllerTreeItem extends TreeItem { - constructor(identifier: string, path: string) { - super(identifier, TreeItemCollapsibleState.None) +import type { ControllerDefinition, ControllerDefinitionsResponse, ControllerDefinitionsOrigin } from "./requests" - this.tooltip = path - this.id = `${path}-${identifier}` - this.iconPath = new ThemeIcon("outline-view-icon") - this.resourceUri = Uri.parse(`file://${path}`) +type ControllerDefinitionTreeItem = ControllerTreeItem | ControllerDefinitionsStateItem - this.command = { - command: "vscode.open", - title: "Open", - arguments: [this.resourceUri], - } - } -} - -export class ControllerTreeView implements TreeDataProvider, Disposable { +export class ControllerTreeView implements TreeDataProvider, Disposable { private client: Client - private readonly treeView: TreeView + private readonly treeView: TreeView private readonly subscriptions: Disposable[] = [] private _onDidChangeTreeData: EventEmitter = new EventEmitter() readonly onDidChangeTreeData: Event = this._onDidChangeTreeData.event @@ -60,21 +47,86 @@ export class ControllerTreeView implements TreeDataProvider, this.treeView.dispose() } - getTreeItem(element: ControllerTreeItem) { + getTreeItem(element: ControllerDefinitionTreeItem) { return element } - getChildren(_element?: ControllerTreeItem) { - return this.requestControllerDefinitions() + async getChildren(element?: ControllerDefinitionTreeItem) { + if (element) { + return element.getChildren() + } else { + const response = await this.requestControllerDefinitions() + + return [ + new ControllerDefinitionsStateItem("Unregistered", [response.unregistered.project, ...response.unregistered.nodeModules]), + new ControllerDefinitionsStateItem("Registered", [response.registered]), + ] + } } refresh() { this._onDidChangeTreeData.fire(undefined) } - private async requestControllerDefinitions(): Promise { - const controllerDefinitions = await this.client.requestControllerDefinitions() - return controllerDefinitions.map(({ path, identifier }) => new ControllerTreeItem(identifier, path)) + private async requestControllerDefinitions(): Promise { + return await this.client.requestControllerDefinitions() + } +} + +class ControllerDefinitionsStateItem extends TreeItem { + public children: ControllerDefinitionsOrigin[] = [] + + constructor(name: string, children: ControllerDefinitionsOrigin[]) { + const collapisbleState = name === "Registered" ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.Collapsed + + super(name, collapisbleState) + + this.tooltip = name + this.children = children + + const controllersCount = this.children.flatMap(c => c.controllerDefinitions).length + this.description = `(${controllersCount} controller${controllersCount == 1 ? "" : "s"})` + } + + getChildren() { + return this.controllerTreeItems.sort((a, b) => a.label.toString().localeCompare(b.label.toString())) + } + + private get controllerTreeItems() { + return this.controllerDefinitions.flatMap(([definition, child]) => new ControllerTreeItem(definition, child)) + } + + private get controllerDefinitions(): [ControllerDefinition, ControllerDefinitionsOrigin][] { + return this.children.map(child => child.controllerDefinitions.map(definition => [definition, child] as [ControllerDefinition, ControllerDefinitionsOrigin])).flat(1) + } +} + +class ControllerTreeItem extends TreeItem { + public registered: boolean = false + + constructor(item: ControllerDefinition, origin: ControllerDefinitionsOrigin) { + super(item.identifier, TreeItemCollapsibleState.None) + + this.id = `${item.path}-${item.identifier}-${item.registered}` + this.tooltip = item.path + this.registered = item.registered + this.iconPath = new ThemeIcon("outline-view-icon") + this.resourceUri = Uri.parse(`file://${item.path}`) + this.contextValue = `controllerDefinition-${item.registered ? "registered" : "unregistered"}` + + if (!item.registered) { + this.description = `(${origin.name})` + } + + this.command = { + command: "vscode.open", + title: "Open", + arguments: [this.resourceUri], + } + } + + getChildren() { + return [] } } diff --git a/client/src/requests.ts b/client/src/requests.ts index 5ebe2e2..d994ab7 100644 --- a/client/src/requests.ts +++ b/client/src/requests.ts @@ -1,7 +1,26 @@ -type ControllerDefinition = { +import { Position } from "vscode-languageclient" + +export type ControllerDefinition = { identifier: string path: string + registered: boolean + position: Position +} + +export interface ControllerDefinitionsOrigin { + name: string, + controllerDefinitions: ControllerDefinition[] +} + +export interface ProjectControllerDefinitions extends ControllerDefinitionsOrigin { + name: "project", } export type ControllerDefinitionsRequest = object -export type ControllerDefinitionsResponse = ControllerDefinition[] +export type ControllerDefinitionsResponse = { + registered: ProjectControllerDefinitions + unregistered: { + project: ProjectControllerDefinitions, + nodeModules: ControllerDefinitionsOrigin[] + } +} diff --git a/server/src/requests.ts b/server/src/requests.ts index 5ebe2e2..b2c3968 100644 --- a/server/src/requests.ts +++ b/server/src/requests.ts @@ -1,7 +1,26 @@ -type ControllerDefinition = { +import { Position } from "vscode-languageserver" + +export type ControllerDefinition = { identifier: string path: string + registered: boolean + position: Position +} + +export interface ControllerDefinitionsOrigin { + name: string, + controllerDefinitions: ControllerDefinition[] +} + +export interface ProjectControllerDefinitions extends ControllerDefinitionsOrigin { + name: "project", } export type ControllerDefinitionsRequest = object -export type ControllerDefinitionsResponse = ControllerDefinition[] +export type ControllerDefinitionsResponse = { + registered: ProjectControllerDefinitions + unregistered: { + project: ProjectControllerDefinitions, + nodeModules: ControllerDefinitionsOrigin[] + } +} diff --git a/server/src/server.ts b/server/src/server.ts index 4dce4dd..912a3ed 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -7,12 +7,18 @@ import { TextDocumentSyncKind, InitializeResult, Diagnostic, + Position, } from "vscode-languageserver/node" import { Service } from "./service" import { StimulusSettings } from "./settings" +import { RegisteredController, ControllerDefinition } from "stimulus-parser" -import type { ControllerDefinitionsRequest, ControllerDefinitionsResponse } from "./requests" +import type { + ControllerDefinition as ControllerDefinitionRequestType, + ControllerDefinitionsRequest, + ControllerDefinitionsResponse, +} from "./requests" let service: Service const connection = createConnection(ProposedFeatures.all) @@ -134,14 +140,59 @@ connection.onCompletionResolve((item) => { connection.onRequest( "stimulus-lsp/controllerDefinitions", async (_request: ControllerDefinitionsRequest): Promise => { - const controllerDefinitions = service.project.registeredControllers.sort((a, b) => - a.identifier.localeCompare(b.identifier), - ) + const sort = (a: ControllerDefinitionRequestType, b: ControllerDefinitionRequestType) => + a.identifier.localeCompare(b.identifier) - return controllerDefinitions.map(({ path, identifier }) => ({ + const mapRegisteredController = ({ path, identifier, classDeclaration: { node } }: RegisteredController) => ({ path, identifier, - })) + registered: true, + position: Position.create(node?.loc?.start.line || 1, node?.loc?.start.column || 1), + }) + + const mapControllerDefinition = ({ + path, + guessedIdentifier, + classDeclaration: { node }, + }: ControllerDefinition) => ({ + path, + identifier: guessedIdentifier, + registered: false, + position: Position.create(node?.loc?.start.line || 1, node?.loc?.start.column || 1), + }) + + const registeredControllerPaths = service.project.registeredControllers.map((c) => c.path) + const unregisteredControllerDefinitions = service.project.controllerDefinitions.filter( + (definition) => !registeredControllerPaths.includes(definition.path), + ) + + const registered = service.project.registeredControllers.map(mapRegisteredController).sort(sort) + const unregistered = unregisteredControllerDefinitions.map(mapControllerDefinition).sort(sort) + + const nodeModules = service.project.detectedNodeModules + .map(({ name, controllerDefinitions }) => ({ + name, + controllerDefinitions: controllerDefinitions + .filter((definition) => !registeredControllerPaths.includes(definition.path)) + .map(mapControllerDefinition) + .sort(sort), + })) + .filter((m) => m.controllerDefinitions.length > 0) + .sort((a, b) => a.name.localeCompare(b.name)) + + return { + registered: { + name: "project", + controllerDefinitions: registered, + }, + unregistered: { + project: { + name: "project", + controllerDefinitions: unregistered, + }, + nodeModules, + }, + } }, )