Full Code of marcoroth/stimulus-lsp for AI

main ec584d90f48a cached
50 files
116.3 KB
28.0k tokens
198 symbols
1 requests
Download .txt
Repository: marcoroth/stimulus-lsp
Branch: main
Commit: ec584d90f48a
Files: 50
Total size: 116.3 KB

Directory structure:
gitextract_y0vvpjxj/

├── .eslintignore
├── .eslintrc
├── .github/
│   ├── dependabot.yml
│   └── workflows/
│       ├── build.yml
│       └── publish.yml
├── .gitignore
├── .node-version
├── .prettierrc.json
├── .vscode/
│   ├── extensions.json
│   ├── launch.json
│   ├── settings.json
│   └── tasks.json
├── .vscodeignore
├── LICENSE.txt
├── README.md
├── client/
│   ├── package.json
│   ├── src/
│   │   ├── client.ts
│   │   ├── controller_tree_view.ts
│   │   ├── extension.ts
│   │   └── requests.ts
│   └── tsconfig.json
├── package.json
├── scripts/
│   └── e2e.sh
├── server/
│   ├── .npmignore
│   ├── README.md
│   ├── package.json
│   ├── scripts/
│   │   └── executable.mjs
│   ├── src/
│   │   ├── action_descriptor.ts
│   │   ├── code_actions.ts
│   │   ├── code_lens.ts
│   │   ├── commands.ts
│   │   ├── config.ts
│   │   ├── data_providers/
│   │   │   └── stimulus_html_data_provider.ts
│   │   ├── definitions.ts
│   │   ├── diagnostics.ts
│   │   ├── document_service.ts
│   │   ├── events.ts
│   │   ├── html_util.ts
│   │   ├── levenshtein.ts
│   │   ├── requests/
│   │   │   └── controller_definitions.ts
│   │   ├── requests.ts
│   │   ├── server.ts
│   │   ├── service.ts
│   │   ├── settings.ts
│   │   └── utils.ts
│   ├── tsconfig.json
│   └── types/
│       ├── typescript-eslint__typescript-estree/
│       │   └── index.d.ts
│       ├── typescript-eslint__typescript-types/
│       │   └── index.d.ts
│       └── typescript-eslint__visitor-keys/
│           └── index.d.ts
└── tsconfig.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .eslintignore
================================================
node_modules/**
client/node_modules/**
client/out/**
server/node_modules/**
server/out/**
**/*.d.ts


================================================
FILE: .eslintrc
================================================
{
  "root": true,
  "parser": "@typescript-eslint/parser",
  "plugins": [
    "@typescript-eslint",
    "prettier"
  ],
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:@typescript-eslint/recommended",
    "prettier"
  ],
  "rules": {
    "prettier/prettier": ["error"],
    "@typescript-eslint/no-explicit-any": "off",
    "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }]
  },
  "env": {
    "node": true
  }
}


================================================
FILE: .github/dependabot.yml
================================================
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates

version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "daily"
    groups:
      typescript-eslint:
        patterns:
          - "@typescript-eslint*"

      eslint:
        patterns:
          - "eslint"
          - "eslint-config-*"
          - "eslint-plugin-*"
          - "prettier-*"

  - package-ecosystem: "npm"
    directory: "/server"
    schedule:
      interval: "daily"
    groups:
      vscode:
        patterns:
          - "vscode*"

  - package-ecosystem: "npm"
    directory: "/client"
    schedule:
      interval: "daily"


================================================
FILE: .github/workflows/build.yml
================================================
name: Build

permissions:
  contents: read

on: [push, pull_request]

jobs:
  tests:
    name: JavaScript Test Action
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node: [20]

    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node }}
          cache: 'yarn'

      - name: Yarn install
        run: yarn install

      - name: Yarn build
        run: yarn build


================================================
FILE: .github/workflows/publish.yml
================================================
name: Publish

permissions:
  contents: read

on:
  release:
    types:
      - created

jobs:
  publish:
    name: Publish VSCode extension
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: 20
          cache: 'yarn'

      - name: Yarn install
        run: yarn install

      - name: Yarn build
        run: yarn build

      - name: Publish
        run: yarn run deploy
        env:
          VSCE_PAT: ${{ secrets.VSCE_PAT }}


================================================
FILE: .gitignore
================================================
client/out/
server/out/
node_modules/

.vscode-test/

*.tsbuildinfo
*.vsix
*.tgz
*~

.DS_Store


================================================
FILE: .node-version
================================================
20.14.0


================================================
FILE: .prettierrc.json
================================================
 {
   "singleQuote": false,
   "printWidth": 120,
   "semi": false
 }


================================================
FILE: .vscode/extensions.json
================================================
{
  // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
  // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp

  // List of extensions which should be recommended for users of this workspace.
  "recommendations": [
    "dbaeumer.vscode-eslint"
  ]
}


================================================
FILE: .vscode/launch.json
================================================
// A launch configuration that compiles the extension and then opens it inside a new window
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "extensionHost",
      "request": "launch",
      "name": "Launch Client",
      "runtimeExecutable": "${execPath}",
      "args": ["--extensionDevelopmentPath=${workspaceRoot}"],
      "outFiles": ["${workspaceRoot}/client/out/**/*.js"],
      "preLaunchTask": {
        "type": "npm",
        "script": "watch"
      }
    },
    {
      "type": "node",
      "request": "attach",
      "name": "Attach to Server",
      "port": 6009,
      "restart": true,
      "outFiles": ["${workspaceRoot}/server/out/**/*.js"]
    },
    {
      "name": "Language Server E2E Test",
      "type": "extensionHost",
      "request": "launch",
      "runtimeExecutable": "${execPath}",
      "args": [
        "--extensionDevelopmentPath=${workspaceRoot}",
        "--extensionTestsPath=${workspaceRoot}/client/out/test/index",
        "${workspaceRoot}/client/testFixture"
      ],
      "outFiles": ["${workspaceRoot}/client/out/test/**/*.js"]
    }
  ],
  "compounds": [
    {
      "name": "Client + Server",
      "configurations": ["Launch Client", "Attach to Server"]
    }
  ]
}


================================================
FILE: .vscode/settings.json
================================================
{
  "editor.insertSpaces": false,
  "tslint.enable": true,
  "typescript.tsc.autoDetect": "off",
  "typescript.preferences.quoteStyle": "single",
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit"
  }
}


================================================
FILE: .vscode/tasks.json
================================================
{
  "version": "2.0.0",
  "tasks": [
    {
      "type": "npm",
      "script": "compile",
      "group": "build",
      "presentation": {
        "panel": "dedicated",
        "reveal": "never"
      },
      "problemMatcher": [
        "$tsc"
      ]
    },
    {
      "type": "npm",
      "script": "watch",
      "isBackground": true,
      "group": {
        "kind": "build",
        "isDefault": true
      },
      "presentation": {
        "panel": "dedicated",
        "reveal": "never"
      },
      "problemMatcher": [
        "$tsc-watch"
      ]
    }
  ]
}


================================================
FILE: .vscodeignore
================================================
.gitignore
.eslintrc
.eslintignore
.prettierrc.json
.node-version

**/*.ts
**/*.map
**/tsconfig.json
**/tsconfig.base.json

.vscode/**
.github/**
scripts/**

client/node_modules/**
!client/node_modules/vscode-jsonrpc/**
!client/node_modules/vscode-languageclient/**
!client/node_modules/vscode-languageserver-protocol/**
!client/node_modules/vscode-languageserver-types/**
!client/node_modules/brace-expansion/**
!client/node_modules/balanced-match/**
!client/node_modules/lru-cache/**
!client/node_modules/yallist/**
!client/node_modules/semver/**


================================================
FILE: LICENSE.txt
================================================
MIT License

Copyright (c) 2021 Marco Roth

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# Stimulus LSP

Intelligent Stimulus tooling for Visual Studio Code and other editors which support the Language Server Protocol.

![](/assets/stimulus-lsp.png)

## Functionality

Currently, this Language Server only works for HTML, though its utility extends to various file types such as ERB, PHP, or Blade files.

### Completions

* Data Attributes
* Completions for controller identifiers
* Completions for controller actions
* Completions for controller targets
* Completions for controller values
* Completions for controller classes

### Diagnostics

#### HTML Files

* Missing controllers (`stimulus.controller.invalid`)
* Missing controller actions (`stimulus.action.invalid`)
* Missing controller targets (`stimulus.controller.target.missing`)
* Missing controller values (`stimulus.controller.value.missing`)
* Invalid action descriptors (`stimulus.action.invalid`)
* Data attributes format mismatches (`stimulus.attribute.mismatch`)
* Controller values type mismatches (`stimulus.controller.value.type_mismatch`)

#### JavaScript Files/Stimulus Controller Files

* Controller value definition default value type mismatch (`stimulus.controller.value_definition.default_value.type_mismatch`)
* Unknown value definition type (`stimulus.controller.value_definition.unknown_type`)
* Controller parsing errors (`stimulus.controller.parse_error`)
* Import from deprecated packages (`stimulus.package.deprecated.import`)

### Quick-Fixes

* Create a controller with the given identifier (`stimulus.controller.create`)
* Update controller identifier with did you mean suggestion (`stimulus.controller.update`)
* Register a controller definition from your project or a NPM package (`stimulus.controller.register`)
* Update controller action name with did you mean suggestion (`stimulus.controller.action.update`)
* Implement a missing controller action on controller (`stimulus.controller.action.implement`)
* Create a default config file at `.stimulus-lsp/config.json` (`stimulus.config.create`)
* Ignore diagnostics for a HTML attribute by adding it to the `ignoredAttributes` config (`stimulus.config.attribute.ignore`)
* Ignore diagnostics for a Stimulus controller identifier by adding it to the `ignoredControllerIdentifiers` config (`stimulus.config.controller.ignore`)

## Structure

```
.
├── package.json // The extension manifest.
|
├── client // Language Client
│   └── src
│      └── extension.ts // Language Client entry point
|
└── server // Language Server
    └── src
        └── server.ts // Language Server entry point
```

## Running the extension locally

- Run `yarn install` in this folder. This installs all necessary npm modules in both the client and server folder
- Open VS Code on this folder.
- Press Ctrl+Shift+B to compile the client and server.
- Switch to the Debug viewlet.
- Select `Launch Client` from the drop down.
- Run the launch config.
- If you want to debug the server as well use the launch configuration `Attach to Server`
- In the [Extension Development Host] instance of VSCode, open a HTML file.
  - Type `<div data-controller="|">`, place your cursor where the `|` is, hit Ctrl+Space and you should see completions.

## Install instructions

### VS Code

Install the [Stimulus LSP extension](https://marketplace.visualstudio.com/items?itemName=marcoroth.stimulus-lsp) from the Visual Studio Marketplace.

### Neovim

[Install instructions can be found at nvim-lspconfig](https://github.com/neovim/nvim-lspconfig/blob/master/doc/configs.md#stimulus_ls)

### Zed

Install the [Zed Stimulus](https://github.com/vitallium/zed-stimulus) extension.


================================================
FILE: client/package.json
================================================
{
  "name": "vscode-stimulus",
  "description": "Intelligent Stimulus tooling for Visual Studio Code",
  "author": "Marco Roth",
  "license": "MIT",
  "version": "1.1.0",
  "publisher": "Marco Roth",
  "repository": {
    "type": "git",
    "url": "https://github.com/marcoroth/stimulus-lsp"
  },
  "engines": {
    "vscode": "^1.52.0"
  },
  "dependencies": {
    "brace-expansion": "^5.0.5",
    "minimatch": "^10.2.5",
    "typescript": "^5.9.3",
    "vscode-languageclient": "^9.0.1"
  },
  "devDependencies": {
    "@types/vscode": "^1.115.0"
  }
}


================================================
FILE: client/src/client.ts
================================================
import * as path from "path"

import { workspace, ExtensionContext } from "vscode"
import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind } from "vscode-languageclient/node"

import { ControllerTreeView } from "./controller_tree_view"
import type { ControllerDefinitionsResponse } from "./requests"

export class Client {
  private client: LanguageClient
  private serverModule: string
  private languageClientId = "languageServerStimulus"
  private languageClientName = "Stimulus LSP"
  private context: ExtensionContext

  constructor(context: ExtensionContext) {
    this.context = context

    this.serverModule = context.asAbsolutePath(path.join("server", "out", "server.js"))

    this.client = new LanguageClient(
      this.languageClientId,
      this.languageClientName,
      this.serverOptions,
      this.clientOptions,
    )
  }

  async start() {
    try {
      this.client.start()
      this.context.subscriptions.push(new ControllerTreeView(this))
    } catch (error: any) {
      console.error(`Error restarting the server: ${error.message}`)
      return
    }
  }

  async stop(): Promise<void> {
    if (this.client) {
      await this.client.stop()
    }
  }

  async sendNotification(method: string, params: any) {
    return await this.client.sendNotification(method, params)
  }

  async sendRequest<T>(method: string, params: any) {
    return await this.client.sendRequest<T>(method, params)
  }

  async requestControllerDefinitions(): Promise<ControllerDefinitionsResponse> {
    return await this.sendRequest<ControllerDefinitionsResponse>("stimulus-lsp/controllerDefinitions", {})
  }

  // The debug options for the server
  // --inspect=6009: runs the server in Node's Inspector mode so VS Code can attach to the server for debugging
  private get debugOptions() {
    return {
      execArgv: ["--nolazy", "--inspect=6009"],
    }
  }

  // If the extension is launched in debug mode then the debug server options are used
  // Otherwise the run options are used
  private get serverOptions(): ServerOptions {
    return {
      run: {
        module: this.serverModule,
        transport: TransportKind.ipc,
      },
      debug: {
        module: this.serverModule,
        transport: TransportKind.ipc,
        options: this.debugOptions,
      },
    }
  }

  private get clientOptions(): LanguageClientOptions {
    return {
      documentSelector: [
        { scheme: "file", language: "ruby" },
        { scheme: "file", language: "erb" },
        { scheme: "file", language: "blade" },
        { scheme: "file", language: "php" },
        { scheme: "file", language: "html" },
        { scheme: "file", language: "javascript" },
        { scheme: "file", language: "typescript" },
      ],
      synchronize: {
        // Notify the server about file changes to '.clientrc files contained in the workspace
        fileEvents: workspace.createFileSystemWatcher("**/.clientrc"),
      },
    }
  }
}


================================================
FILE: client/src/controller_tree_view.ts
================================================
import {
  TreeView,
  TreeItem,
  TreeItemCollapsibleState,
  TreeDataProvider,
  Disposable,
  ThemeIcon,
  EventEmitter,
  Uri,
  Event,
} from "vscode"

import * as vscode from "vscode"

import { Client } from "./client"

import type { ControllerDefinition, ControllerDefinitionsResponse, ControllerDefinitionsOrigin } from "./requests"

type ControllerDefinitionTreeItem = ControllerTreeItem | ControllerDefinitionsStateItem

export class ControllerTreeView implements TreeDataProvider<ControllerDefinitionTreeItem>, Disposable {
  private client: Client
  private readonly treeView: TreeView<ControllerDefinitionTreeItem>
  private readonly subscriptions: Disposable[] = []
  private _onDidChangeTreeData: EventEmitter<any> = new EventEmitter<any>()
  readonly onDidChangeTreeData: Event<any> = this._onDidChangeTreeData.event

  constructor(client: Client) {
    this.client = client

    this.treeView = vscode.window.createTreeView("controllerDefinitions", {
      treeDataProvider: this,
      showCollapseAll: true,
    })

    vscode.commands.registerCommand("controllerDefinitions.refreshEntry", () => this.refresh())
    vscode.commands.registerCommand("controllerDefinitions.registerControllerDefinition", (item) =>
      this.registerControllerDefinition(item),
    )

    this.subscriptions.push(
      this.treeView.onDidChangeVisibility(() => this.refresh()),
      vscode.workspace.onDidRenameFiles(() => this.refresh()),
      vscode.workspace.onDidSaveTextDocument(() => this.refresh()),
    )
  }

  dispose() {
    this.subscriptions.forEach((item) => item.dispose())
    this.treeView.dispose()
  }

  getTreeItem(element: ControllerDefinitionTreeItem) {
    return element
  }

  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)
  }

  registerControllerDefinition(item: ControllerTreeItem) {
    if (item.isImportable) {
      this.client.sendRequest("workspace/executeCommand", {
        command: "stimulus.controller.register",
        arguments: [
          item.controllerDefinition.importStatement,
          item.controllerDefinition.identifier,
          item.controllerDefinition.localName,
        ],
      })
    }
  }

  private async requestControllerDefinitions(): Promise<ControllerDefinitionsResponse> {
    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
  public controllerDefinition: ControllerDefinition

  constructor(item: ControllerDefinition, origin: ControllerDefinitionsOrigin) {
    super(item.identifier, TreeItemCollapsibleState.None)

    this.controllerDefinition = item
    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"}${this.isImportable ? "importable" : "non-importable"}`

    if (!item.registered) {
      this.description = `(${origin.name})`
    }

    this.command = {
      command: "vscode.open",
      title: "Open",
      arguments: [this.resourceUri],
    }
  }

  get isImportable() {
    return (
      !!this.controllerDefinition.importStatement &&
      !!this.controllerDefinition.identifier &&
      !!this.controllerDefinition.localName
    )
  }

  getChildren() {
    return []
  }
}


================================================
FILE: client/src/extension.ts
================================================
import { ExtensionContext } from "vscode"
import { Client } from "./client"

let client: Client

export async function activate(context: ExtensionContext) {
  client = new Client(context)

  await client.start()
}

export async function deactivate(): Promise<void> {
  if (client) {
    await client.stop()
  } else {
    return undefined
  }
}


================================================
FILE: client/src/requests.ts
================================================
import { Position } from "vscode-languageclient"

export type ControllerDefinition = {
  identifier: string
  path: string
  registered: boolean
  position: Position
  importStatement?: string
  localName?: string
}

export interface ControllerDefinitionsOrigin {
  name: string
  controllerDefinitions: ControllerDefinition[]
}

export interface ProjectControllerDefinitions extends ControllerDefinitionsOrigin {
  name: "project"
}

export type ControllerDefinitionsRequest = object
export type ControllerDefinitionsResponse = {
  registered: ProjectControllerDefinitions
  unregistered: {
    project: ProjectControllerDefinitions
    nodeModules: ControllerDefinitionsOrigin[]
  }
}


================================================
FILE: client/tsconfig.json
================================================
{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es2019",
    "lib": ["ES2019"],
    "outDir": "out",
    "rootDir": "src",
    "sourceMap": true,
    "skipLibCheck": true,
    "erasableSyntaxOnly": true
  },
  "include": ["src"],
  "exclude": ["node_modules"]
}


================================================
FILE: package.json
================================================
{
  "name": "stimulus-lsp",
  "displayName": "Stimulus LSP",
  "description": "Intelligent Stimulus tooling",
  "license": "MIT",
  "pricing": "Free",
  "version": "1.1.0",
  "icon": "icon.png",
  "publisher": "marcoroth",
  "author": {
    "name": "Marco Roth"
  },
  "categories": [
    "Programming Languages",
    "Language Packs",
    "Linters"
  ],
  "keywords": [
    "Stimulus",
    "Hotwire",
    "Ruby on Rails"
  ],
  "sponsor": {
    "url": "http://github.com/sponsors/marcoroth"
  },
  "repository": "https://github.com/marcoroth/stimulus-lsp",
  "engines": {
    "vscode": "^1.43.0"
  },
  "extensionDependencies": [
    "marcoroth.herb-lsp"
  ],
  "activationEvents": [
    "onLanguage:ruby",
    "onLanguage:erb",
    "onLanguage:blade",
    "onLanguage:php",
    "onLanguage:html",
    "onLanguage:javascript",
    "onLanguage:typescript",
    "onView:controllerDefinitions"
  ],
  "main": "./client/out/extension",
  "contributes": {
    "configurationDefaults": {
      "[html]": {
        "editor.quickSuggestions": {
          "strings": "on"
        }
      },
      "[erb]": {
        "editor.quickSuggestions": {
          "strings": "on"
        }
      }
    },
    "configuration": {
      "type": "object",
      "title": "Stimulus configuration",
      "properties": {
        "languageServerStimulus.trace.server": {
          "scope": "window",
          "type": "string",
          "enum": [
            "off",
            "messages",
            "verbose"
          ],
          "default": "verbose",
          "description": "Traces the communication between VS Code and the language server."
        }
      }
    },
    "views": {
      "explorer": [
        {
          "id": "controllerDefinitions",
          "name": "Stimulus Controllers",
          "icon": "assets/stimulus.svg",
          "description": "View and inspect detected Stimulus Controllers",
          "contextualTitle": "Stimulus Controllers"
        }
      ]
    },
    "commands": [
      {
        "command": "controllerDefinitions.refreshEntry",
        "title": "Refresh Stimulus Controller Definitions",
        "icon": "$(refresh)"
      },
      {
        "command": "controllerDefinitions.registerControllerDefinition",
        "title": "Register controller definition on the Stimulus Application",
        "icon": "$(add)"
      }
    ],
    "menus": {
      "view/title": [
        {
          "command": "controllerDefinitions.refreshEntry",
          "when": "view == controllerDefinitions",
          "group": "navigation"
        }
      ],
      "view/item/context": [
        {
          "command": "controllerDefinitions.registerControllerDefinition",
          "when": "view == controllerDefinitions && viewItem == controllerDefinition-unregistered-importable",
          "group": "inline"
        }
      ]
    },
    "viewsWelcome": [
      {
        "view": "controllerDefinitions",
        "contents": "No Stimulus Controller found [learn more](https://stimulus.hotwired.dev/handbook/installing)."
      }
    ]
  },
  "scripts": {
    "vscode:prepublish": "yarn run build",
    "prebuild": "yarn run clean",
    "clean": "yarn rimraf client/out && yarn rimraf server/out",
    "deploy": "vsce publish --yarn",
    "build": "tsc -b",
    "watch": "tsc -b -w",
    "lint": "eslint client/**/*.ts server/**/*.ts --no-ignore",
    "format": "yarn lint --fix",
    "postinstall": "cd client && yarn install && cd ../server && yarn install && cd ..",
    "test": "sh ./scripts/e2e.sh"
  },
  "devDependencies": {
    "@types/node": "^25.6.0",
    "@typescript-eslint/eslint-plugin": "^8.0.0",
    "@typescript-eslint/parser": "^8.0.0",
    "@vscode/vsce": "^3.0.0",
    "eslint": "^10.0.0",
    "eslint-config-prettier": "^10.0.1",
    "eslint-plugin-prettier": "^5.0.1",
    "prettier": "^3.1.0",
    "rimraf": "^6.0.0",
    "typescript": "^5.8.2"
  }
}


================================================
FILE: scripts/e2e.sh
================================================
#!/usr/bin/env bash

export CODE_TESTS_PATH="$(pwd)/client/out/test"
export CODE_TESTS_WORKSPACE="$(pwd)/client/testFixture"

node "$(pwd)/client/out/test/runTest"

================================================
FILE: server/.npmignore
================================================
.babelrc
.babelrc.js
.DS_Store
.gitignore
.yarn.lock

*.log
*.tsbuildinfo
*.tgz

README.md
rollup.config.js
tsconfig.json
yarn-error.log
*~

/.git
/.github
/.gitattributes

/node_modules
/src
/test
/coverage
/assets


================================================
FILE: server/README.md
================================================
# Stimulus Language Server

[Language Server Protocol](https://github.com/Microsoft/language-server-protocol) implementation for [Stimulus](https://stimulus.hotwired.dev), used by [Stimulus LSP for VS Code](https://marketplace.visualstudio.com/items?itemName=marcoroth.stimulus-lsp).

## Install

```bash
npm install -g stimulus-language-server
```

```bash
yarn global add stimulus-language-server
```

## Run

```bash
stimulus-language-server --stdio
```

```
Usage: stimulus-language-server [options]

Options:

  --stdio          use stdio
  --node-ipc       use node-ipc
  --socket=<port>  use socket
```


================================================
FILE: server/package.json
================================================
{
  "name": "stimulus-language-server",
  "description": "Intelligent Stimulus tooling",
  "version": "1.1.0",
  "author": "Marco Roth",
  "license": "MIT",
  "engines": {
    "node": "*"
  },
  "bugs": "https://github.com/marcoroth/stimulus-lsp/issues",
  "repository": "https://github.com/marcoroth/stimulus-lsp",
  "homepage": "https://hotwire.io/ecosystem/tooling/stimulus-lsp",
  "bin": {
    "stimulus-language-server": "./out/stimulus-language-server"
  },
  "scripts": {
    "clean": "rimraf out",
    "prebuild": "yarn run clean",
    "build": "tsc -b",
    "postbuild": "node scripts/executable.mjs",
    "watch": "tsc -b -w"
  },
  "files": [
    "out"
  ],
  "dependencies": {
    "@hotwired/stimulus": "https://github.com/hotwired/dev-builds/archive/refs/tags/@hotwired/stimulus/8cbca6d.tar.gz",
    "dedent": "^1.5.1",
    "stimulus-parser": "^0.3.2",
    "typescript": "^5.8.2",
    "@herb-tools/core": "0.9.5",
    "@herb-tools/language-service": "0.9.5",
    "@herb-tools/node-wasm": "0.9.5",
    "vscode-html-languageservice": "^5.1.1",
    "vscode-languageserver": "^9.0.1",
    "vscode-languageserver-textdocument": "^1.0.1"
  },
  "devDependencies": {
    "@types/estree": "^1.0.5",
    "acorn": "^8.11.3",
    "astring": "^1.8.6",
    "rimraf": "^6.0.0",
    "source-map": "^0.7.4"
  }
}


================================================
FILE: server/scripts/executable.mjs
================================================
import { readFileSync, writeFileSync } from 'fs'
import { dirname, resolve } from 'path'
import { fileURLToPath } from 'url'
import { exec } from 'child_process'

const __dirname = dirname(fileURLToPath(import.meta.url))

const infile = resolve(__dirname, '../out/server.js')
const outfile = resolve(__dirname, '../out/stimulus-language-server')

writeFileSync(
  outfile,
  '#!/usr/bin/env node\n' + readFileSync(infile, 'utf-8'),
  'utf-8'
)

exec('chmod +x out/stimulus-language-server', (error, _stdout, _stderr) => {
  if (error) {
    console.error(`Error setting file permissions: ${error}`);
  } else {
    console.log('File permissions set successfully');
  }
});


================================================
FILE: server/src/action_descriptor.ts
================================================
// https://github.com/hotwired/stimulus/blob/8cbca6db3b1b2ddb384deb3dd98397d3609d25a0/src/core/action_descriptor.ts

export interface ActionDescriptor {
  eventTarget: string
  eventOptions: AddEventListenerOptions
  eventName: string
  identifier: string
  methodName: string
  keyFilter: string
}

// capture nos.:                  1      1    2   2     3   3      4               4      5   5    6      6     7  7
const descriptorPattern = /^(?:(?:([^.]+?)\+)?(.+?)(?:\.(.+?))?(?:@(window|document))?->)?(.+?)(?:#([^:]+?))(?::(.+))?$/

export function parseActionDescriptorString(descriptorString: string): Partial<ActionDescriptor> {
  const source = descriptorString.trim()
  const matches = source.match(descriptorPattern) || []
  let eventName = matches[2]
  let keyFilter = matches[3]

  if (keyFilter && !["keydown", "keyup", "keypress"].includes(eventName)) {
    eventName += `.${keyFilter}`
    keyFilter = ""
  }

  return {
    eventTarget: matches[4],
    eventName,
    eventOptions: matches[7] ? parseEventOptions(matches[7]) : {},
    identifier: matches[5],
    methodName: matches[6],
    keyFilter: matches[1] || keyFilter,
  }
}

function parseEventOptions(eventOptions: string): AddEventListenerOptions {
  return eventOptions
    .split(":")
    .reduce((options, token) => Object.assign(options, { [token.replace(/^!/, "")]: !/^!/.test(token) }), {})
}


================================================
FILE: server/src/code_actions.ts
================================================
import { CodeAction, CodeActionKind, CodeActionParams, Command, Diagnostic } from "vscode-languageserver/node"

import { DocumentService } from "./document_service"
import {
  InvalidActionDiagnosticData,
  InvalidControllerDiagnosticData,
  DeprecatedPackageImportsDiagnosticData,
} from "./diagnostics"
import { importStatementForController } from "./utils"

import { Project } from "stimulus-parser"

export class CodeActions {
  private readonly documentService: DocumentService
  private readonly project: Project

  constructor(documentService: DocumentService, project: Project) {
    this.documentService = documentService
    this.project = project
  }

  onCodeAction(params: CodeActionParams): CodeAction[] {
    const { diagnostics } = params.context
    if (diagnostics.length === 0) return []

    const textDocument = this.documentService.get(params.textDocument.uri)
    if (textDocument === undefined) return []

    const invalidControllerDiagnostics = diagnostics.filter((d) => d.code === "stimulus.controller.invalid")
    const invalidActionDiagnostics = diagnostics.filter((d) => d.code === "stimulus.controller.action.invalid")
    const deprecatedPackageImports = diagnostics.filter((d) => d.code === "stimulus.package.deprecated.import")

    return [
      ...this.handleInvalidControllerDiagnostics(invalidControllerDiagnostics),
      ...this.handleInvalidActionDiagnostics(invalidActionDiagnostics),
      ...this.handleDeprecatedPackageImports(deprecatedPackageImports),
    ]
  }

  private handleInvalidControllerDiagnostics(diagnostics: Diagnostic[]) {
    return diagnostics.flatMap((diagnostic) => {
      const codeActions: CodeAction[] = []
      const { identifier, suggestion } = diagnostic.data as InvalidControllerDiagnosticData

      // Code Action: stimulus.package.deprecated.import

      if (diagnostic.code === "stimulus.package.deprecated.import") {
        const updateImport = `Replace "${identifier}" with suggestion: "${suggestion}"`
        const updateDeprecatedImport = CodeAction.create(
          updateImport,
          Command.create(
            updateImport,
            "stimulus.package.deprecated.controller.update",
            identifier,
            diagnostic,
            suggestion,
          ),
          CodeActionKind.QuickFix,
        )

        codeActions.push(updateDeprecatedImport)
      }

      // Code Action: stimulus.controller.update

      if (suggestion) {
        const updateTitle = `Replace "${identifier}" with suggestion: "${suggestion}"`
        const updateReferenceAction = CodeAction.create(
          updateTitle,
          Command.create(updateTitle, "stimulus.controller.update", identifier, diagnostic, suggestion),
          CodeActionKind.QuickFix,
        )

        codeActions.push(updateReferenceAction)
      }

      // Code Action: stimulus.controller.register

      if (identifier) {
        const projectControllers = this.project.projectFiles.flatMap((file) => file.exportedControllerDefinitions)

        const entrypointExports = this.project.detectedNodeModules.flatMap(
          (m) => m.entrypointSourceFile?.exportDeclarations || [],
        )
        const nodeModulesControllers = entrypointExports.flatMap((exportDeclaration) => {
          try {
            return exportDeclaration.nextResolvedClassDeclaration?.controllerDefinition || []
          } catch (error: any) {
            return []
          }
        })

        const controllers = projectControllers
          .concat(nodeModulesControllers)
          .filter((controller) => controller.guessedIdentifier === identifier)

        controllers.forEach((controller) => {
          const { localName, importStatement, importSource } = importStatementForController(controller, this.project)

          if (importStatement) {
            const registerTitle = `Register controller "${identifier}" from "${importSource}"`

            codeActions.push(
              CodeAction.create(
                registerTitle,
                Command.create(registerTitle, "stimulus.controller.register", importStatement, identifier, localName),
                CodeActionKind.QuickFix,
              ),
            )
          }
        })
      }

      // Code Action: stimulus.controller.create

      const controllerRootsInProject = this.project.controllerRoots.filter(
        (project) => !project.includes("node_modules"),
      )

      const manyRoots = controllerRootsInProject.length > 1

      if (controllerRootsInProject.length === 0) controllerRootsInProject.push(this.project.controllerRootFallback)

      const createControllerActions = controllerRootsInProject.map((root) => {
        const folder = `${manyRoots ? ` in "${root}/"` : ""}`
        const title = `Create "${identifier}" Stimulus Controller${folder}`

        return CodeAction.create(
          title,
          Command.create(title, "stimulus.controller.create", identifier, diagnostic, root),
          CodeActionKind.QuickFix,
        )
      })

      codeActions.push(...createControllerActions)

      // Code Action: stimulus.config.attribute.ignore

      const { attribute } = diagnostic?.data?.data || {}

      if (attribute) {
        const ignoreAttributeTitle = `Ignore diagnostics for "${attribute}" attribute.`

        const ignoreAttributeAction = CodeAction.create(
          ignoreAttributeTitle,
          Command.create(ignoreAttributeTitle, "stimulus.config.attribute.ignore", attribute, diagnostic),
          CodeActionKind.QuickFix,
        )

        codeActions.push(ignoreAttributeAction)
      }

      // Code Action: stimulus.config.controller.ignore

      const ignoreControllerTitle = `Ignore diagnostics for "${identifier}" controller.`

      const ignoreControllerAction = CodeAction.create(
        ignoreControllerTitle,
        Command.create(ignoreControllerTitle, "stimulus.config.controller.ignore", identifier, diagnostic),
        CodeActionKind.QuickFix,
      )

      codeActions.push(ignoreControllerAction)

      return codeActions
    })
  }

  private handleInvalidActionDiagnostics(diagnostics: Diagnostic[]) {
    return diagnostics.flatMap((diagnostic) => {
      const { actionName, suggestion, identifier } = diagnostic.data as InvalidActionDiagnosticData

      const updateTitle = `Replace "${actionName}" with suggestion: "${suggestion}"`

      const updateReferenceAction = CodeAction.create(
        updateTitle,
        Command.create(updateTitle, "stimulus.controller.action.update", actionName, diagnostic, suggestion),
        CodeActionKind.QuickFix,
      )

      const implementTitle = `Implement "${actionName}" action on "${identifier}" controller`

      const implementControllerAction = CodeAction.create(
        implementTitle,
        Command.create(implementTitle, "stimulus.controller.action.implement", actionName, identifier, diagnostic),
        CodeActionKind.QuickFix,
      )

      return [updateReferenceAction, implementControllerAction]
    })
  }

  private handleDeprecatedPackageImports(diagnostics: Diagnostic[]) {
    return diagnostics.flatMap((diagnostic) => {
      const codeActions: CodeAction[] = []
      const { identifier, suggestion } = diagnostic.data as DeprecatedPackageImportsDiagnosticData

      // Code Action: stimulus.package.deprecated.import

      const updateImport = `Replace "${identifier}" with suggestion: "${suggestion}"`
      const updateDeprecatedImport = CodeAction.create(
        updateImport,
        Command.create(updateImport, "stimulus.import.source.update", diagnostic),
        CodeActionKind.QuickFix,
      )

      codeActions.push(updateDeprecatedImport)

      return codeActions
    })
  }
}


================================================
FILE: server/src/code_lens.ts
================================================
import { CodeLens, CodeLensParams, Range, Command } from "vscode-languageserver/node"

import { DocumentService } from "./document_service"

import type { Project } from "stimulus-parser"

export class CodeLensProvider {
  private readonly documentService: DocumentService
  private readonly project: Project

  constructor(documentService: DocumentService, project: Project) {
    this.documentService = documentService
    this.project = project
  }

  onCodeLens(params: CodeLensParams) {
    const textDocument = this.documentService.get(params.textDocument.uri)

    if (!textDocument) return []

    const file = this.project.projectFiles.find((file) => `file://${file.path}` === textDocument.uri)

    if (!file) return []
    if (file.controllerDefinitions.length === 0) return []

    return file.controllerDefinitions.flatMap((definition) => {
      const loc = definition.classDeclaration.node?.loc

      if (!loc) return []

      const registeredController = this.project.registeredControllers.find(
        (registered) => registered.controllerDefinition === definition,
      )

      const range = Range.create(loc.start.line - 1, loc.start.column, loc.end.line - 1, loc.start.column)

      if (registeredController) {
        return [
          CodeLens.create(range, {
            filePath: file.path,
            registered: true,
            identifier: registeredController.identifier,
          }),
        ]
      } else {
        return [
          CodeLens.create(range, {
            filePath: file.path,
            registered: false,
            identifier: definition.guessedIdentifier,
          }),
        ]
      }
    })
  }

  onCodeLensResolve(codeLens: CodeLens) {
    const identifier = codeLens.data?.identifier
    const registered = codeLens.data?.registered
    const file = this.project.projectFiles.find((file) => file.path === codeLens.data?.filePath)

    if (!file) return codeLens
    if (file.controllerDefinitions.length === 0) return codeLens

    const registeredController = this.project.registeredControllers.find(
      (definition) => definition.identifier === identifier,
    )

    if (registered && registeredController) {
      codeLens.command = Command.create(
        `Stimulus: Connects to data-controller="${registeredController.identifier}"`,
        "",
      )
    } else {
      codeLens.command = Command.create(
        `Stimulus: The "${identifier}" controller isn't registered on your Stimulus Application`,
        "",
      )
    }

    return codeLens
  }
}


================================================
FILE: server/src/commands.ts
================================================
import dedent from "dedent"

import { Connection, TextDocumentEdit, TextEdit, CreateFile, Range, Diagnostic } from "vscode-languageserver/node"
import { DeprecatedPackageImportsDiagnosticData } from "./diagnostics"
import { Config } from "./config"
import { Project, ControllerDefinition } from "stimulus-parser"

type SerializedTextDocument = {
  _uri: string
  _languageId: string
  _version: number
  _content: string
  _lineOffsets: number[]
}

export class Commands {
  private readonly project: Project
  private readonly connection: Connection

  constructor(project: Project, connection: Connection) {
    this.project = project
    this.connection = connection
  }

  async updateControllerReference(identifier: string, diagnostic: Diagnostic, suggestion: string) {
    if (identifier === undefined) return
    if (diagnostic === undefined) return
    if (suggestion === undefined) return

    const { textDocument, range } = diagnostic.data as { textDocument: SerializedTextDocument; range: Range }

    const document = { uri: textDocument._uri, version: textDocument._version }
    const textEdit: TextEdit = { range, newText: suggestion }

    const documentChanges: TextDocumentEdit[] = [TextDocumentEdit.create(document, [textEdit])]

    await this.connection.workspace.applyEdit({ documentChanges })
  }

  async registerControllerDefinition(importStatement: string, identifier: string, localName: string) {
    if (importStatement === undefined) return
    if (identifier === undefined) return
    if (localName === undefined) return
    if (this.project.controllersIndexFiles.length === 0) return

    // TODO: there must be a better way to get the end of the file without having the textDocument
    const endOfFile = { line: 10000000, character: 0 }

    // TODO: don't always choose first contollersFile
    const uri = `file://${this.project.controllersIndexFiles[0].path}`
    const document = { uri, version: null }
    const textEdit: TextEdit = {
      range: { start: endOfFile, end: endOfFile },
      newText: `\n\n${importStatement}\napplication.register("${identifier}", ${localName})\n`,
    }

    const documentChanges: TextDocumentEdit[] = [TextDocumentEdit.create(document, [textEdit])]

    await this.connection.workspace.applyEdit({ documentChanges })
    await this.connection.window.showDocument({
      uri,
      external: false,
      takeFocus: true,
    })
  }

  async createController(identifier: string, diagnostic: Diagnostic, controllerRoot: string) {
    if (identifier === undefined) return
    if (diagnostic === undefined) return
    if (controllerRoot === undefined) controllerRoot = this.project.controllerRoot

    const path = ControllerDefinition.controllerPathForIdentifier(identifier)
    const newControllerPath = `file://${this.project.projectPath}/${controllerRoot}/${path}`
    const createFile: CreateFile = { kind: "create", uri: newControllerPath }

    await this.connection.workspace.applyEdit({ documentChanges: [createFile] })

    const documentRange: Range = Range.create(0, 0, 0, 0)
    const textEdit: TextEdit = { range: documentRange, newText: this.controllerTemplateFor(identifier) }
    const textDocumentEdit = TextDocumentEdit.create({ uri: newControllerPath, version: 1 }, [textEdit])

    await this.connection.workspace.applyEdit({ documentChanges: [textDocumentEdit] })
    await this.connection.window.showDocument({
      uri: textDocumentEdit.textDocument.uri,
      external: false,
      takeFocus: true,
    })
  }

  async updateControllerActionReference(actionName: string, diagnostic: Diagnostic, suggestion: string) {
    if (actionName === undefined) return
    if (diagnostic === undefined) return
    if (suggestion === undefined) return

    const { textDocument, range } = diagnostic.data as { textDocument: SerializedTextDocument; range: Range }

    const document = { uri: textDocument._uri, version: textDocument._version }
    const textEdit: TextEdit = { range, newText: suggestion }

    const documentChanges: TextDocumentEdit[] = [TextDocumentEdit.create(document, [textEdit])]

    await this.connection.workspace.applyEdit({ documentChanges })
  }

  async implementControllerAction(actionName: string, identifier: string, diagnostic: Diagnostic) {
    if (identifier === undefined) return
    if (actionName === undefined) return
    if (diagnostic === undefined) return

    const controller = this.project.registeredControllers.find((controller) => controller.identifier === identifier)
    if (controller === undefined) return

    const loc = controller.controllerDefinition.classDeclaration?.node?.loc

    if (!loc) return

    const position = { line: loc.end.line - 1, character: 0 }

    const textEdit: TextEdit = {
      range: { start: position, end: position },
      newText: `
  ${actionName}(event) {
    console.log("${identifier}#${actionName}", event)
  }
`,
    }

    const uri = `file://${controller.sourceFile.path}`
    const document = { uri, version: null }
    const documentChanges: TextDocumentEdit[] = [TextDocumentEdit.create(document, [textEdit])]

    await this.connection.workspace.applyEdit({ documentChanges })
    await this.connection.window.showDocument({
      uri,
      external: false,
      takeFocus: true,
    })
  }

  async updateImportSource(diagnostic: Diagnostic) {
    const {
      textDocument,
      importSourceRange: range,
      suggestion: newText,
    } = diagnostic.data as DeprecatedPackageImportsDiagnosticData & { textDocument: SerializedTextDocument }

    const textEdit: TextEdit = {
      range,
      newText,
    }

    const uri = textDocument._uri
    const document = { uri, version: null }

    const documentChanges: TextDocumentEdit[] = [TextDocumentEdit.create(document, [textEdit])]
    await this.connection.workspace.applyEdit({ documentChanges })
    await this.connection.window.showDocument({
      uri,
      external: false,
      takeFocus: true,
    })
  }

  async createStimulusLSPConfig() {
    const config = await Config.fromPathOrNew(this.project.projectPath)
    const configPath = config.path
    const createFile: CreateFile = { kind: "create", uri: configPath }

    await this.connection.workspace.applyEdit({ documentChanges: [createFile] })

    const documentRange: Range = Range.create(0, 0, 0, 0)
    const textEdit: TextEdit = { range: documentRange, newText: config.toJSON() }
    const textDocumentEdit = TextDocumentEdit.create({ uri: configPath, version: 1 }, [textEdit])

    await this.connection.workspace.applyEdit({ documentChanges: [textDocumentEdit] })
    await this.connection.window.showDocument({
      uri: textDocumentEdit.textDocument.uri,
      external: false,
      takeFocus: true,
    })
  }

  async addIgnoredControllerToConfig(identifier: string) {
    const config = await Config.fromPathOrNew(this.project.projectPath)

    config.addIgnoredController(identifier)

    await config.write()

    await this.connection.window.showDocument({
      uri: `file://${config.path}`,
      external: false,
      takeFocus: true,
    })
  }

  async addIgnoredAttributeToConfig(attribute: string) {
    const config = await Config.fromPathOrNew(this.project.projectPath)

    config.addIgnoredAttribute(attribute)

    await config.write()

    await this.connection.window.showDocument({
      uri: `file://${config.path}`,
      external: false,
      takeFocus: true,
    })
  }

  private controllerTemplateFor(identifier: string) {
    return dedent`
      import { Controller } from "@hotwired/stimulus"

      export default class extends Controller {
        connect() {
          console.log("${identifier} controller connected")
        }
      }
    `
  }
}


================================================
FILE: server/src/config.ts
================================================
export type StimulusConfigOptions = {
  ignoredControllerIdentifiers: Array<string>
  ignoredAttributes: Array<string>
}

export type StimulusLSPConfig = {
  version: string
  createdAt: string
  updatedAt: string
  options: StimulusConfigOptions
}

import path from "path"
import { version } from "../package.json"
import { promises as fs } from "fs"

export class Config {
  static configPath = ".stimulus-lsp/config.json"

  public readonly path: string
  public config: StimulusLSPConfig

  constructor(projectPath: string, config: StimulusLSPConfig) {
    this.path = Config.configPathFromProjectPath(projectPath)
    this.config = config
  }

  get version(): string {
    return this.config.version
  }

  get createdAt(): Date {
    return new Date(this.config.createdAt)
  }

  get updatedAt(): Date {
    return new Date(this.config.updatedAt)
  }

  get options(): StimulusConfigOptions {
    return this.config.options
  }

  get ignoredControllerIdentifiers(): Array<string> {
    return this.options.ignoredControllerIdentifiers
  }

  get ignoredAttributes(): Array<string> {
    return this.options.ignoredAttributes
  }

  public addIgnoredController(identifier: string) {
    const identifiers = this.ignoredControllerIdentifiers
    identifiers.push(identifier)

    this.options.ignoredControllerIdentifiers = Array.from(new Set(identifiers)).sort()
  }

  public addIgnoredAttribute(attribute: string) {
    const attributes = this.ignoredAttributes
    attributes.push(attribute)

    this.options.ignoredAttributes = Array.from(new Set(attributes)).sort()
  }

  public toJSON() {
    return JSON.stringify(this.config, null, "  ")
  }

  private updateTimestamp() {
    this.config.updatedAt = new Date().toISOString()
  }

  private updateVersion() {
    this.config.version = version
  }

  async write() {
    this.updateVersion()
    this.updateTimestamp()

    const folder = path.dirname(this.path)

    fs.stat(folder)
      .then(() => {})
      .catch(async () => await fs.mkdir(folder))
      .finally(async () => await fs.writeFile(this.path, this.toJSON()))
  }

  async read() {
    return await fs.readFile(this.path, "utf8")
  }

  static configPathFromProjectPath(projectPath: string) {
    return path.join(projectPath, this.configPath)
  }

  static async fromPathOrNew(projectPath: string) {
    try {
      return await this.fromPath(projectPath)
    } catch (error: any) {
      return Config.newConfig(projectPath)
    }
  }

  static async fromPath(projectPath: string) {
    const configPath = Config.configPathFromProjectPath(projectPath)

    try {
      const config = JSON.parse(await fs.readFile(configPath, "utf8"))

      return new Config(projectPath, config)
    } catch (error: any) {
      throw new Error(`Error reading config file at: ${configPath}. Error: ${error.message}`)
    }
  }

  static newConfig(projectPath: string): Config {
    return new Config(projectPath, {
      version,
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString(),
      options: {
        ignoredControllerIdentifiers: [],
        ignoredAttributes: []
      }
    })
  }
}


================================================
FILE: server/src/data_providers/stimulus_html_data_provider.ts
================================================
import { IHTMLDataProvider } from "@herb-tools/language-service"

import { EVENTS } from "../events"

import { Project } from "stimulus-parser"
import { dasherize } from "../utils"

export class StimulusHTMLDataProvider implements IHTMLDataProvider {
  private id: string;
  private project: Project

  constructor(id: string, project: Project) {
    this.id = id;
    this.project = project;
  }

  get controllers() {
    return this.project.registeredControllers
  }

  get controllerRoots() {
    return this.project.controllerRoots
  }

  isApplicable() {
    return true
  }

  getId() {
    return this.id
  }

  provideTags() {
    return []
  }

  provideAttributes(_tag: string) {
    const targetAttribtues = this.controllers
      .filter((controller) => controller.controllerDefinition.targetNames.length > 0)
      .map((controller) => {
        const name = `data-${controller.identifier}-target`
        return { name }
      })

    const valueAttribtues = this.controllers.flatMap((controller) => {
      return controller.controllerDefinition.values.map((definition) => {
        return { name: `data-${controller.identifier}-${dasherize(definition.name)}-value` }
      })
    })

    const classAttribtues = this.controllers.flatMap((controller) => {
      return controller.controllerDefinition.classNames.map((klass) => {
        return { name: `data-${controller.identifier}-${dasherize(klass)}-class` }
      })
    })

    return [
      { name: "data-controller" },
      { name: "data-action" },
      { name: "data-target" },
      ...targetAttribtues,
      ...valueAttribtues,
      ...classAttribtues,
    ]
  }

  provideValues(_tag: string, attribute: string) {
    if (attribute == "data-controller") {
      return this.controllers.map((controller) => ({ name: controller.identifier }))
    }

    if (attribute == "data-action") {
      const events = EVENTS.map((name) => ({ name }))
      const controllers = this.controllers.map((controller) => ({ name: `${controller.identifier}`, controller }))

      // const keys = [
      //   "alt",
      //   "ctrl",
      //   "meta",
      //   "shift",
      //   "enter",
      //   "tab",
      //   "esc",
      //   "space",
      //   "up",
      //   "down",
      //   "left",
      //   "right",
      //   "home",
      //   "end",
      //   "page_up",
      //   "page_down",
      //   ..."abcdefghijklmnopqrstuvwxyz".split(""),
      //   ..."0123456789".split(""),
      // ]

      const controllersWithEvents = EVENTS.flatMap((event) => {
        return controllers.flatMap((item) => {
          const { controller } = item

          // const keyEvents = (["keydown", "keyup", "keypress"].includes(event)) ? keys.flatMap((key1) =>
          //   keys.flatMap((key2) => [
          //     { name: `${event}.${key1}+${key2}->${controller.identifier}`, controller },
          //     { name: `${event}.${key1}+${key2}@window->${controller.identifier}`, controller },
          //     { name: `${event}.${key1}+${key2}@document->${controller.identifier}`, controller }
          //   ])
          // ) : []

          return [
            { name: `${event}->${item.controller.identifier}`, controller },
            { name: `${event}@window->${item.controller.identifier}`, controller },
            { name: `${event}@document->${item.controller.identifier}`, controller },
            // ...keyEvents
          ]
        })
      })

      const controllersWithActions = controllers.concat(controllersWithEvents).flatMap((item) => {
        const { controller } = item
        const { actionNames } = controller.controllerDefinition

        return actionNames.map((action) => {
          return { name: `${item.name}#${action}`, controller }
        })
      })

      // const options = [
      //   "capture",
      //   "once",
      //   "passive",
      //   "!passive",
      //   "stop",
      //   "self",
      // ]

      // const controllersWithActionOptions = controllersWithActions.flatMap((item) => {
      //   const { controller } = item
      //
      //   return options.map((option) => {
      //     return { name: `${item.name}:${option}`, controller }
      //   })
      // })

      return [
        ...events,
        ...controllers,
        ...controllersWithEvents,
        ...controllersWithActions,
        // ...controllersWithActionOptions,
      ]
    }

    const targetMatches = attribute.match(/data-(.+)-target/)

    if (targetMatches && Array.isArray(targetMatches) && targetMatches[1]) {
      const identifier = targetMatches[1]
      const controller = this.controllers.find((controller) => controller.identifier == identifier)

      if (!controller) return []

      return controller.controllerDefinition.targetNames.map((name) => ({ name }))
    }

    const valueMatches = attribute.match(/data-(.+)-(.+)-value/)

    if (valueMatches && Array.isArray(valueMatches) && valueMatches[1]) {
      const identifier = valueMatches[1]
      const value = valueMatches[2]

      const controller = this.controllers.find((controller) => controller.identifier == identifier)

      if (controller) {
        const valueDefiniton = controller.controllerDefinition.values.find((definition) => definition.name === value)

        if (!valueDefiniton) return []

        const defaultValue = (valueDefiniton.hasExplicitDefaultValue) ? { name: JSON.stringify(valueDefiniton.default).replace(/"/g, '\\"') } : { name: "" }

        if (valueDefiniton.type === "Boolean") {
          return [
            defaultValue,
            { name: "true" },
            { name: "false" },
            { name: "null" },
          ]
        }

        if (valueDefiniton.type === "Number") {
          return [
            { name: "-1" },
            { name: "0" },
            defaultValue,
            { name: "1" },
            { name: "2" },
            { name: "3" },
            { name: "4" },
            { name: "5" },
            { name: "6" },
            { name: "7" },
            { name: "8" },
            { name: "9" },
            { name: "10" },
          ]
        }

        if (valueDefiniton.type === "Object") {
          return [defaultValue, { name: "{}" }]
        }

        if (valueDefiniton.type === "Array") {
          return [defaultValue, { name: "[]" }]
        }

        if (valueDefiniton.type === "String") {
          return [defaultValue, { name: identifier }, { name: value }]
        }
      }
    }

    return []
  }
}


================================================
FILE: server/src/definitions.ts
================================================
import { Herb } from "@herb-tools/node-wasm"
import { Range, DefinitionParams, LocationLink } from "vscode-languageserver/node"
import { DocumentService } from "./document_service"
import { StimulusHTMLDataProvider } from "./data_providers/stimulus_html_data_provider"

import { getLanguageService } from "@herb-tools/language-service"
import { parseActionDescriptorString } from "./action_descriptor"
import { tokenList, reverseString } from "./html_util"

import type { Node, HerbHTMLNode } from "@herb-tools/language-service"
import type { TextDocument } from "vscode-languageserver-textdocument"

export class Definitions {
  private readonly documentService: DocumentService
  private readonly stimulusDataProvider: StimulusHTMLDataProvider

  constructor(documentService: DocumentService, stimulusDataProvider: StimulusHTMLDataProvider) {
    this.documentService = documentService
    this.stimulusDataProvider = stimulusDataProvider
  }

  get controllers() {
    return this.stimulusDataProvider.controllers
  }

  onDefinition(params: DefinitionParams) {
    const textDocument = this.documentService.get(params.textDocument.uri)
    if (!textDocument) return

    const html = getLanguageService({ herb: Herb }).parseHTMLDocument(textDocument)
    const offset = textDocument.offsetAt(params.position)
    const node = html.findNodeAt(offset)
    const content = textDocument.getText()

    const attributeNameResult = this.resolveAttributeNameDefinition(node, offset, content, textDocument)
    if (attributeNameResult) return attributeNameResult

    const herbNode = node as HerbHTMLNode
    let activeAttribute: string | null = null

    if (herbNode.attributeSourceRanges) {
      for (const [name, range] of Object.entries(herbNode.attributeSourceRanges)) {
        if (offset >= range.valueStart && offset <= range.valueEnd) {
          activeAttribute = name
          break
        }
      }
    }

    if (!activeAttribute) return []

    const attributeStart = this.previousIndex(content, ["'", '"'], offset)
    const attributeEnd = this.nextIndex(content, ["'", '"'], offset)
    const fullValue = content.substring(attributeStart, attributeEnd)

    let token: string
    let tokenStart: number

    if (!fullValue.includes(" ")) {
      token = fullValue
      tokenStart = attributeStart
    } else {
      const relativeStart = this.previousIndex(fullValue, [" "], offset - attributeStart)
      const relativeEnd = this.nextIndex(fullValue, [" "], offset - attributeStart)

      token = fullValue.substring(relativeStart, relativeEnd)
      tokenStart = attributeStart + relativeStart
    }

    if (activeAttribute === "data-action") {
      return this.resolveActionDefinition(token, tokenStart, offset, textDocument)
    }

    if (activeAttribute === "data-controller") {
      return this.resolveControllerDefinition(token, tokenStart, node, textDocument)
    }

    return []
  }

  private resolveAttributeNameDefinition(
    node: Node,
    offset: number,
    _content: string,
    textDocument: TextDocument,
  ): LocationLink[] | null {
    const herbNode = node as HerbHTMLNode
    if (!herbNode.attributeSourceRanges) return null

    for (const [attributeName, sourceRange] of Object.entries(herbNode.attributeSourceRanges)) {
      if (offset < sourceRange.nameStart || offset > sourceRange.nameEnd) continue

      if (!attributeName.startsWith("data-")) return []
      if (attributeName === "data-controller" || attributeName === "data-action") return []
      if (attributeName.startsWith("aria-")) return []

      const withoutPrefix = attributeName.slice(5)
      const identifier = this.findControllerIdentifierInAttribute(withoutPrefix)

      if (!identifier) continue

      const nameRange = herbNode.getAttributeNameRange(attributeName)
      if (!nameRange) continue

      const source = textDocument.getText()
      const nameInSource = source.slice(nameRange.start, nameRange.end)

      const identifierUnderscored = identifier.replace(/-/g, "_")
      const identifierPosition = nameInSource.indexOf(identifierUnderscored) !== -1
        ? nameInSource.indexOf(identifierUnderscored)
        : nameInSource.indexOf(identifier) !== -1
          ? nameInSource.indexOf(identifier)
          : 0

      const identifierLength = nameInSource.indexOf(identifierUnderscored) !== -1
        ? identifierUnderscored.length
        : identifier.length

      const originRange = Range.create(
        textDocument.positionAt(nameRange.start + identifierPosition),
        textDocument.positionAt(nameRange.start + identifierPosition + identifierLength),
      )

      return this.controllerLinks([identifier], originRange)
    }

    return null
  }

  private findControllerIdentifierInAttribute(withoutPrefix: string): string | null {
    const suffixes = ["-target", "-class"]
    const controllerIdentifiers = this.controllers.map((controller) => controller.identifier)

    for (const suffix of suffixes) {
      if (withoutPrefix.endsWith(suffix)) {
        const candidate = withoutPrefix.slice(0, -suffix.length)

        if (controllerIdentifiers.includes(candidate)) {
          return candidate
        }
      }
    }

    if (withoutPrefix.endsWith("-value")) {
      const withoutValue = withoutPrefix.slice(0, -6)
      const parts = withoutValue.split("-")

      for (let splitIndex = 1; splitIndex < parts.length; splitIndex++) {
        const candidate = parts.slice(0, splitIndex).join("-")

        if (controllerIdentifiers.includes(candidate)) {
          return candidate
        }
      }
    }

    if (controllerIdentifiers.includes(withoutPrefix)) {
      return withoutPrefix
    }

    return null
  }

  private resolveControllerDefinition(
    identifier: string,
    identifierStart: number,
    node: Node,
    textDocument: TextDocument,
  ): LocationLink[] {
    let identifiers: string[]

    if (this.controllers.some((controller) => controller.identifier === identifier)) {
      identifiers = [identifier]
    } else {
      identifiers = tokenList(node, "data-controller")
    }

    const originRange = Range.create(
      textDocument.positionAt(identifierStart),
      textDocument.positionAt(identifierStart + identifier.length),
    )

    return this.controllerLinks(identifiers, originRange)
  }

  private resolveActionDefinition(
    actionString: string,
    actionStringStart: number,
    cursorOffset: number,
    textDocument: TextDocument,
  ): LocationLink[] {
    const descriptor = parseActionDescriptorString(actionString)

    if (!descriptor.identifier || !descriptor.methodName) return []

    const arrowIndex = actionString.indexOf("->")
    const hashIndex = actionString.indexOf("#")

    const cursorRelative = cursorOffset - actionStringStart

    if (arrowIndex !== -1 && cursorRelative < arrowIndex) {
      return []
    }

    if (hashIndex !== -1 && cursorRelative > hashIndex) {
      const methodStart = actionStringStart + hashIndex + 1
      const colonIndex = actionString.indexOf(":", hashIndex)
      const methodEnd = colonIndex !== -1
        ? actionStringStart + colonIndex
        : actionStringStart + actionString.length

      const originRange = Range.create(
        textDocument.positionAt(methodStart),
        textDocument.positionAt(methodEnd),
      )

      return this.methodLinks(descriptor.identifier, descriptor.methodName, originRange)
    }

    const identifierStart = arrowIndex !== -1
      ? actionStringStart + arrowIndex + 2
      : actionStringStart

    const identifierEnd = hashIndex !== -1
      ? actionStringStart + hashIndex
      : actionStringStart + actionString.length

    const originRange = Range.create(
      textDocument.positionAt(identifierStart),
      textDocument.positionAt(identifierEnd),
    )

    return this.controllerLinks([descriptor.identifier], originRange)
  }

  private controllerLinks(identifiers: string[], originRange: Range): LocationLink[] {
    const controllers = this.controllers.filter(
      (controller) => identifiers.includes(controller.identifier),
    )

    return controllers.map((controller) =>
      LocationLink.create(
        `file://${controller.path}`,
        Range.create(0, 0, 0, 0),
        Range.create(0, 0, 0, 0),
        originRange,
      ),
    )
  }

  private methodLinks(identifier: string, methodName: string, originRange: Range): LocationLink[] {
    const controller = this.controllers.find(
      (controller) => controller.identifier === identifier,
    )

    if (!controller) return []

    const methodDefinition = controller.controllerDefinition.methodDefinitions.find(
      (method: any) => method.name === methodName,
    )

    if (methodDefinition?.node?.loc) {
      const targetRange = Range.create(
        methodDefinition.node.loc.start.line - 1,
        methodDefinition.node.loc.start.column,
        methodDefinition.node.loc.end.line - 1,
        methodDefinition.node.loc.end.column,
      )

      return [
        LocationLink.create(
          `file://${controller.path}`,
          targetRange,
          targetRange,
          originRange,
        ),
      ]
    }

    return this.controllerLinks([identifier], originRange)
  }

  private nextIndex(string: string, tokens: string[], offset: number) {
    const indexes = tokens
      .map((token) => string.indexOf(token, offset))
      .filter((index) => index !== -1)

    if (indexes.length === 0) return string.length

    return Math.min(...indexes)
  }

  private previousIndex(string: string, tokens: string[], offset: number) {
    const indexes = tokens
      .map((token) => reverseString(string).indexOf(token, string.length - offset))
      .filter((index) => index !== -1)
      .map((index) => string.length - index)

    if (indexes.length === 0) return 0

    return Math.min(...indexes)
  }
}


================================================
FILE: server/src/diagnostics.ts
================================================
import dedent from "dedent"
import { Connection, Diagnostic, DiagnosticSeverity, Position, Range } from "vscode-languageserver/node"
import { TextDocument } from "vscode-languageserver-textdocument"
import { getLanguageService, Node, findTokenIndex } from "@herb-tools/language-service"
import type { HerbHTMLNode } from "@herb-tools/language-service"
import { Herb } from "@herb-tools/node-wasm"

import { parseActionDescriptorString } from "./action_descriptor"

import { DocumentService } from "./document_service"
import { attributeValue, tokenList } from "./html_util"
import { didyoumean, camelize, dasherize } from "./utils"
import { StimulusHTMLDataProvider } from "./data_providers/stimulus_html_data_provider"

import type { Service } from "./service"
import type { Project, SourceFile } from "stimulus-parser"
import type * as Acorn from "acorn"

export interface InvalidControllerDiagnosticData {
  identifier: string
  suggestion: string
}

export interface DeprecatedPackageImportsDiagnosticData {
  identifier: string
  suggestion: string
  importSourceRange: Range
  textDocument: TextDocument
}

export interface InvalidActionDiagnosticData {
  identifier: string
  actionName: string
  suggestion: string
}

export class Diagnostics {
  private readonly connection: Connection
  private readonly stimulusDataProvider: StimulusHTMLDataProvider
  private readonly documentService: DocumentService
  private readonly project: Project
  private readonly service: Service
  private readonly diagnosticsSource = "Stimulus LSP "
  private diagnostics: Map<TextDocument, Diagnostic[]> = new Map()

  controllerAttribute = "data-controller"
  actionAttribute = "data-action"
  targetAttribute = /data-(.+)-target/
  valueAttribute = /data-(.+)-(.+)-value/

  constructor(
    connection: Connection,
    stimulusDataProvider: StimulusHTMLDataProvider,
    documentService: DocumentService,
    project: Project,
    service: Service,
  ) {
    this.connection = connection
    this.stimulusDataProvider = stimulusDataProvider
    this.documentService = documentService
    this.project = project
    this.service = service
  }

  get controllers() {
    return this.stimulusDataProvider.controllers
  }

  get controllerIdentifiers() {
    return this.controllers.map((controller) => controller.identifier)
  }

  validateParsedControllerWithoutErrors(node: Node, textDocument: TextDocument) {
    const identifiers = tokenList(node, this.controllerAttribute)

    identifiers.forEach((identifier) => {
      const controller = this.controllers.find((controller) => controller.identifier === identifier)

      if (!controller || !controller.controllerDefinition.hasErrors) return

      const attributeValueRange = this.attributeValueRange(textDocument, node, this.controllerAttribute, identifier)

      controller.controllerDefinition.errors.forEach((error) => {
        this.createParseErrorDiagnosticFor(identifier, error.message || "", textDocument, attributeValueRange)
      })
    })
  }

  populateSourceFileErrorsAsDiagnostics(sourceFile: SourceFile, textDocument: TextDocument) {
    const errors = sourceFile.errors.concat(
      sourceFile.classDeclarations.flatMap((classDeclaration) => classDeclaration.controllerDefinition?.errors || []),
    )

    errors.map((error) => {
      const range = this.rangeFromLoc(textDocument, error.loc)

      this.pushDiagnostic(
        error.message,
        "stimulus.source_file.error",
        range,
        textDocument,
        {},
        DiagnosticSeverity.Warning,
      )
    })
  }

  validateDataControllerAttribute(node: Node, textDocument: TextDocument) {
    const identifiers = tokenList(node, this.controllerAttribute)
    const invalidIdentifiers = identifiers.filter(
      (identifier) => !this.controllerIdentifiers.includes(identifier) && !this.foundSkippableTags(identifier),
    )

    invalidIdentifiers.forEach((identifier) => {
      const attributeValueRange = this.attributeValueRange(textDocument, node, this.controllerAttribute, identifier)

      this.createInvalidControllerDiagnosticFor(identifier, textDocument, attributeValueRange)
    })
  }

  validateDataActionAttribute(node: Node, textDocument: TextDocument) {
    const actions = tokenList(node, this.actionAttribute)

    actions.forEach((action) => {
      const actionDescriptor = parseActionDescriptorString(action)
      const { identifier, methodName } = actionDescriptor

      if (!identifier || !methodName) {
        const attributeValueRange = this.attributeValueRange(textDocument, node, this.actionAttribute, action)

        this.createInvalidActionDiagnosticFor(action, textDocument, attributeValueRange)

        return
      }

      const controller = this.controllers.find((controller) => controller.identifier === identifier)

      if (!controller && !this.foundSkippableTags(identifier)) {
        const attributeValueRange = this.attributeValueRange(textDocument, node, this.actionAttribute, identifier)

        this.createInvalidControllerDiagnosticFor(identifier, textDocument, attributeValueRange)
      }

      if (controller && controller.controllerDefinition.hasErrors) return

      if (
        controller &&
        methodName &&
        !controller.controllerDefinition.actionNames.includes(methodName) &&
        !this.foundSkippableTags(methodName)
      ) {
        const attributeValueRange = this.attributeValueRange(textDocument, node, this.actionAttribute, methodName)

        this.createInvalidControllerActionDiagnosticFor(identifier, methodName, textDocument, attributeValueRange)
      }
    })
  }

  validateDataValueAttribute(node: Node, textDocument: TextDocument) {
    const attributes = node.attributes || {}

    const valueAttributeNames = Object.keys(attributes).filter((attribute) => attribute.match(this.valueAttribute))

    valueAttributeNames.forEach((attribute) => {
      const value = attributeValue(node, attribute) || ""
      const attributeMatches = attribute.match(this.valueAttribute)

      if (this.isIgnoredAttribute(attribute)) {
        return
      }

      // cannot analyze value if it is interpolated
      if (this.foundSkippableTags(value)) {
        return
      }

      if (attributeMatches && Array.isArray(attributeMatches) && attributeMatches[1]) {
        let identifier = attributeMatches[1]
        let valueName = attributeMatches[2]

        let controller = this.controllers.find((controller) => controller.identifier === identifier)

        if (!controller) {
          const identifierSplits = identifier.split("--")

          let valuePart
          let namespacePart

          // has namespace
          if (identifierSplits.length > 1) {
            namespacePart = identifierSplits.slice(0, -1).join("--")
            valuePart = identifierSplits[identifierSplits.length - 1]
          } else {
            namespacePart = null
            valuePart = identifierSplits[0]
          }

          const allParts = valuePart.split("-").concat(valueName.split("-"))

          for (let i = 1; i <= allParts.length; i++) {
            if (controller) continue

            let potentialIdentifier = allParts.slice(0, i).join("-")

            if (namespacePart) {
              potentialIdentifier = `${namespacePart}--${potentialIdentifier}`
            }

            const potentialValueName = allParts.slice(i, allParts.length).join("-")

            controller = this.controllers.find((controller) => controller.identifier === potentialIdentifier)

            if (controller) {
              identifier = potentialIdentifier
              valueName = potentialValueName
            }
          }
        }

        if (!controller) {
          const attributeNameRange = this.attributeNameRange(textDocument, node, attribute, identifier)
          this.createInvalidControllerDiagnosticFor(identifier, textDocument, attributeNameRange, { attribute })

          return
        }

        const hasUppercaseLetter = valueName.match(/[A-Z]/g)

        if (hasUppercaseLetter) {
          const attributeNameRange = this.attributeNameRange(textDocument, node, attribute, valueName)
          this.createAttributeFormatMismatchDiagnosticFor(identifier, valueName, textDocument, attributeNameRange)

          return
        }

        const camelizedValueName = camelize(valueName)
        const valueDefiniton = controller.controllerDefinition.values.find(
          (definition) => definition.name === camelizedValueName,
        )

        if (controller && controller.controllerDefinition.hasErrors) return

        if (controller && !valueDefiniton) {
          const attributeNameRange = this.attributeNameRange(textDocument, node, attribute, valueName)
          this.createMissingValueOnControllerDiagnosticFor(
            identifier,
            camelizedValueName,
            textDocument,
            attributeNameRange,
          )

          return
        }

        if (!valueDefiniton) return

        let actualType
        const expectedType = valueDefiniton.type

        try {
          actualType = this.parseValueType(JSON.parse(value))
        } catch (e) {
          try {
            actualType = this.parseValueType(JSON.parse(`"${value}"`))
          } catch (e: any) {
            actualType = e?.message || "unparsable"
          }
        }

        if (actualType !== expectedType) {
          const attributeValueRange = this.attributeValueRange(textDocument, node, attribute, value)

          this.createValueMismatchOnControllerDiagnosticFor(
            identifier,
            camelizedValueName,
            expectedType,
            actualType,
            textDocument,
            attributeValueRange,
          )
        }
      }
    })
  }

  validateDataClassAttribute(_node: Node, _textDocument: TextDocument) {
    // TODO: implement
  }

  validateDataTargetAttribute(node: Node, textDocument: TextDocument) {
    const attributes = node.attributes || {}

    const targetAttributeNames = Object.keys(attributes).filter((attribute) => attribute.match(this.targetAttribute))

    targetAttributeNames.forEach((attribute) => {
      if (this.isIgnoredAttribute(attribute)) return

      const targetName = attributeValue(node, attribute) || ""
      const targetMatches = attribute.match(this.targetAttribute)
      const matchedTarget = targetMatches && Array.isArray(targetMatches)
      const identifier = matchedTarget && targetMatches[1]

      if (identifier) {
        const controller = this.controllers.find((controller) => controller.identifier === identifier)

        if (!controller) {
          const attributeNameRange = this.attributeNameRange(textDocument, node, attribute, identifier)
          this.createInvalidControllerDiagnosticFor(identifier, textDocument, attributeNameRange, { attribute })

          return
        }

        if (controller && controller.controllerDefinition.hasErrors) return

        if (
          controller &&
          !controller.controllerDefinition.targetNames.includes(targetName) &&
          this.foundSkippableTags(targetName)
        ) {
          const attributeNameRange = this.attributeValueRange(textDocument, node, attribute, targetName)

          this.createMissingTargetOnControllerDiagnosticFor(identifier, targetName, textDocument, attributeNameRange)
        }
      }
    })
  }

  validateStimulusImports(sourceFile: SourceFile, textDocument: TextDocument) {
    if (sourceFile.importDeclarations.length === 0) return

    const replacements: { [key: string]: string } = {
      stimulus: "@hotwired/stimulus",
      "@stimulus/webpack-helpers": "@hotwired/stimulus-webpack-helpers",
    }

    sourceFile.importDeclarations.forEach((importDeclaration) => {
      if (!importDeclaration.node.loc) return

      const range = this.rangeFromLoc(textDocument, importDeclaration.node.loc)
      const importSourceRange = this.rangeFromLoc(textDocument, importDeclaration.node.source.loc)

      // Strip out the quotes
      importSourceRange.start.character += 1
      importSourceRange.end.character -= 1

      const data: DeprecatedPackageImportsDiagnosticData = {
        identifier: importDeclaration.source,
        suggestion: replacements[importDeclaration.source],
        textDocument,
        importSourceRange,
      }

      if (Object.keys(replacements).includes(importDeclaration.source)) {
        this.pushDiagnostic(
          `You are importing from the deprecated \`${importDeclaration.source}\` package.\nPlease use the new \`${replacements[importDeclaration.source]}\` package.\n`,
          "stimulus.package.deprecated.import",
          range,
          textDocument,
          data,
          DiagnosticSeverity.Information,
        )
      }
    })
  }

  validateValueDefinitions(sourceFile: SourceFile, textDocument: TextDocument) {
    sourceFile.controllerDefinitions.forEach((controller) => {
      if (controller.values.length === 0) return

      controller.values.forEach((valueDefinition) => {
        const defaultValueType = this.parseValueType(valueDefinition.default)

        if (!["Array", "Boolean", "Number", "Object", "String"].includes(valueDefinition.type)) {
          const range = this.rangeFromLoc(textDocument, valueDefinition.typeLoc)

          this.pushDiagnostic(
            `Unknown Value type. The "${valueDefinition.name}" value is defined as type "${valueDefinition.type}". \nPossible Values: \`Array\`, \`Boolean\`, \`Number\`, \`Object\`, or \`String\`.\n`,
            "stimulus.controller.value_definition.unknown_type",
            range,
            textDocument,
            {},
            DiagnosticSeverity.Error,
          )

          return
        }

        if (valueDefinition.type !== defaultValueType) {
          const range = this.rangeFromLoc(textDocument, valueDefinition.defaultValueLoc)

          const message = dedent`
            The type of the default value you provided doesn't match the type you defined.
            The "${valueDefinition.name}" Stimulus Value is of type \`${valueDefinition.type}\`.
            The default value you provided for "${valueDefinition.name}" is of type \`${defaultValueType}\`.
          `

          this.pushDiagnostic(
            message,
            "stimulus.controller.value_definition.default_value.type_mismatch",
            range,
            textDocument,
            {},
            DiagnosticSeverity.Error,
          )
        }
      })
    })
  }

  visitNode(node: Node, textDocument: TextDocument) {
    this.validateParsedControllerWithoutErrors(node, textDocument)
    this.validateDataControllerAttribute(node, textDocument)
    this.validateDataActionAttribute(node, textDocument)
    this.validateDataValueAttribute(node, textDocument)
    this.validateDataClassAttribute(node, textDocument)
    this.validateDataTargetAttribute(node, textDocument)

    node.children.forEach((child) => {
      this.visitNode(child, textDocument)
    })
  }

  validate(textDocument: TextDocument) {
    if (["javascript", "typescript"].includes(textDocument.languageId)) {
      this.validateJavaScriptDocument(textDocument)
    } else {
      this.validateHTMLDocument(textDocument)
    }

    this.sendDiagnosticsFor(textDocument)
  }

  validateJavaScriptDocument(textDocument: TextDocument) {
    const sourceFile = this.project.projectFiles.find((file) => `file://${file.path}` === textDocument.uri)

    if (sourceFile) {
      this.populateSourceFileErrorsAsDiagnostics(sourceFile, textDocument)
      this.validateValueDefinitions(sourceFile, textDocument)
      this.validateStimulusImports(sourceFile, textDocument)
    }
  }

  validateHTMLDocument(textDocument: TextDocument) {
    const service = getLanguageService({ herb: Herb })
    const html = service.parseHTMLDocument(textDocument)

    html.roots.forEach((node: Node) => {
      this.visitNode(node, textDocument)
    })
  }

  refreshDocument(document: TextDocument) {
    this.validate(document)
  }

  refreshAllDocuments() {
    this.documentService.getAll().forEach((document) => {
      this.refreshDocument(document)
    })
  }

  private rangeFromLoc(textDocument: TextDocument, loc?: Acorn.SourceLocation | null): Range {
    let range = Range.create(textDocument.positionAt(0), textDocument.positionAt(0))

    if (loc) {
      const start = Position.create(loc.start.line - 1, loc.start.column)
      const end = Position.create(loc.end.line - 1, loc.end.column)

      range = Range.create(start, end)
    }

    return range
  }

  private rangeFromNode(textDocument: TextDocument, node: Node) {
    return Range.create(textDocument.positionAt(node.start), textDocument.positionAt(node.startTagEnd || node.end))
  }

  private attributeNameRange(textDocument: TextDocument, node: Node, attribute: string, search: string) {
    const herbNode = node as HerbHTMLNode
    const nameRange = herbNode.getAttributeNameRange?.(attribute)

    if (nameRange) {
      return Range.create(
        textDocument.positionAt(nameRange.start),
        textDocument.positionAt(nameRange.end),
      )
    }

    const range = this.rangeFromNode(textDocument, node)
    const startTagContent = textDocument.getText(range)

    return this.rangeForAttributeName(textDocument, startTagContent, node, attribute, search)
  }

  private rangeForAttributeName(
    textDocument: TextDocument,
    tagContent: string,
    node: Node,
    attribute: string,
    search: string,
  ) {
    const searchIndex = attribute.indexOf(search) || 0
    const attributeNameStartIndex = tagContent.indexOf(attribute)

    const attributeNameStart = node.start + attributeNameStartIndex + searchIndex
    const attributeNameEnd = attributeNameStart + search.length

    return Range.create(textDocument.positionAt(attributeNameStart), textDocument.positionAt(attributeNameEnd))
  }

  private attributeValueRange(textDocument: TextDocument, node: Node, attribute: string, search: string) {
    const herbNode = node as HerbHTMLNode
    const tokenRange = herbNode.getAttributeValueTokenRange?.(attribute, search, textDocument.getText())

    if (tokenRange) {
      return Range.create(
        textDocument.positionAt(tokenRange.start),
        textDocument.positionAt(tokenRange.end),
      )
    }

    const range = this.rangeFromNode(textDocument, node)
    const startTagContent = textDocument.getText(range)

    return this.rangeForAttributeValue(textDocument, startTagContent, node, attribute, search)
  }

  private rangeForAttributeValue(
    textDocument: TextDocument,
    tagContent: string,
    node: Node,
    attribute: string,
    search: string,
  ) {
    const value = attributeValue(node, attribute) || ""

    const searchIndex = findTokenIndex(value, search) !== -1 ? findTokenIndex(value, search) : 0
    const attributeStartIndex = tagContent.indexOf(attribute)

    const attributeValueStart = node.start + attributeStartIndex + attribute.length + searchIndex + 2
    const attributeValueEnd = attributeValueStart + search.length

    return Range.create(textDocument.positionAt(attributeValueStart), textDocument.positionAt(attributeValueEnd))
  }

  private createParseErrorDiagnosticFor(identifier: string, error: string, textDocument: TextDocument, range: Range) {
    this.pushDiagnostic(
      `There was an error parsing the "${identifier}" Stimulus controller. \nPlease check the controller for the following error: ${error}`,
      "stimulus.controller.parse_error",
      range,
      textDocument,
      { identifier },
    )
  }

  private isIgnoredController(identifier: string) {
    const ignoredIdentifiers = this.service.config?.ignoredControllerIdentifiers || []

    return ignoredIdentifiers.includes(identifier)
  }

  private isIgnoredAttribute(attribute: string) {
    const ignoredAttributes = this.service.config?.ignoredAttributes || []

    return ignoredAttributes.includes(attribute)
  }

  private createInvalidControllerDiagnosticFor(identifier: string, textDocument: TextDocument, range: Range, data?: Object) {
    const match = didyoumean(
      identifier,
      this.controllers.map((controller) => controller.identifier),
    )
    const suggestion = match ? `Did you mean "${match}"?` : ""

    if (this.isIgnoredController(identifier)) return

    this.pushDiagnostic(
      `"${identifier}" isn't a valid Stimulus controller. ${suggestion}`,
      "stimulus.controller.invalid",
      range,
      textDocument,
      { identifier, suggestion: match, textDocument, range, data },
    )
  }

  private createInvalidActionDiagnosticFor(action: string, textDocument: TextDocument, range: Range) {
    this.pushDiagnostic(`"${action}" isn't a valid action descriptor`, "stimulus.action.invalid", range, textDocument, {
      action,
    })
  }

  private createInvalidControllerActionDiagnosticFor(
    identifier: string,
    actionName: string,
    textDocument: TextDocument,
    range: Range,
  ) {
    const controller = this.controllers.find((controller) => controller.identifier === identifier)
    const match = controller ? didyoumean(actionName, controller.controllerDefinition.actionNames) : null
    const suggestion = match ? `Did you mean "${match}"?` : ""

    this.pushDiagnostic(
      `"${actionName}" isn't a valid Controller Action on the "${identifier}" controller. ${suggestion}`,
      "stimulus.controller.action.invalid",
      range,
      textDocument,
      { identifier, actionName, suggestion: match, textDocument, range },
    )
  }

  private createAttributeFormatMismatchDiagnosticFor(
    identifier: string,
    valueName: string,
    textDocument: TextDocument,
    range: Range,
  ) {
    this.pushDiagnostic(
      `The data attribute for "${valueName}" on the "${identifier}" controller is camelCased, but should be dasherized ("${dasherize(valueName)}"). Please use dashes for Stimulus data attributes.`,
      "stimulus.attribute.mismatch",
      range,
      textDocument,
      { identifier, valueName },
    )
  }

  private createMissingValueOnControllerDiagnosticFor(
    identifier: string,
    valueName: string,
    textDocument: TextDocument,
    range: Range,
  ) {
    const controller = this.controllers.find((controller) => controller.identifier === identifier)
    const match = controller ? didyoumean(valueName, Object.keys(controller.controllerDefinition.values)) : null
    const suggestion = match ? `Did you mean "${match}"?` : ""

    this.pushDiagnostic(
      `"${valueName}" isn't a valid Stimulus Value name on the "${identifier}" controller. ${suggestion}`,
      "stimulus.controller.value.missing",
      range,
      textDocument,
      { identifier, valueName },
    )
  }

  private createMissingTargetOnControllerDiagnosticFor(
    identifier: string,
    targetName: string,
    textDocument: TextDocument,
    range: Range,
  ) {
    const controller = this.controllers.find((controller) => controller.identifier === identifier)
    const match = controller ? didyoumean(targetName, controller.controllerDefinition.targetNames) : null
    const suggestion = match ? `Did you mean "${match}"?` : ""

    this.pushDiagnostic(
      `"${targetName}" isn't a valid Stimulus Target on the "${identifier}" controller. ${suggestion}`,
      "stimulus.controller.target.missing",
      range,
      textDocument,
      { identifier, targetName },
    )
  }

  private createValueMismatchOnControllerDiagnosticFor(
    identifier: string,
    valueName: string,
    expectedType: string,
    actualType: string,
    textDocument: TextDocument,
    range: Range,
  ) {
    this.pushDiagnostic(
      `The value you passed for the "${valueName}" Stimulus Value is of type "${actualType}". But the "${valueName}" Stimulus Value defined in the "${identifier}" controller is of type "${expectedType}".`,
      "stimulus.controller.value.type_mismatch",
      range,
      textDocument,
      { identifier, valueName },
    )
  }

  private pushDiagnostic(
    message: string,
    code: string,
    range: Range,
    textDocument: TextDocument,
    data = {},
    severity: DiagnosticSeverity = DiagnosticSeverity.Error,
  ) {
    const diagnostic: Diagnostic = {
      source: this.diagnosticsSource,
      severity,
      range,
      message,
      code,
      data,
    }

    const diagnostics = this.diagnostics.get(textDocument) || []
    diagnostics.push(diagnostic)

    this.diagnostics.set(textDocument, diagnostics)

    return diagnostic
  }

  private sendDiagnosticsFor(textDocument: TextDocument) {
    const diagnostics = this.diagnostics.get(textDocument) || []

    this.connection.sendDiagnostics({
      uri: textDocument.uri,
      diagnostics,
    })

    this.diagnostics.delete(textDocument)
  }

  private parseValueType(string: any) {
    switch (typeof string) {
      case "boolean":
        return "Boolean"
      case "number":
        return "Number"
      case "string":
        return "String"
    }

    if (Array.isArray(string)) return "Array"
    if (Object.prototype.toString.call(string) === "[object Object]") return "Object"
  }

  private foundSkippableTags(value: string) {
    const skippableTags = ["<%", "<%=", "<%-", "%>", "<?=", "<?php", "?>", "{{", "}}"]
    return skippableTags.some((tag) => value.includes(tag))
  }
}


================================================
FILE: server/src/document_service.ts
================================================
import { Connection, TextDocuments } from "vscode-languageserver/node"
import { TextDocument } from "vscode-languageserver-textdocument"

export class DocumentService {
  public documents: TextDocuments<TextDocument>
  document?: TextDocument

  constructor(connection: Connection) {
    this.documents = new TextDocuments(TextDocument)

    // Make the text document manager listen on the connection
    // for open, change and close text document events
    this.documents.listen(connection)
  }

  get(uri: string) {
    return this.documents.get(uri)
  }

  getAll() {
    return this.documents.all()
  }

  get onDidChangeContent() {
    return this.documents.onDidChangeContent
  }

  get onDidOpen() {
    return this.documents.onDidOpen
  }

  get onDidClose() {
    return this.documents.onDidClose
  }
}


================================================
FILE: server/src/events.ts
================================================
export const EVENTS = [
  "DOMContentLoaded",
  "abort",
  "animationcancel",
  "animationend",
  "animationiteration",
  "animationstart",
  "auxclick",
  "change",
  "click",
  "compositionend",
  "compositionstart",
  "compositionupdate",
  "contextmenu",
  "copy",
  "cut",
  "dblclick",
  "drag",
  "dragend",
  "dragenter",
  "dragleave",
  "dragover",
  "dragstart",
  "drop",
  "error",
  "focusin",
  "focusout",
  "fullscreenchange",
  "fullscreenerror",
  "hashchange",
  "input",
  "keydown",
  "keyup",
  "mousedown",
  "mousemove",
  "mouseout",
  "mouseover",
  "mouseup",
  "paste",
  "pointercancel",
  "pointerdown",
  "pointerlockchange",
  "pointerlockerror",
  "pointermove",
  "pointerout",
  "pointerover",
  "pointerup",
  "popstate",
  "reset",
  "scroll",
  "select",
  "submit",
  "touchcancel",
  "touchend",
  "touchmove",
  "touchstart",
  "transitioncancel",
  "transitionend",
  "transitionrun",
  "transitionstart",
  "visibilitychange",
  "wheel",
]


================================================
FILE: server/src/html_util.ts
================================================
import { Node } from "@herb-tools/language-service"

export function attributeValue(node: Node, attribute: string) {
  if (!node.attributes) return null

  const value = node.attributes[attribute]

  if (!value) return null

  return unquote(value)
}

export function tokenList(node: Node, attribute: string) {
  let value = attributeValue(node, attribute)

  if (!value) return []

  value = squish(value).trim()

  if (value.length === 0) return []

  return splitOnSpaceIgnoreTags(value)
}

export function unquote(string: string) {
  return string.substr(1, string.length - 2)
}

export function reverseString(string: string) {
  return string.split("").reverse().join("")
}

export function squish(string: string) {
  return string.replace(/\s+/g, " ")
}

export function splitOnSpaceIgnoreTags(string: string) {
  // All spaces inside certain opening/closing tags are ignored in this regex pattern
  // Supported tags:
  // - Opening: <%=, <%, <%-, <?php, <?=, {{
  // - Closing: %>, -%>, ?>, }}
  const pattern = /(?<!<%=|<%|<%-|<\?php|<\?=|\{\{.*?)\s+(?![^<]*?%>|-%>|\?>|\}\})/g
  return string.split(pattern)
}


================================================
FILE: server/src/levenshtein.ts
================================================
/*
 * The following code is derived from the "js-levenshtein" repository,
 * Copyright (c) 2017 Gustaf Andersson (https://github.com/gustf/js-levenshtein)
 * Licensed under the MIT License (https://github.com/gustf/js-levenshtein/blob/master/LICENSE).
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

export function levenshtein(a: string, b: string): number {
  function _min(d0: any, d1: any, d2: any, bx: any, ay: any) {
    return d0 < d1 || d2 < d1 ? (d0 > d2 ? d2 + 1 : d0 + 1) : bx === ay ? d1 : d1 + 1
  }

  if (a === b) {
    return 0
  }

  if (a.length > b.length) {
    const tmp = a
    a = b
    b = tmp
  }

  let la = a.length
  let lb = b.length

  while (la > 0 && a.charCodeAt(la - 1) === b.charCodeAt(lb - 1)) {
    la--
    lb--
  }

  let offset = 0

  while (offset < la && a.charCodeAt(offset) === b.charCodeAt(offset)) {
    offset++
  }

  la -= offset
  lb -= offset

  if (la === 0 || lb < 3) {
    return lb
  }

  let x = 0
  let y
  let d0
  let d1
  let d2
  let d3
  let dd
  let dy
  let ay
  let bx0
  let bx1
  let bx2
  let bx3

  const vector = []

  for (y = 0; y < la; y++) {
    vector.push(y + 1)
    vector.push(a.charCodeAt(offset + y))
  }

  const len = vector.length - 1

  for (; x < lb - 3; ) {
    bx0 = b.charCodeAt(offset + (d0 = x))
    bx1 = b.charCodeAt(offset + (d1 = x + 1))
    bx2 = b.charCodeAt(offset + (d2 = x + 2))
    bx3 = b.charCodeAt(offset + (d3 = x + 3))
    dd = x += 4
    for (y = 0; y < len; y += 2) {
      dy = vector[y]
      ay = vector[y + 1]
      d0 = _min(dy, d0, d1, bx0, ay)
      d1 = _min(d0, d1, d2, bx1, ay)
      d2 = _min(d1, d2, d3, bx2, ay)
      dd = _min(d2, d3, dd, bx3, ay)
      vector[y] = dd
      d3 = d2
      d2 = d1
      d1 = d0
      d0 = dy
    }
  }

  for (; x < lb; ) {
    bx0 = b.charCodeAt(offset + (d0 = x))
    dd = ++x
    for (y = 0; y < len; y += 2) {
      dy = vector[y]
      vector[y] = dd = _min(dy, d0, dd, bx0, vector[y + 1])
      d0 = dy
    }
  }

  return dd
}


================================================
FILE: server/src/requests/controller_definitions.ts
================================================
import { Position } from "vscode-languageserver/node"
import { RegisteredController, ControllerDefinition, ClassDeclarationNode } from "stimulus-parser"

import { Service } from "../service"

import { importStatementForController } from "../utils"

import type {
  ControllerDefinition as ControllerDefinitionRequestType,
  ControllerDefinitionsRequest as ControllerDefinitionsRequestType,
  ControllerDefinitionsResponse,
} from "../requests"

export class ControllerDefinitionsRequest {
  private service: Service

  constructor(service: Service) {
    this.service = service
  }

  async handleRequest(_request: ControllerDefinitionsRequestType): Promise<ControllerDefinitionsResponse> {
    return {
      registered: {
        name: "project",
        controllerDefinitions: this.registeredControllers,
      },
      unregistered: {
        project: {
          name: "project",
          controllerDefinitions: this.unregisteredControllers,
        },
        nodeModules: this.nodeModuleControllers,
      },
    }
  }

  private controllerSort(a: ControllerDefinitionRequestType, b: ControllerDefinitionRequestType) {
    return a.identifier.localeCompare(b.identifier)
  }

  private positionFromNode(node: ClassDeclarationNode | undefined) {
    return Position.create(node?.loc?.start?.line || 1, node?.loc?.start?.column || 1)
  }

  private mapControllerDefinition = (controllerDefinition: ControllerDefinition) => {
    const { path, guessedIdentifier: identifier, classDeclaration } = controllerDefinition

    const registered = false
    const position = this.positionFromNode(classDeclaration.node)

    const { localName, importStatement } = importStatementForController(controllerDefinition, this.service.project)

    return {
      path,
      identifier,
      position,
      registered,
      importStatement,
      localName,
    }
  }

  private mapRegisteredController = (registeredController: RegisteredController) => {
    const { path, identifier, classDeclaration } = registeredController

    const registered = true
    const position = this.positionFromNode(classDeclaration.node)

    return {
      path,
      identifier,
      position,
      registered,
    }
  }

  private get registeredControllerPaths() {
    return this.service.project.registeredControllers.map((c) => c.path)
  }

  private get unregisteredControllerDefinitions() {
    return this.service.project.controllerDefinitions.filter(
      (definition) => !this.registeredControllerPaths.includes(definition.path),
    )
  }

  private get detectedNodeModules() {
    return this.service.project.detectedNodeModules
  }

  private get registeredControllers() {
    return this.service.project.registeredControllers.map(this.mapRegisteredController).sort(this.controllerSort)
  }

  private get unregisteredControllers() {
    return this.unregisteredControllerDefinitions.map(this.mapControllerDefinition).sort(this.controllerSort)
  }

  private get nodeModuleControllers() {
    // Stimulus-Use's controllers are "abstract" and meant to be extended. So we shouldn't suggest to register them.
    const excludeList = ["stimulus-use"]

    const nodeModules = this.detectedNodeModules
      .filter((module) => !excludeList.includes(module.name))
      .map((detectedModule) => {
        const { name } = detectedModule

        const controllerDefinitions = detectedModule.controllerDefinitions
          .filter((definition) => !this.registeredControllerPaths.includes(definition.path))
          .map(this.mapControllerDefinition)
          .sort(this.controllerSort)

        return { name, controllerDefinitions }
      })

    return nodeModules.filter((m) => m.controllerDefinitions.length > 0).sort((a, b) => a.name.localeCompare(b.name))
  }
}


================================================
FILE: server/src/requests.ts
================================================
import { Position } from "vscode-languageserver"

export type ControllerDefinition = {
  identifier: string
  path: string
  registered: boolean
  position: Position
  importStatement?: string
  localName?: string
}

export interface ControllerDefinitionsOrigin {
  name: string
  controllerDefinitions: ControllerDefinition[]
}

export interface ProjectControllerDefinitions extends ControllerDefinitionsOrigin {
  name: "project"
}

export type ControllerDefinitionsRequest = object
export type ControllerDefinitionsResponse = {
  registered: ProjectControllerDefinitions
  unregistered: {
    project: ProjectControllerDefinitions
    nodeModules: ControllerDefinitionsOrigin[]
  }
}


================================================
FILE: server/src/server.ts
================================================
import {
  createConnection,
  ProposedFeatures,
  InitializeParams,
  DidChangeConfigurationNotification,
  DidChangeWatchedFilesNotification,
  TextDocumentSyncKind,
  InitializeResult,
  Diagnostic,
} from "vscode-languageserver/node"

import { Service } from "./service"
import { StimulusSettings } from "./settings"
import { version } from "../package.json"

import { ControllerDefinitionsRequest } from "./requests/controller_definitions"

import type {
  ControllerDefinitionsRequest as ControllerDefinitionsRequestType,
  ControllerDefinitionsResponse,
} from "./requests"

let service: Service
const connection = createConnection(ProposedFeatures.all)

connection.onInitialize(async (params: InitializeParams) => {
  service = new Service(connection, params)
  await service.init()

  const result: InitializeResult = {
    serverInfo: {
      name: "Stimulus LSP",
      version
    },
    capabilities: {
      textDocumentSync: TextDocumentSyncKind.Incremental,
      completionProvider: {
        resolveProvider: true,
        triggerCharacters: ['"', "'", " ", "=", "<", "/", "-", ">", "#", "."],
      },
      codeLensProvider: { resolveProvider: true },
      codeActionProvider: true,
      definitionProvider: true,
      executeCommandProvider: {
        commands: [
          "stimulus.controller.create",
          "stimulus.controller.update",
          "stimulus.controller.register",
          "stimulus.controller.action.update",
          "stimulus.controller.action.implement",
          "stimulus.config.create",
          "stimulus.config.controller.ignore",
          "stimulus.config.attribute.ignore",
          "stimulus.import.source.update",
        ],
      },
    },
  }

  if (service.settings.hasWorkspaceFolderCapability) {
    result.capabilities.workspace = {
      workspaceFolders: {
        supported: true,
      },
    }
  }

  return result
})

connection.onInitialized(() => {
  if (service.settings.hasConfigurationCapability) {
    // Register for all configuration changes.
    connection.client.register(DidChangeConfigurationNotification.type, undefined)
  }

  if (service.settings.hasWorkspaceFolderCapability) {
    connection.workspace.onDidChangeWorkspaceFolders((_event) => {
      connection.console.log("Workspace folder change event received.")
    })
  }

  connection.client.register(DidChangeWatchedFilesNotification.type, {
    watchers: service.project.controllerRoots.map((root) => ({ globPattern: `**/${root}/**/*` })),
  })

  connection.client.register(DidChangeWatchedFilesNotification.type, {
    watchers: [
      { globPattern: `**/**/*.{ts,js}` },
      { globPattern: `**/**/.stimulus-lsp/config.json` },
    ],
  })
})

connection.onDidChangeConfiguration((change) => {
  if (service.settings.hasConfigurationCapability) {
    // Reset all cached document settings
    service.settings.documentSettings.clear()
  } else {
    service.settings.globalSettings = (
      (change.settings.languageServerStimulus || service.settings.defaultSettings)
    ) as StimulusSettings
  }

  service.refresh()
})

connection.onDidOpenTextDocument((params) => {
  const document = service.documentService.get(params.textDocument.uri)

  if (document) {
    service.diagnostics.refreshDocument(document)
  }
})


connection.onDidChangeWatchedFiles((params) => {
  params.changes.forEach(async (event) => {
    if (event.uri.endsWith("/.stimulus-lsp/config.json")) {
      await service.refreshConfig()

      service.documentService.getAll().forEach((document) => {
        service.diagnostics.refreshDocument(document)
      })
    }
  })
})

connection.onDefinition((params) => service.definitions.onDefinition(params))
connection.onCodeAction((params) => service.codeActions.onCodeAction(params))
connection.onCodeLens((params) => service.codeLens.onCodeLens(params))
connection.onCodeLensResolve((codeLens) => service.codeLens.onCodeLensResolve(codeLens))

connection.onExecuteCommand((params) => {
  if (!params.arguments) return

  if (params.command === "stimulus.controller.create") {
    const [identifier, diagnostic, controllerRoot] = params.arguments as [string, Diagnostic, string]

    service.commands.createController(identifier, diagnostic, controllerRoot)
  }

  if (params.command === "stimulus.controller.update") {
    const [identifier, diagnostic, suggestion] = params.arguments as [string, Diagnostic, string]

    service.commands.updateControllerReference(identifier, diagnostic, suggestion)
  }

  if (params.command === "stimulus.controller.action.update") {
    const [actionName, diagnostic, suggestion] = params.arguments as [string, Diagnostic, string]

    service.commands.updateControllerActionReference(actionName, diagnostic, suggestion)
  }

  if (params.command === "stimulus.controller.action.implement") {
    const [identifer, actionName, diagnostic] = params.arguments as [string, string, Diagnostic]

    service.commands.implementControllerAction(identifer, actionName, diagnostic)
  }

  if (params.command === "stimulus.import.source.update") {
    const [diagnostic] = params.arguments as [Diagnostic]

    service.commands.updateImportSource(diagnostic)
  }

  if (params.command === "stimulus.config.create") {
    const [_identifier, _diagnostic] = params.arguments as [string, Diagnostic]

    service.commands.createStimulusLSPConfig()
  }

  if (params.command === "stimulus.config.controller.ignore") {
    const [identifier, _diagnostic] = params.arguments as [string, Diagnostic]

    service.commands.addIgnoredControllerToConfig(identifier)
  }

  if (params.command === "stimulus.config.attribute.ignore") {
    const [attribute, _diagnostic] = params.arguments as [string, Diagnostic]

    service.commands.addIgnoredAttributeToConfig(attribute)
  }

  if (params.command === "stimulus.controller.register") {
    const [importStatement, identifier, localName] = params.arguments as [string, string, string]

    service.commands.registerControllerDefinition(importStatement, identifier, localName)
  }
})

connection.onCompletion((textDocumentPosition) => {
  const document = service.documentService.get(textDocumentPosition.textDocument.uri)

  if (!document) return null

  return service.htmlLanguageService.doComplete(
    document,
    textDocumentPosition.position,
    service.htmlLanguageService.parseHTMLDocument(document),
  )
})

// This handler resolves additional information for the item selected in
// the completion list.
connection.onCompletionResolve((item) => {
  if (item.data?.detail) item.detail = item.data.detail
  if (item.data?.documentation) item.documentation = item.data.documentation
  if (item.data?.kind) item.kind = item.data.kind

  return item
})

connection.onRequest(
  "stimulus-lsp/controllerDefinitions",
  (request: ControllerDefinitionsRequestType): Promise<ControllerDefinitionsResponse> =>
    new ControllerDefinitionsRequest(service).handleRequest(request),
)

// Listen on the connection
connection.listen()


================================================
FILE: server/src/service.ts
================================================
import { Connection, InitializeParams } from "vscode-languageserver/node"
import { getLanguageService, LanguageService } from "@herb-tools/language-service"
import { Herb } from "@herb-tools/node-wasm"

import { StimulusHTMLDataProvider } from "./data_providers/stimulus_html_data_provider"
import { Settings } from "./settings"
import { DocumentService } from "./document_service"
import { Diagnostics } from "./diagnostics"
import { Definitions } from "./definitions"
import { Commands } from "./commands"
import { CodeActions } from "./code_actions"
import { Config } from "./config"
import { CodeLensProvider as CodeLens } from "./code_lens"

import { Project } from "stimulus-parser"

export class Service {
  connection: Connection
  settings: Settings
  htmlLanguageService: LanguageService
  stimulusDataProvider: StimulusHTMLDataProvider
  diagnostics: Diagnostics
  definitions: Definitions
  commands: Commands
  documentService: DocumentService
  codeActions: CodeActions
  project: Project
  codeLens: CodeLens
  config?: Config

  constructor(connection: Connection, params: InitializeParams) {
    this.connection = connection
    this.settings = new Settings(params, this.connection)
    this.documentService = new DocumentService(this.connection)
    this.project = new Project(this.settings.projectPath.replace("file://", ""))
    this.codeActions = new CodeActions(this.documentService, this.project)
    this.stimulusDataProvider = new StimulusHTMLDataProvider("id", this.project)
    this.diagnostics = new Diagnostics(this.connection, this.stimulusDataProvider, this.documentService, this.project, this)
    this.definitions = new Definitions(this.documentService, this.stimulusDataProvider)
    this.commands = new Commands(this.project, this.connection)
    this.codeLens = new CodeLens(this.documentService, this.project)

    this.htmlLanguageService = getLanguageService({
      herb: Herb,
      customDataProviders: [this.stimulusDataProvider],
      tokenListAttributes: ["data-controller", "data-action"],
    })
  }

  async init() {
    await Herb.load()
    await this.project.initialize()

    // TODO: we need to setup a file listener to check when new packages get installed
    await this.project.detectAvailablePackages()
    await this.project.analyzeAllDetectedModules()

    this.config = await Config.fromPathOrNew(this.project.projectPath)

    // Only keep settings for open documents
    this.documentService.onDidClose((change) => {
      this.settings.documentSettings.delete(change.document.uri)
    })

    // The content of a text document has changed. This event is emitted
    // when the text document first opened or when its content has changed.
    this.documentService.onDidChangeContent((change) => {
      this.diagnostics.refreshDocument(change.document)
    })
  }

  async refresh() {
    await this.project.refresh()

    this.diagnostics.refreshAllDocuments()
  }

  async refreshConfig() {
    this.config = await Config.fromPathOrNew(this.project.projectPath)
  }
}


================================================
FILE: server/src/settings.ts
================================================
import { ClientCapabilities, Connection, InitializeParams } from "vscode-languageserver/node"

export interface StimulusSettings {}

export class Settings {
  // The global settings, used when the `workspace/configuration` request is not supported by the client.
  // Please note that this is not the case when using this server with the client provided in this example
  // but could happen with other clients.
  defaultSettings: StimulusSettings = {}
  globalSettings: StimulusSettings = this.defaultSettings
  documentSettings: Map<string, Thenable<StimulusSettings>> = new Map()

  hasConfigurationCapability = false
  hasWorkspaceFolderCapability = false
  hasDiagnosticRelatedInformationCapability = false

  params: InitializeParams
  capabilities: ClientCapabilities
  connection: Connection

  constructor(params: InitializeParams, connection: Connection) {
    this.params = params
    this.capabilities = params.capabilities
    this.connection = connection

    // Does the client support the `workspace/configuration` request?
    // If not, we fall back using global settings.
    this.hasConfigurationCapability = !!(this.capabilities.workspace && !!this.capabilities.workspace.configuration)

    this.hasWorkspaceFolderCapability = !!(
      this.capabilities.workspace && !!this.capabilities.workspace.workspaceFolders
    )

    this.hasDiagnosticRelatedInformationCapability = !!(
      this.capabilities.textDocument &&
      this.capabilities.textDocument.publishDiagnostics &&
      this.capabilities.textDocument.publishDiagnostics.relatedInformation
    )
  }

  get projectPath() {
    return this.params.rootUri || ""
  }

  getDocumentSettings(resource: string): Thenable<StimulusSettings> {
    if (!this.hasConfigurationCapability) {
      return Promise.resolve(this.globalSettings)
    }

    let result = this.documentSettings.get(resource)

    if (!result) {
      result = this.connection.workspace.getConfiguration({
        scopeUri: resource,
        section: "languageServerStimulus",
      })
      this.documentSettings.set(resource, result)
    }

    return result
  }
}


================================================
FILE: server/src/utils.ts
================================================
import path from "path"
import { levenshtein } from "./levenshtein"

import type { Project, ExportDeclaration, ControllerDefinition } from "stimulus-parser"

function rank(input: string, list: string[]) {
  return list
    .map((item) => {
      const score = levenshtein(input.toLowerCase(), item.toLowerCase())

      return { item, score }
    })
    .sort((a, b) => a.score - b.score)
}

export function didyoumean(input: string, list: string[]): string | null {
  if (list.length === 0) return null

  const scores = rank(input, list)

  if (scores.length === 0) return null

  return scores[0].item
}

export function camelize(value: string) {
  return value.replace(/(?:[_-])([a-z0-9])/g, (_, char) => char.toUpperCase())
}

export function dasherize(value: string) {
  return value.replace(/([A-Z])/g, (_, char) => `-${char.toLowerCase()}`)
}

export function capitalize(value: string) {
  return value.charAt(0).toUpperCase() + value.slice(1)
}

export function importStatementForController(controllerDefinition: ControllerDefinition, project: Project) {
  const importSource = importSourceForController(controllerDefinition, project)
  const exportDeclaration = exportDeclarationFromControllerDefinition(controllerDefinition, project)

  if (!exportDeclaration) return { importStatement: undefined, localName: undefined, importSpecifier: undefined, importSource, exportDeclaration }

  return importStatementFromExportDeclaration(exportDeclaration, controllerDefinition, importSource)
}

export function importSourceForController(controllerDefinition: ControllerDefinition, project: Project) {
  if (controllerDefinition.sourceFile.isProjectFile) {
    return relativeControllersFilePath(project, controllerDefinition.sourceFile.path)
  }

  const nodeModule = nodeModleForController(controllerDefinition, project)

  return nodeModule?.name || ""
}

export function nodeModleForController(controllerDefinition: ControllerDefinition, project: Project) {
  return project.detectedNodeModules.find((module) => module.sourceFiles.includes(controllerDefinition.sourceFile))
}

export function localNameForExportDeclaration(exportDeclaration: ExportDeclaration, controller: ControllerDefinition) {
  return exportDeclaration.type === "default"
    ? controller.classDeclaration.className || `${capitalize(camelize(controller.guessedIdentifier))}Controller`
    : exportDeclaration.exportedName || controller.guessedIdentifier
}

export function importStatementFromExportDeclaration(
  exportDeclaration: ExportDeclaration,
  controller: ControllerDefinition,
  importSource: string,
) {
  const exportType = exportDeclaration?.type
  const localName = localNameForExportDeclaration(exportDeclaration, controller)
  const importSpecifier = exportType === "default" ? localName : `{ ${localName} }`
  const importStatement = `import ${importSpecifier} from "${importSource}"`

  return {
    exportDeclaration,
    localName,
    importSpecifier,
    importStatement,
    importSource,
  }
}

export function relativeControllersFilePath(project: Project, filePath: string): string {
  if (project.controllersIndexFiles.length === 0) return ""

  // TODO: Account for importmaps
  const relativePath = path.relative(
    path.dirname(project.controllersIndexFiles[0].path),
    filePath,
  )

  const fileName = path.basename(
    relativePath,
    path.extname(relativePath)
  )

  const controllerPath = path.join(
    path.dirname(relativePath),
    fileName
  )

  return controllerPath.startsWith(".") ? controllerPath : `./${controllerPath}`
}

export function exportDeclarationFromControllerDefinition(controllerDefinition: ControllerDefinition, project: Project) {
  if (controllerDefinition.sourceFile.isProjectFile) return controllerDefinition.classDeclaration.exportDeclaration

  const nodeModule = nodeModleForController(controllerDefinition, project)

  if (!nodeModule) return undefined

  return nodeModule.entrypointSourceFile?.exportDeclarations.find((exportDeclaration) => {
    try {
      return exportDeclaration.nextResolvedClassDeclaration?.controllerDefinition === controllerDefinition
    } catch(error: any) {
      return false
    }
  })
}


================================================
FILE: server/tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "es2019",
    "lib": ["ES2019", "dom"],
    "module": "commonjs",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "esModuleInterop": true,
    "sourceMap": true,
    "strict": true,
    "skipLibCheck": true,
    "outDir": "out",
    "rootDir": "src",
    "erasableSyntaxOnly": true,
    "typeRoots": [
      "./types",
      "./node_modules/@types"
    ]
  },
  "include": ["src"],
  "exclude": ["node_modules"]
}


================================================
FILE: server/types/typescript-eslint__typescript-estree/index.d.ts
================================================
declare module "@typescript-eslint/typescript-estree" {
  export * from "@typescript-eslint/typescript-estree/dist/index"
}


================================================
FILE: server/types/typescript-eslint__typescript-types/index.d.ts
================================================
declare module "@typescript-eslint/types" {
  export * from "@typescript-eslint/types/dist/index"
}


================================================
FILE: server/types/typescript-eslint__visitor-keys/index.d.ts
================================================
declare module "@typescript-eslint/visitor-keys" {
  export * from "@typescript-eslint/visitor-keys/dist/index"
}


================================================
FILE: tsconfig.json
================================================
{
  "compilerOptions": {
    "module": "commonjs",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "target": "es2019",
    "lib": ["ES2019"],
    "outDir": "out",
    "rootDir": "src",
    "sourceMap": true,
    "skipLibCheck": true,
    "erasableSyntaxOnly": true
  },
  "include": [
    "src"
  ],
  "exclude": [
    "node_modules",
    ".vscode-test"
  ],
  "references": [
    { "path": "./client" },
    { "path": "./server" },
  ]
}
Download .txt
gitextract_y0vvpjxj/

├── .eslintignore
├── .eslintrc
├── .github/
│   ├── dependabot.yml
│   └── workflows/
│       ├── build.yml
│       └── publish.yml
├── .gitignore
├── .node-version
├── .prettierrc.json
├── .vscode/
│   ├── extensions.json
│   ├── launch.json
│   ├── settings.json
│   └── tasks.json
├── .vscodeignore
├── LICENSE.txt
├── README.md
├── client/
│   ├── package.json
│   ├── src/
│   │   ├── client.ts
│   │   ├── controller_tree_view.ts
│   │   ├── extension.ts
│   │   └── requests.ts
│   └── tsconfig.json
├── package.json
├── scripts/
│   └── e2e.sh
├── server/
│   ├── .npmignore
│   ├── README.md
│   ├── package.json
│   ├── scripts/
│   │   └── executable.mjs
│   ├── src/
│   │   ├── action_descriptor.ts
│   │   ├── code_actions.ts
│   │   ├── code_lens.ts
│   │   ├── commands.ts
│   │   ├── config.ts
│   │   ├── data_providers/
│   │   │   └── stimulus_html_data_provider.ts
│   │   ├── definitions.ts
│   │   ├── diagnostics.ts
│   │   ├── document_service.ts
│   │   ├── events.ts
│   │   ├── html_util.ts
│   │   ├── levenshtein.ts
│   │   ├── requests/
│   │   │   └── controller_definitions.ts
│   │   ├── requests.ts
│   │   ├── server.ts
│   │   ├── service.ts
│   │   ├── settings.ts
│   │   └── utils.ts
│   ├── tsconfig.json
│   └── types/
│       ├── typescript-eslint__typescript-estree/
│       │   └── index.d.ts
│       ├── typescript-eslint__typescript-types/
│       │   └── index.d.ts
│       └── typescript-eslint__visitor-keys/
│           └── index.d.ts
└── tsconfig.json
Download .txt
SYMBOL INDEX (198 symbols across 21 files)

FILE: client/src/client.ts
  class Client (line 9) | class Client {
    method constructor (line 16) | constructor(context: ExtensionContext) {
    method start (line 29) | async start() {
    method stop (line 39) | async stop(): Promise<void> {
    method sendNotification (line 45) | async sendNotification(method: string, params: any) {
    method sendRequest (line 49) | async sendRequest<T>(method: string, params: any) {
    method requestControllerDefinitions (line 53) | async requestControllerDefinitions(): Promise<ControllerDefinitionsRes...
    method debugOptions (line 59) | private get debugOptions() {
    method serverOptions (line 67) | private get serverOptions(): ServerOptions {
    method clientOptions (line 81) | private get clientOptions(): LanguageClientOptions {

FILE: client/src/controller_tree_view.ts
  type ControllerDefinitionTreeItem (line 19) | type ControllerDefinitionTreeItem = ControllerTreeItem | ControllerDefin...
  class ControllerTreeView (line 21) | class ControllerTreeView implements TreeDataProvider<ControllerDefinitio...
    method constructor (line 28) | constructor(client: Client) {
    method dispose (line 48) | dispose() {
    method getTreeItem (line 53) | getTreeItem(element: ControllerDefinitionTreeItem) {
    method getChildren (line 57) | async getChildren(element?: ControllerDefinitionTreeItem) {
    method refresh (line 73) | refresh() {
    method registerControllerDefinition (line 77) | registerControllerDefinition(item: ControllerTreeItem) {
    method requestControllerDefinitions (line 90) | private async requestControllerDefinitions(): Promise<ControllerDefini...
  class ControllerDefinitionsStateItem (line 95) | class ControllerDefinitionsStateItem extends TreeItem {
    method constructor (line 98) | constructor(name: string, children: ControllerDefinitionsOrigin[]) {
    method getChildren (line 111) | getChildren() {
    method controllerTreeItems (line 115) | private get controllerTreeItems() {
    method controllerDefinitions (line 119) | private get controllerDefinitions(): [ControllerDefinition, Controller...
  class ControllerTreeItem (line 130) | class ControllerTreeItem extends TreeItem {
    method constructor (line 134) | constructor(item: ControllerDefinition, origin: ControllerDefinitionsO...
    method isImportable (line 156) | get isImportable() {
    method getChildren (line 164) | getChildren() {

FILE: client/src/extension.ts
  function activate (line 6) | async function activate(context: ExtensionContext) {
  function deactivate (line 12) | async function deactivate(): Promise<void> {

FILE: client/src/requests.ts
  type ControllerDefinition (line 3) | type ControllerDefinition = {
  type ControllerDefinitionsOrigin (line 12) | interface ControllerDefinitionsOrigin {
  type ProjectControllerDefinitions (line 17) | interface ProjectControllerDefinitions extends ControllerDefinitionsOrig...
  type ControllerDefinitionsRequest (line 21) | type ControllerDefinitionsRequest = object
  type ControllerDefinitionsResponse (line 22) | type ControllerDefinitionsResponse = {

FILE: server/src/action_descriptor.ts
  type ActionDescriptor (line 3) | interface ActionDescriptor {
  function parseActionDescriptorString (line 15) | function parseActionDescriptorString(descriptorString: string): Partial<...
  function parseEventOptions (line 36) | function parseEventOptions(eventOptions: string): AddEventListenerOptions {

FILE: server/src/code_actions.ts
  class CodeActions (line 13) | class CodeActions {
    method constructor (line 17) | constructor(documentService: DocumentService, project: Project) {
    method onCodeAction (line 22) | onCodeAction(params: CodeActionParams): CodeAction[] {
    method handleInvalidControllerDiagnostics (line 40) | private handleInvalidControllerDiagnostics(diagnostics: Diagnostic[]) {
    method handleInvalidActionDiagnostics (line 169) | private handleInvalidActionDiagnostics(diagnostics: Diagnostic[]) {
    method handleDeprecatedPackageImports (line 193) | private handleDeprecatedPackageImports(diagnostics: Diagnostic[]) {

FILE: server/src/code_lens.ts
  class CodeLensProvider (line 7) | class CodeLensProvider {
    method constructor (line 11) | constructor(documentService: DocumentService, project: Project) {
    method onCodeLens (line 16) | onCodeLens(params: CodeLensParams) {
    method onCodeLensResolve (line 57) | onCodeLensResolve(codeLens: CodeLens) {

FILE: server/src/commands.ts
  type SerializedTextDocument (line 8) | type SerializedTextDocument = {
  class Commands (line 16) | class Commands {
    method constructor (line 20) | constructor(project: Project, connection: Connection) {
    method updateControllerReference (line 25) | async updateControllerReference(identifier: string, diagnostic: Diagno...
    method registerControllerDefinition (line 40) | async registerControllerDefinition(importStatement: string, identifier...
    method createController (line 67) | async createController(identifier: string, diagnostic: Diagnostic, con...
    method updateControllerActionReference (line 90) | async updateControllerActionReference(actionName: string, diagnostic: ...
    method implementControllerAction (line 105) | async implementControllerAction(actionName: string, identifier: string...
    method updateImportSource (line 140) | async updateImportSource(diagnostic: Diagnostic) {
    method createStimulusLSPConfig (line 164) | async createStimulusLSPConfig() {
    method addIgnoredControllerToConfig (line 183) | async addIgnoredControllerToConfig(identifier: string) {
    method addIgnoredAttributeToConfig (line 197) | async addIgnoredAttributeToConfig(attribute: string) {
    method controllerTemplateFor (line 211) | private controllerTemplateFor(identifier: string) {

FILE: server/src/config.ts
  type StimulusConfigOptions (line 1) | type StimulusConfigOptions = {
  type StimulusLSPConfig (line 6) | type StimulusLSPConfig = {
  class Config (line 17) | class Config {
    method constructor (line 23) | constructor(projectPath: string, config: StimulusLSPConfig) {
    method version (line 28) | get version(): string {
    method createdAt (line 32) | get createdAt(): Date {
    method updatedAt (line 36) | get updatedAt(): Date {
    method options (line 40) | get options(): StimulusConfigOptions {
    method ignoredControllerIdentifiers (line 44) | get ignoredControllerIdentifiers(): Array<string> {
    method ignoredAttributes (line 48) | get ignoredAttributes(): Array<string> {
    method addIgnoredController (line 52) | public addIgnoredController(identifier: string) {
    method addIgnoredAttribute (line 59) | public addIgnoredAttribute(attribute: string) {
    method toJSON (line 66) | public toJSON() {
    method updateTimestamp (line 70) | private updateTimestamp() {
    method updateVersion (line 74) | private updateVersion() {
    method write (line 78) | async write() {
    method read (line 90) | async read() {
    method configPathFromProjectPath (line 94) | static configPathFromProjectPath(projectPath: string) {
    method fromPathOrNew (line 98) | static async fromPathOrNew(projectPath: string) {
    method fromPath (line 106) | static async fromPath(projectPath: string) {
    method newConfig (line 118) | static newConfig(projectPath: string): Config {

FILE: server/src/data_providers/stimulus_html_data_provider.ts
  class StimulusHTMLDataProvider (line 8) | class StimulusHTMLDataProvider implements IHTMLDataProvider {
    method constructor (line 12) | constructor(id: string, project: Project) {
    method controllers (line 17) | get controllers() {
    method controllerRoots (line 21) | get controllerRoots() {
    method isApplicable (line 25) | isApplicable() {
    method getId (line 29) | getId() {
    method provideTags (line 33) | provideTags() {
    method provideAttributes (line 37) | provideAttributes(_tag: string) {
    method provideValues (line 67) | provideValues(_tag: string, attribute: string) {

FILE: server/src/definitions.ts
  class Definitions (line 13) | class Definitions {
    method constructor (line 17) | constructor(documentService: DocumentService, stimulusDataProvider: St...
    method controllers (line 22) | get controllers() {
    method onDefinition (line 26) | onDefinition(params: DefinitionParams) {
    method resolveAttributeNameDefinition (line 81) | private resolveAttributeNameDefinition(
    method findControllerIdentifierInAttribute (line 130) | private findControllerIdentifierInAttribute(withoutPrefix: string): st...
    method resolveControllerDefinition (line 164) | private resolveControllerDefinition(
    method resolveActionDefinition (line 186) | private resolveActionDefinition(
    method controllerLinks (line 236) | private controllerLinks(identifiers: string[], originRange: Range): Lo...
    method methodLinks (line 251) | private methodLinks(identifier: string, methodName: string, originRang...
    method nextIndex (line 283) | private nextIndex(string: string, tokens: string[], offset: number) {
    method previousIndex (line 293) | private previousIndex(string: string, tokens: string[], offset: number) {

FILE: server/src/diagnostics.ts
  type InvalidControllerDiagnosticData (line 19) | interface InvalidControllerDiagnosticData {
  type DeprecatedPackageImportsDiagnosticData (line 24) | interface DeprecatedPackageImportsDiagnosticData {
  type InvalidActionDiagnosticData (line 31) | interface InvalidActionDiagnosticData {
  class Diagnostics (line 37) | class Diagnostics {
    method constructor (line 51) | constructor(
    method controllers (line 65) | get controllers() {
    method controllerIdentifiers (line 69) | get controllerIdentifiers() {
    method validateParsedControllerWithoutErrors (line 73) | validateParsedControllerWithoutErrors(node: Node, textDocument: TextDo...
    method populateSourceFileErrorsAsDiagnostics (line 89) | populateSourceFileErrorsAsDiagnostics(sourceFile: SourceFile, textDocu...
    method validateDataControllerAttribute (line 108) | validateDataControllerAttribute(node: Node, textDocument: TextDocument) {
    method validateDataActionAttribute (line 121) | validateDataActionAttribute(node: Node, textDocument: TextDocument) {
    method validateDataValueAttribute (line 159) | validateDataValueAttribute(node: Node, textDocument: TextDocument) {
    method validateDataClassAttribute (line 286) | validateDataClassAttribute(_node: Node, _textDocument: TextDocument) {
    method validateDataTargetAttribute (line 290) | validateDataTargetAttribute(node: Node, textDocument: TextDocument) {
    method validateStimulusImports (line 328) | validateStimulusImports(sourceFile: SourceFile, textDocument: TextDocu...
    method validateValueDefinitions (line 366) | validateValueDefinitions(sourceFile: SourceFile, textDocument: TextDoc...
    method visitNode (line 410) | visitNode(node: Node, textDocument: TextDocument) {
    method validate (line 423) | validate(textDocument: TextDocument) {
    method validateJavaScriptDocument (line 433) | validateJavaScriptDocument(textDocument: TextDocument) {
    method validateHTMLDocument (line 443) | validateHTMLDocument(textDocument: TextDocument) {
    method refreshDocument (line 452) | refreshDocument(document: TextDocument) {
    method refreshAllDocuments (line 456) | refreshAllDocuments() {
    method rangeFromLoc (line 462) | private rangeFromLoc(textDocument: TextDocument, loc?: Acorn.SourceLoc...
    method rangeFromNode (line 475) | private rangeFromNode(textDocument: TextDocument, node: Node) {
    method attributeNameRange (line 479) | private attributeNameRange(textDocument: TextDocument, node: Node, att...
    method rangeForAttributeName (line 496) | private rangeForAttributeName(
    method attributeValueRange (line 512) | private attributeValueRange(textDocument: TextDocument, node: Node, at...
    method rangeForAttributeValue (line 529) | private rangeForAttributeValue(
    method createParseErrorDiagnosticFor (line 547) | private createParseErrorDiagnosticFor(identifier: string, error: strin...
    method isIgnoredController (line 557) | private isIgnoredController(identifier: string) {
    method isIgnoredAttribute (line 563) | private isIgnoredAttribute(attribute: string) {
    method createInvalidControllerDiagnosticFor (line 569) | private createInvalidControllerDiagnosticFor(identifier: string, textD...
    method createInvalidActionDiagnosticFor (line 587) | private createInvalidActionDiagnosticFor(action: string, textDocument:...
    method createInvalidControllerActionDiagnosticFor (line 593) | private createInvalidControllerActionDiagnosticFor(
    method createAttributeFormatMismatchDiagnosticFor (line 612) | private createAttributeFormatMismatchDiagnosticFor(
    method createMissingValueOnControllerDiagnosticFor (line 627) | private createMissingValueOnControllerDiagnosticFor(
    method createMissingTargetOnControllerDiagnosticFor (line 646) | private createMissingTargetOnControllerDiagnosticFor(
    method createValueMismatchOnControllerDiagnosticFor (line 665) | private createValueMismatchOnControllerDiagnosticFor(
    method pushDiagnostic (line 682) | private pushDiagnostic(
    method sendDiagnosticsFor (line 707) | private sendDiagnosticsFor(textDocument: TextDocument) {
    method parseValueType (line 718) | private parseValueType(string: any) {
    method foundSkippableTags (line 732) | private foundSkippableTags(value: string) {

FILE: server/src/document_service.ts
  class DocumentService (line 4) | class DocumentService {
    method constructor (line 8) | constructor(connection: Connection) {
    method get (line 16) | get(uri: string) {
    method getAll (line 20) | getAll() {
    method onDidChangeContent (line 24) | get onDidChangeContent() {
    method onDidOpen (line 28) | get onDidOpen() {
    method onDidClose (line 32) | get onDidClose() {

FILE: server/src/events.ts
  constant EVENTS (line 1) | const EVENTS = [

FILE: server/src/html_util.ts
  function attributeValue (line 3) | function attributeValue(node: Node, attribute: string) {
  function tokenList (line 13) | function tokenList(node: Node, attribute: string) {
  function unquote (line 25) | function unquote(string: string) {
  function reverseString (line 29) | function reverseString(string: string) {
  function squish (line 33) | function squish(string: string) {
  function splitOnSpaceIgnoreTags (line 37) | function splitOnSpaceIgnoreTags(string: string) {

FILE: server/src/levenshtein.ts
  function levenshtein (line 25) | function levenshtein(a: string, b: string): number {

FILE: server/src/requests.ts
  type ControllerDefinition (line 3) | type ControllerDefinition = {
  type ControllerDefinitionsOrigin (line 12) | interface ControllerDefinitionsOrigin {
  type ProjectControllerDefinitions (line 17) | interface ProjectControllerDefinitions extends ControllerDefinitionsOrig...
  type ControllerDefinitionsRequest (line 21) | type ControllerDefinitionsRequest = object
  type ControllerDefinitionsResponse (line 22) | type ControllerDefinitionsResponse = {

FILE: server/src/requests/controller_definitions.ts
  class ControllerDefinitionsRequest (line 14) | class ControllerDefinitionsRequest {
    method constructor (line 17) | constructor(service: Service) {
    method handleRequest (line 21) | async handleRequest(_request: ControllerDefinitionsRequestType): Promi...
    method controllerSort (line 37) | private controllerSort(a: ControllerDefinitionRequestType, b: Controll...
    method positionFromNode (line 41) | private positionFromNode(node: ClassDeclarationNode | undefined) {
    method registeredControllerPaths (line 77) | private get registeredControllerPaths() {
    method unregisteredControllerDefinitions (line 81) | private get unregisteredControllerDefinitions() {
    method detectedNodeModules (line 87) | private get detectedNodeModules() {
    method registeredControllers (line 91) | private get registeredControllers() {
    method unregisteredControllers (line 95) | private get unregisteredControllers() {
    method nodeModuleControllers (line 99) | private get nodeModuleControllers() {

FILE: server/src/service.ts
  class Service (line 17) | class Service {
    method constructor (line 31) | constructor(connection: Connection, params: InitializeParams) {
    method init (line 50) | async init() {
    method refresh (line 72) | async refresh() {
    method refreshConfig (line 78) | async refreshConfig() {

FILE: server/src/settings.ts
  type StimulusSettings (line 3) | interface StimulusSettings {}
  class Settings (line 5) | class Settings {
    method constructor (line 21) | constructor(params: InitializeParams, connection: Connection) {
    method projectPath (line 41) | get projectPath() {
    method getDocumentSettings (line 45) | getDocumentSettings(resource: string): Thenable<StimulusSettings> {

FILE: server/src/utils.ts
  function rank (line 6) | function rank(input: string, list: string[]) {
  function didyoumean (line 16) | function didyoumean(input: string, list: string[]): string | null {
  function camelize (line 26) | function camelize(value: string) {
  function dasherize (line 30) | function dasherize(value: string) {
  function capitalize (line 34) | function capitalize(value: string) {
  function importStatementForController (line 38) | function importStatementForController(controllerDefinition: ControllerDe...
  function importSourceForController (line 47) | function importSourceForController(controllerDefinition: ControllerDefin...
  function nodeModleForController (line 57) | function nodeModleForController(controllerDefinition: ControllerDefiniti...
  function localNameForExportDeclaration (line 61) | function localNameForExportDeclaration(exportDeclaration: ExportDeclarat...
  function importStatementFromExportDeclaration (line 67) | function importStatementFromExportDeclaration(
  function relativeControllersFilePath (line 86) | function relativeControllersFilePath(project: Project, filePath: string)...
  function exportDeclarationFromControllerDefinition (line 108) | function exportDeclarationFromControllerDefinition(controllerDefinition:...
Condensed preview — 50 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (128K chars).
[
  {
    "path": ".eslintignore",
    "chars": 100,
    "preview": "node_modules/**\nclient/node_modules/**\nclient/out/**\nserver/node_modules/**\nserver/out/**\n**/*.d.ts\n"
  },
  {
    "path": ".eslintrc",
    "chars": 497,
    "preview": "{\n  \"root\": true,\n  \"parser\": \"@typescript-eslint/parser\",\n  \"plugins\": [\n    \"@typescript-eslint\",\n    \"prettier\"\n  ],\n"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 909,
    "preview": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where "
  },
  {
    "path": ".github/workflows/build.yml",
    "chars": 508,
    "preview": "name: Build\n\npermissions:\n  contents: read\n\non: [push, pull_request]\n\njobs:\n  tests:\n    name: JavaScript Test Action\n  "
  },
  {
    "path": ".github/workflows/publish.yml",
    "chars": 577,
    "preview": "name: Publish\n\npermissions:\n  contents: read\n\non:\n  release:\n    types:\n      - created\n\njobs:\n  publish:\n    name: Publ"
  },
  {
    "path": ".gitignore",
    "chars": 95,
    "preview": "client/out/\nserver/out/\nnode_modules/\n\n.vscode-test/\n\n*.tsbuildinfo\n*.vsix\n*.tgz\n*~\n\n.DS_Store\n"
  },
  {
    "path": ".node-version",
    "chars": 8,
    "preview": "20.14.0\n"
  },
  {
    "path": ".prettierrc.json",
    "chars": 70,
    "preview": " {\n   \"singleQuote\": false,\n   \"printWidth\": 120,\n   \"semi\": false\n }\n"
  },
  {
    "path": ".vscode/extensions.json",
    "chars": 319,
    "preview": "{\n  // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.\n  // Extension ident"
  },
  {
    "path": ".vscode/launch.json",
    "chars": 1232,
    "preview": "// A launch configuration that compiles the extension and then opens it inside a new window\n{\n  \"version\": \"0.2.0\",\n  \"c"
  },
  {
    "path": ".vscode/settings.json",
    "chars": 223,
    "preview": "{\n  \"editor.insertSpaces\": false,\n  \"tslint.enable\": true,\n  \"typescript.tsc.autoDetect\": \"off\",\n  \"typescript.preferenc"
  },
  {
    "path": ".vscode/tasks.json",
    "chars": 573,
    "preview": "{\n  \"version\": \"2.0.0\",\n  \"tasks\": [\n    {\n      \"type\": \"npm\",\n      \"script\": \"compile\",\n      \"group\": \"build\",\n     "
  },
  {
    "path": ".vscodeignore",
    "chars": 549,
    "preview": ".gitignore\n.eslintrc\n.eslintignore\n.prettierrc.json\n.node-version\n\n**/*.ts\n**/*.map\n**/tsconfig.json\n**/tsconfig.base.js"
  },
  {
    "path": "LICENSE.txt",
    "chars": 1067,
    "preview": "MIT License\n\nCopyright (c) 2021 Marco Roth\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
  },
  {
    "path": "README.md",
    "chars": 3594,
    "preview": "# Stimulus LSP\n\nIntelligent Stimulus tooling for Visual Studio Code and other editors which support the Language Server "
  },
  {
    "path": "client/package.json",
    "chars": 554,
    "preview": "{\n  \"name\": \"vscode-stimulus\",\n  \"description\": \"Intelligent Stimulus tooling for Visual Studio Code\",\n  \"author\": \"Marc"
  },
  {
    "path": "client/src/client.ts",
    "chars": 2963,
    "preview": "import * as path from \"path\"\n\nimport { workspace, ExtensionContext } from \"vscode\"\nimport { LanguageClient, LanguageClie"
  },
  {
    "path": "client/src/controller_tree_view.ts",
    "chars": 5133,
    "preview": "import {\n  TreeView,\n  TreeItem,\n  TreeItemCollapsibleState,\n  TreeDataProvider,\n  Disposable,\n  ThemeIcon,\n  EventEmitt"
  },
  {
    "path": "client/src/extension.ts",
    "chars": 345,
    "preview": "import { ExtensionContext } from \"vscode\"\nimport { Client } from \"./client\"\n\nlet client: Client\n\nexport async function a"
  },
  {
    "path": "client/src/requests.ts",
    "chars": 687,
    "preview": "import { Position } from \"vscode-languageclient\"\n\nexport type ControllerDefinition = {\n  identifier: string\n  path: stri"
  },
  {
    "path": "client/tsconfig.json",
    "chars": 280,
    "preview": "{\n  \"compilerOptions\": {\n    \"module\": \"commonjs\",\n    \"target\": \"es2019\",\n    \"lib\": [\"ES2019\"],\n    \"outDir\": \"out\",\n "
  },
  {
    "path": "package.json",
    "chars": 3879,
    "preview": "{\n  \"name\": \"stimulus-lsp\",\n  \"displayName\": \"Stimulus LSP\",\n  \"description\": \"Intelligent Stimulus tooling\",\n  \"license"
  },
  {
    "path": "scripts/e2e.sh",
    "chars": 163,
    "preview": "#!/usr/bin/env bash\n\nexport CODE_TESTS_PATH=\"$(pwd)/client/out/test\"\nexport CODE_TESTS_WORKSPACE=\"$(pwd)/client/testFixt"
  },
  {
    "path": "server/.npmignore",
    "chars": 216,
    "preview": ".babelrc\n.babelrc.js\n.DS_Store\n.gitignore\n.yarn.lock\n\n*.log\n*.tsbuildinfo\n*.tgz\n\nREADME.md\nrollup.config.js\ntsconfig.jso"
  },
  {
    "path": "server/README.md",
    "chars": 610,
    "preview": "# Stimulus Language Server\n\n[Language Server Protocol](https://github.com/Microsoft/language-server-protocol) implementa"
  },
  {
    "path": "server/package.json",
    "chars": 1310,
    "preview": "{\n  \"name\": \"stimulus-language-server\",\n  \"description\": \"Intelligent Stimulus tooling\",\n  \"version\": \"1.1.0\",\n  \"author"
  },
  {
    "path": "server/scripts/executable.mjs",
    "chars": 673,
    "preview": "import { readFileSync, writeFileSync } from 'fs'\nimport { dirname, resolve } from 'path'\nimport { fileURLToPath } from '"
  },
  {
    "path": "server/src/action_descriptor.ts",
    "chars": 1378,
    "preview": "// https://github.com/hotwired/stimulus/blob/8cbca6db3b1b2ddb384deb3dd98397d3609d25a0/src/core/action_descriptor.ts\n\nexp"
  },
  {
    "path": "server/src/code_actions.ts",
    "chars": 7697,
    "preview": "import { CodeAction, CodeActionKind, CodeActionParams, Command, Diagnostic } from \"vscode-languageserver/node\"\n\nimport {"
  },
  {
    "path": "server/src/code_lens.ts",
    "chars": 2536,
    "preview": "import { CodeLens, CodeLensParams, Range, Command } from \"vscode-languageserver/node\"\n\nimport { DocumentService } from \""
  },
  {
    "path": "server/src/commands.ts",
    "chars": 7708,
    "preview": "import dedent from \"dedent\"\n\nimport { Connection, TextDocumentEdit, TextEdit, CreateFile, Range, Diagnostic } from \"vsco"
  },
  {
    "path": "server/src/config.ts",
    "chars": 3146,
    "preview": "export type StimulusConfigOptions = {\n  ignoredControllerIdentifiers: Array<string>\n  ignoredAttributes: Array<string>\n}"
  },
  {
    "path": "server/src/data_providers/stimulus_html_data_provider.ts",
    "chars": 6481,
    "preview": "import { IHTMLDataProvider } from \"@herb-tools/language-service\"\n\nimport { EVENTS } from \"../events\"\n\nimport { Project }"
  },
  {
    "path": "server/src/definitions.ts",
    "chars": 9827,
    "preview": "import { Herb } from \"@herb-tools/node-wasm\"\nimport { Range, DefinitionParams, LocationLink } from \"vscode-languageserve"
  },
  {
    "path": "server/src/diagnostics.ts",
    "chars": 25247,
    "preview": "import dedent from \"dedent\"\nimport { Connection, Diagnostic, DiagnosticSeverity, Position, Range } from \"vscode-language"
  },
  {
    "path": "server/src/document_service.ts",
    "chars": 814,
    "preview": "import { Connection, TextDocuments } from \"vscode-languageserver/node\"\nimport { TextDocument } from \"vscode-languageserv"
  },
  {
    "path": "server/src/events.ts",
    "chars": 984,
    "preview": "export const EVENTS = [\n  \"DOMContentLoaded\",\n  \"abort\",\n  \"animationcancel\",\n  \"animationend\",\n  \"animationiteration\",\n"
  },
  {
    "path": "server/src/html_util.ts",
    "chars": 1120,
    "preview": "import { Node } from \"@herb-tools/language-service\"\n\nexport function attributeValue(node: Node, attribute: string) {\n  i"
  },
  {
    "path": "server/src/levenshtein.ts",
    "chars": 3020,
    "preview": "/*\n * The following code is derived from the \"js-levenshtein\" repository,\n * Copyright (c) 2017 Gustaf Andersson (https:"
  },
  {
    "path": "server/src/requests/controller_definitions.ts",
    "chars": 3761,
    "preview": "import { Position } from \"vscode-languageserver/node\"\nimport { RegisteredController, ControllerDefinition, ClassDeclarat"
  },
  {
    "path": "server/src/requests.ts",
    "chars": 687,
    "preview": "import { Position } from \"vscode-languageserver\"\n\nexport type ControllerDefinition = {\n  identifier: string\n  path: stri"
  },
  {
    "path": "server/src/server.ts",
    "chars": 6983,
    "preview": "import {\n  createConnection,\n  ProposedFeatures,\n  InitializeParams,\n  DidChangeConfigurationNotification,\n  DidChangeWa"
  },
  {
    "path": "server/src/service.ts",
    "chars": 3033,
    "preview": "import { Connection, InitializeParams } from \"vscode-languageserver/node\"\nimport { getLanguageService, LanguageService }"
  },
  {
    "path": "server/src/settings.ts",
    "chars": 2115,
    "preview": "import { ClientCapabilities, Connection, InitializeParams } from \"vscode-languageserver/node\"\n\nexport interface Stimulus"
  },
  {
    "path": "server/src/utils.ts",
    "chars": 4171,
    "preview": "import path from \"path\"\nimport { levenshtein } from \"./levenshtein\"\n\nimport type { Project, ExportDeclaration, Controlle"
  },
  {
    "path": "server/tsconfig.json",
    "chars": 472,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"es2019\",\n    \"lib\": [\"ES2019\", \"dom\"],\n    \"module\": \"commonjs\",\n    \"moduleReso"
  },
  {
    "path": "server/types/typescript-eslint__typescript-estree/index.d.ts",
    "chars": 124,
    "preview": "declare module \"@typescript-eslint/typescript-estree\" {\n  export * from \"@typescript-eslint/typescript-estree/dist/index"
  },
  {
    "path": "server/types/typescript-eslint__typescript-types/index.d.ts",
    "chars": 100,
    "preview": "declare module \"@typescript-eslint/types\" {\n  export * from \"@typescript-eslint/types/dist/index\"\n}\n"
  },
  {
    "path": "server/types/typescript-eslint__visitor-keys/index.d.ts",
    "chars": 114,
    "preview": "declare module \"@typescript-eslint/visitor-keys\" {\n  export * from \"@typescript-eslint/visitor-keys/dist/index\"\n}\n"
  },
  {
    "path": "tsconfig.json",
    "chars": 456,
    "preview": "{\n  \"compilerOptions\": {\n    \"module\": \"commonjs\",\n    \"moduleResolution\": \"node\",\n    \"esModuleInterop\": true,\n    \"tar"
  }
]

About this extraction

This page contains the full source code of the marcoroth/stimulus-lsp GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 50 files (116.3 KB), approximately 28.0k tokens, and a symbol index with 198 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!