diff --git a/packages/emacs/README.md b/packages/emacs/README.md index 4e37cc734..06ec15b11 100644 --- a/packages/emacs/README.md +++ b/packages/emacs/README.md @@ -2,39 +2,86 @@ Provides a major mode for editing Dotprompt (`.prompt`) files with syntax highlighting and LSP support. +## Features + +- **Syntax Highlighting**: Handlebars helpers, partials, Dotprompt markers +- **LSP Integration**: Diagnostics, formatting, hover via eglot or lsp-mode +- **Format Buffer**: `C-c C-f` or `M-x dotprompt-format-buffer` +- **Format on Save**: Optional automatic formatting + ## Installation -### Manual +### use-package (Recommended) + +```elisp +(use-package dotprompt-mode + :load-path "path/to/dotprompt/packages/emacs" + :mode "\\.prompt\\'" + :custom + (dotprompt-promptly-path "promptly") + (dotprompt-format-on-save t)) +``` -1. Copy `dotprompt-mode.el` to your load path (e.g., `~/.emacs.d/lisp/`). -2. Add the following to your init file: +### straight.el ```elisp -(add-to-list 'load-path "~/.emacs.d/lisp/") -(require 'dotprompt-mode) +(straight-use-package + '(dotprompt-mode :type git + :host github + :repo "google/dotprompt" + :files ("packages/emacs/*.el"))) + +(use-package dotprompt-mode + :mode "\\.prompt\\'" + :custom + (dotprompt-format-on-save t)) +``` + +### Doom Emacs + +Add to `packages.el`: + +```elisp +(package! dotprompt-mode + :recipe (:host github + :repo "google/dotprompt" + :files ("packages/emacs/*.el"))) +``` + +Add to `config.el`: + +```elisp +(use-package! dotprompt-mode + :mode "\\.prompt\\'" + :config + (setq dotprompt-format-on-save t) + (add-hook 'dotprompt-mode-hook #'eglot-ensure)) ``` -### use-package +### Spacemacs + +Add to your `dotspacemacs/user-config`: ```elisp (use-package dotprompt-mode :load-path "path/to/dotprompt/packages/emacs" - :mode "\\.prompt\\'") + :mode "\\.prompt\\'" + :init + (spacemacs/set-leader-keys-for-major-mode 'dotprompt-mode + "f" 'dotprompt-format-buffer)) ``` -## Features +### Manual -- **Syntax highlighting** for: - - Dotprompt markers (`<<>>`) - - Handlebars helpers (`if`, `unless`, `each`) - - Dotprompt custom helpers (`json`, `role`, `history`) - - Partials (`{{> ... }}`) -- **Auto-detection** of `.prompt` files -- **LSP integration** via eglot or lsp-mode +1. Copy `dotprompt-mode.el` to your load path (e.g., `~/.emacs.d/lisp/`) +2. Add to your init file: -## LSP Support +```elisp +(add-to-list 'load-path "~/.emacs.d/lisp/") +(require 'dotprompt-mode) +``` -For diagnostics, formatting, and hover documentation, install `promptly`: +## Install promptly for LSP Features ```bash cargo install --path rs/promptly @@ -42,20 +89,16 @@ cargo install --path rs/promptly cargo build --release -p promptly ``` +## LSP Support + ### Using Eglot (Emacs 29+, built-in) -Eglot integration is automatic. Just enable eglot in your dotprompt buffers: +Eglot integration is automatic. Enable in your dotprompt buffers: ```elisp (add-hook 'dotprompt-mode-hook 'eglot-ensure) ``` -If `promptly` is not in your PATH, customize the path: - -```elisp -(setq dotprompt-promptly-path "/path/to/promptly") -``` - ### Using lsp-mode lsp-mode integration is also automatic: @@ -64,15 +107,42 @@ lsp-mode integration is also automatic: (add-hook 'dotprompt-mode-hook 'lsp-deferred) ``` -### LSP Features - -With LSP enabled, you get: -- **Diagnostics**: Real-time error detection for YAML and Handlebars syntax -- **Formatting**: Format buffer with `M-x lsp-format-buffer` or `M-x eglot-format-buffer` -- **Hover**: Documentation for Handlebars helpers and frontmatter fields (`M-x eldoc` or hover) - ## Configuration | Variable | Default | Description | |----------|---------|-------------| | `dotprompt-promptly-path` | `"promptly"` | Path to the promptly executable | +| `dotprompt-format-on-save` | `nil` | Format buffer before saving | + +### Custom promptly path + +```elisp +(setq dotprompt-promptly-path "/path/to/promptly") +``` + +### Enable format on save + +```elisp +(setq dotprompt-format-on-save t) +``` + +## Keybindings + +| Key | Command | Description | +|-----|---------|-------------| +| `C-c C-f` | `dotprompt-format-buffer` | Format the current buffer | + +## LSP Features + +With LSP enabled (eglot or lsp-mode), you get: + +| Feature | Description | +|---------|-------------| +| **Diagnostics** | Real-time error detection for YAML and Handlebars syntax | +| **Formatting** | Format with `M-x eglot-format-buffer` or `M-x lsp-format-buffer` | +| **Hover** | Documentation with `M-x eldoc` or mouse hover | +| **Go to Definition** | Jump to partial files with `M-.` | + +## Commands + +- `dotprompt-format-buffer` - Format the current buffer using LSP or promptly directly diff --git a/packages/emacs/dotprompt-mode.el b/packages/emacs/dotprompt-mode.el index 53150eb39..be9118ded 100644 --- a/packages/emacs/dotprompt-mode.el +++ b/packages/emacs/dotprompt-mode.el @@ -15,9 +15,10 @@ ;; limitations under the License. ;; Author: Google -;; Version: 0.2.0 +;; Version: 0.3.0 ;; Keywords: languages, dotprompt ;; URL: https://github.com/google/dotprompt +;; Package-Requires: ((emacs "27.1")) ;;; Commentary: @@ -26,6 +27,12 @@ ;; Includes LSP integration via eglot or lsp-mode for diagnostics, ;; formatting, and hover documentation when `promptly` is installed. ;; +;; Features: +;; - Syntax highlighting for Handlebars templates +;; - LSP integration via eglot (Emacs 29+) or lsp-mode +;; - Format buffer command +;; - Format on save (optional) +;; ;; For best results with frontmatter, consider using polymode or mmm-mode. ;;; Code: @@ -40,6 +47,11 @@ :type 'string :group 'dotprompt) +(defcustom dotprompt-format-on-save nil + "When non-nil, format the buffer before saving." + :type 'boolean + :group 'dotprompt) + (defvar dotprompt-mode-hook nil "Hook run after entering `dotprompt-mode'.") @@ -66,6 +78,12 @@ ) "Minimal highlighting for Dotprompt.") +(defvar dotprompt-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd "C-c C-f") #'dotprompt-format-buffer) + map) + "Keymap for `dotprompt-mode'.") + ;;;###autoload (define-derived-mode dotprompt-mode prog-mode "Dotprompt" "Major mode for editing Dotprompt files." @@ -79,11 +97,48 @@ (setq-local tab-width 2) ;; Font lock - (setq-local font-lock-defaults '(dotprompt-font-lock-keywords))) + (setq-local font-lock-defaults '(dotprompt-font-lock-keywords)) + + ;; Format on save hook + (when dotprompt-format-on-save + (add-hook 'before-save-hook #'dotprompt-format-buffer nil t))) ;;;###autoload (add-to-list 'auto-mode-alist '("\\.prompt\\'" . dotprompt-mode)) +;;; Format Command + +(defun dotprompt-format-buffer () + "Format the current buffer using promptly or LSP. +If an LSP client is connected, use LSP formatting. +Otherwise, call promptly fmt directly." + (interactive) + (cond + ;; Try eglot first (Emacs 29+) + ((and (fboundp 'eglot-managed-p) (eglot-managed-p)) + (eglot-format-buffer)) + ;; Try lsp-mode + ((and (fboundp 'lsp-workspaces) (lsp-workspaces)) + (lsp-format-buffer)) + ;; Fall back to direct promptly call + (t + (dotprompt--format-with-promptly)))) + +(defun dotprompt--format-with-promptly () + "Format the current buffer using promptly fmt." + (let ((temp-file (make-temp-file "dotprompt-format" nil ".prompt")) + (original-point (point))) + (unwind-protect + (progn + (write-region (point-min) (point-max) temp-file nil 'silent) + (let ((exit-code (call-process dotprompt-promptly-path nil nil nil + "fmt" temp-file))) + (when (zerop exit-code) + (erase-buffer) + (insert-file-contents temp-file) + (goto-char (min original-point (point-max)))))) + (delete-file temp-file)))) + ;;; LSP Integration ;; Eglot integration (built-in to Emacs 29+) @@ -106,4 +161,3 @@ (provide 'dotprompt-mode) ;;; dotprompt-mode.el ends here - diff --git a/packages/jetbrains/README.md b/packages/jetbrains/README.md index 77fabf149..9eeaf4d81 100644 --- a/packages/jetbrains/README.md +++ b/packages/jetbrains/README.md @@ -6,7 +6,9 @@ Language support for Dotprompt (`.prompt`) files in JetBrains IDEs (IntelliJ IDE - **Syntax Highlighting**: YAML frontmatter, Handlebars templates, Dotprompt markers - **LSP Integration**: Real-time diagnostics, formatting, and hover documentation (via LSP4IJ) +- **Live Templates**: Type `role`, `if`, `each`, `json` + Tab for quick insertions - **Code Comments**: Block comment support using `{{! ... }}` +- **Settings UI**: Configure promptly path and format on save ## Requirements @@ -35,7 +37,7 @@ Language support for Dotprompt (`.prompt`) files in JetBrains IDEs (IntelliJ IDE 3. **Install**: - Go to **Settings/Preferences** → **Plugins** → **⚙️** → **Install Plugin from Disk...** - - Select `build/distributions/dotprompt-intellij-0.1.0.zip` + - Select `build/distributions/dotprompt-intellij-0.2.0.zip` ### From Source (Bazel) @@ -72,6 +74,30 @@ cargo build --release -p promptly ./gradlew test ``` +## Live Templates + +Type these abbreviations and press Tab to expand: + +| Abbreviation | Expands To | +|--------------|------------| +| `role` | Role block with customizable role name | +| `system` | System role block | +| `user` | User role block | +| `model` | Model role block | +| `if` | Handlebars if block | +| `ifelse` | Handlebars if-else block | +| `unless` | Handlebars unless block | +| `each` | Handlebars each loop | +| `with` | Handlebars with block | +| `json` | JSON serialization helper | +| `media` | Media embedding helper | +| `history` | History insertion | +| `section` | Named section block | +| `partial` | Partial template inclusion | +| `comment` | Handlebars comment | +| `prompt` | Complete prompt template | +| `frontmatter` | YAML frontmatter | + ## LSP Features When `promptly` is installed and in your PATH, you get: @@ -84,9 +110,20 @@ When `promptly` is installed and in your PATH, you get: ## Configuration +### Settings UI + +Go to **Settings/Preferences** → **Languages & Frameworks** → **Dotprompt**: + +- **Promptly path**: Custom path to the promptly executable +- **Enable LSP features**: Toggle diagnostics, formatting, hover +- **Format on save**: Automatically format when saving + +### Auto-Detection + The plugin automatically finds `promptly` in: -1. System PATH -2. `~/.cargo/bin/promptly` +1. User-configured path (Settings) +2. System PATH +3. `~/.cargo/bin/promptly` ## Architecture diff --git a/packages/jetbrains/src/main/kotlin/com/google/dotprompt/DotpromptSettings.kt b/packages/jetbrains/src/main/kotlin/com/google/dotprompt/DotpromptSettings.kt new file mode 100644 index 000000000..116c6f344 --- /dev/null +++ b/packages/jetbrains/src/main/kotlin/com/google/dotprompt/DotpromptSettings.kt @@ -0,0 +1,74 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package com.google.dotprompt + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.PersistentStateComponent +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage + +/** + * Persistent settings for the Dotprompt plugin. + */ +@Service(Service.Level.APP) +@State( + name = "DotpromptSettings", + storages = [Storage("dotprompt.xml")] +) +class DotpromptSettings : PersistentStateComponent { + + /** + * Settings state data class. + */ + data class State( + /** Custom path to the promptly executable. */ + var promptlyPath: String = "", + /** Whether to enable format on save. */ + var formatOnSave: Boolean = true, + /** Whether to enable LSP features. */ + var enableLsp: Boolean = true + ) + + private var myState = State() + + override fun getState(): State = myState + + override fun loadState(state: State) { + myState = state + } + + /** Custom path to the promptly executable. Empty string means auto-detect. */ + var promptlyPath: String + get() = myState.promptlyPath + set(value) { myState.promptlyPath = value } + + /** Whether to enable format on save. */ + var formatOnSave: Boolean + get() = myState.formatOnSave + set(value) { myState.formatOnSave = value } + + /** Whether to enable LSP features. */ + var enableLsp: Boolean + get() = myState.enableLsp + set(value) { myState.enableLsp = value } + + companion object { + fun getInstance(): DotpromptSettings = + ApplicationManager.getApplication().getService(DotpromptSettings::class.java) + } +} diff --git a/packages/jetbrains/src/main/kotlin/com/google/dotprompt/DotpromptSettingsConfigurable.kt b/packages/jetbrains/src/main/kotlin/com/google/dotprompt/DotpromptSettingsConfigurable.kt new file mode 100644 index 000000000..3f9102101 --- /dev/null +++ b/packages/jetbrains/src/main/kotlin/com/google/dotprompt/DotpromptSettingsConfigurable.kt @@ -0,0 +1,97 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package com.google.dotprompt + +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory +import com.intellij.openapi.options.Configurable +import com.intellij.openapi.ui.TextFieldWithBrowseButton +import com.intellij.ui.components.JBCheckBox +import com.intellij.ui.components.JBLabel +import com.intellij.util.ui.FormBuilder +import javax.swing.JComponent +import javax.swing.JPanel + +/** + * Settings UI for the Dotprompt plugin. + * Accessible via Settings > Languages & Frameworks > Dotprompt. + */ +class DotpromptSettingsConfigurable : Configurable { + + private var promptlyPathField: TextFieldWithBrowseButton? = null + private var formatOnSaveCheckbox: JBCheckBox? = null + private var enableLspCheckbox: JBCheckBox? = null + + override fun getDisplayName(): String = "Dotprompt" + + override fun createComponent(): JComponent { + promptlyPathField = TextFieldWithBrowseButton().apply { + addBrowseFolderListener( + "Select Promptly Executable", + "Select the path to the promptly executable", + null, + FileChooserDescriptorFactory.createSingleFileDescriptor() + ) + } + + formatOnSaveCheckbox = JBCheckBox("Format on save") + enableLspCheckbox = JBCheckBox("Enable LSP features (diagnostics, formatting, hover)") + + return FormBuilder.createFormBuilder() + .addLabeledComponent( + JBLabel("Promptly path:"), + promptlyPathField!!, + 1, + false + ) + .addComponent( + JBLabel("Leave empty to auto-detect from PATH or ~/.cargo/bin"), + 0 + ) + .addSeparator() + .addComponent(enableLspCheckbox!!, 1) + .addComponent(formatOnSaveCheckbox!!, 1) + .addComponentFillVertically(JPanel(), 0) + .panel + } + + override fun isModified(): Boolean { + val settings = DotpromptSettings.getInstance() + return promptlyPathField?.text != settings.promptlyPath || + formatOnSaveCheckbox?.isSelected != settings.formatOnSave || + enableLspCheckbox?.isSelected != settings.enableLsp + } + + override fun apply() { + val settings = DotpromptSettings.getInstance() + settings.promptlyPath = promptlyPathField?.text ?: "" + settings.formatOnSave = formatOnSaveCheckbox?.isSelected ?: true + settings.enableLsp = enableLspCheckbox?.isSelected ?: true + } + + override fun reset() { + val settings = DotpromptSettings.getInstance() + promptlyPathField?.text = settings.promptlyPath + formatOnSaveCheckbox?.isSelected = settings.formatOnSave + enableLspCheckbox?.isSelected = settings.enableLsp + } + + override fun disposeUIResources() { + promptlyPathField = null + formatOnSaveCheckbox = null + enableLspCheckbox = null + } +} diff --git a/packages/jetbrains/src/main/kotlin/com/google/dotprompt/DotpromptTemplateContext.kt b/packages/jetbrains/src/main/kotlin/com/google/dotprompt/DotpromptTemplateContext.kt new file mode 100644 index 000000000..49be9803c --- /dev/null +++ b/packages/jetbrains/src/main/kotlin/com/google/dotprompt/DotpromptTemplateContext.kt @@ -0,0 +1,33 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package com.google.dotprompt + +import com.intellij.codeInsight.template.TemplateActionContext +import com.intellij.codeInsight.template.TemplateContextType + +/** + * Template context for Dotprompt live templates. + * Enables live templates when editing .prompt files. + */ +class DotpromptTemplateContext : TemplateContextType("Dotprompt") { + + override fun isInContext(templateActionContext: TemplateActionContext): Boolean { + val file = templateActionContext.file + return file.fileType == DotpromptFileType.INSTANCE || + file.name.endsWith(".prompt") + } +} diff --git a/packages/jetbrains/src/main/kotlin/com/google/dotprompt/PromptlyServerFactory.kt b/packages/jetbrains/src/main/kotlin/com/google/dotprompt/PromptlyServerFactory.kt index 1d7bf30da..a1ed00227 100644 --- a/packages/jetbrains/src/main/kotlin/com/google/dotprompt/PromptlyServerFactory.kt +++ b/packages/jetbrains/src/main/kotlin/com/google/dotprompt/PromptlyServerFactory.kt @@ -35,9 +35,24 @@ class PromptlyServerFactory : LanguageServerFactory { /** * Finds the promptly executable in common locations. + * + * Priority: + * 1. User-configured path in settings + * 2. System PATH + * 3. ~/.cargo/bin/promptly + * 4. Fallback to "promptly" (hope it's in PATH) */ private fun findPromptlyExecutable(): String { - // Check PATH first + // Check user settings first + val settings = DotpromptSettings.getInstance() + if (settings.promptlyPath.isNotBlank()) { + val configuredPath = File(settings.promptlyPath) + if (configuredPath.exists() && configuredPath.canExecute()) { + return configuredPath.absolutePath + } + } + + // Check PATH val pathDirs = System.getenv("PATH")?.split(File.pathSeparator) ?: emptyList() for (dir in pathDirs) { val promptly = File(dir, "promptly") diff --git a/packages/jetbrains/src/main/resources/META-INF/plugin.xml b/packages/jetbrains/src/main/resources/META-INF/plugin.xml index ce69390c2..a13df2629 100644 --- a/packages/jetbrains/src/main/resources/META-INF/plugin.xml +++ b/packages/jetbrains/src/main/resources/META-INF/plugin.xml @@ -30,6 +30,7 @@
  • Real-time diagnostics via LSP (requires promptly)
  • Document formatting
  • Hover documentation for helpers and frontmatter fields
  • +
  • Live templates for common patterns (role, if, each, json, etc.)
  • Installation

    @@ -38,6 +39,12 @@ ]]> 0.2.0 +
      +
    • Live templates for common patterns
    • +
    • Settings UI for promptly path configuration
    • +
    • Format on save option
    • +

    0.1.0

    • Initial release
    • @@ -72,6 +79,21 @@ + + + + + + + + @@ -87,3 +109,4 @@ + diff --git a/packages/jetbrains/src/main/resources/liveTemplates/Dotprompt.xml b/packages/jetbrains/src/main/resources/liveTemplates/Dotprompt.xml new file mode 100644 index 000000000..9dd9182fc --- /dev/null +++ b/packages/jetbrains/src/main/resources/liveTemplates/Dotprompt.xml @@ -0,0 +1,176 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/treesitter/README.md b/packages/treesitter/README.md index 39aa7e0a8..e276b81dd 100644 --- a/packages/treesitter/README.md +++ b/packages/treesitter/README.md @@ -1,90 +1,165 @@ # Tree-sitter Grammar for Dotprompt -A [Tree-sitter](https://tree-sitter.github.io/tree-sitter/) grammar for Dotprompt (`.prompt`) files, providing enhanced syntax highlighting for Neovim and other Tree-sitter compatible editors. +A [Tree-sitter](https://tree-sitter.github.io/tree-sitter/) grammar for Dotprompt (`.prompt`) files. ## Features -- **YAML Frontmatter**: Parses frontmatter between `---` delimiters -- **Handlebars Expressions**: Full support for `{{ ... }}` expressions -- **Block Helpers**: `{{#if}}`, `{{#each}}`, `{{#role}}`, etc. -- **Dotprompt Markers**: Special `<<>>` syntax -- **Comments**: Both `{{! ... }}` and `{{!-- ... --}}` styles +- Parses YAML frontmatter +- Parses Handlebars template expressions +- Parses Dotprompt-specific syntax (markers, helpers) +- Supports license header comments ## Installation -### For Neovim (nvim-treesitter) +### Using npm -Add this configuration to register the parser: +```bash +npm install tree-sitter-dotprompt +``` + +### Building from Source + +```bash +cd packages/treesitter +npm install +npm run generate +``` + +## Usage + +### Node.js + +```javascript +const Parser = require('tree-sitter'); +const Dotprompt = require('tree-sitter-dotprompt'); + +const parser = new Parser(); +parser.setLanguage(Dotprompt); + +const source = `--- +model: gemini-2.0-flash +--- +Hello {{ name }}!`; + +const tree = parser.parse(source); +console.log(tree.rootNode.toString()); +``` + +### Neovim (nvim-treesitter) + +1. Add the parser configuration: ```lua local parser_config = require("nvim-treesitter.parsers").get_parser_configs() - parser_config.dotprompt = { install_info = { url = "https://github.com/google/dotprompt", - location = "packages/treesitter", - files = { "src/parser.c" }, + files = { "packages/treesitter/src/parser.c" }, branch = "main", + subdirectory = "packages/treesitter", }, filetype = "dotprompt", } - --- Register the filetype -vim.filetype.add({ - extension = { - prompt = "dotprompt", - }, -}) ``` -Then install the parser: +2. Install the parser: ```vim :TSInstall dotprompt ``` -### Manual Installation +3. Copy highlight queries to your Neovim config: ```bash -cd packages/treesitter -npm install -npm run generate -npm run build +mkdir -p ~/.config/nvim/queries/dotprompt +cp packages/treesitter/queries/highlights.scm ~/.config/nvim/queries/dotprompt/ ``` -## Highlight Groups +### Helix + +Add to your `languages.toml`: + +```toml +[[language]] +name = "dotprompt" +scope = "source.dotprompt" +file-types = ["prompt"] +roots = [] + +[[grammar]] +name = "dotprompt" +source = { git = "https://github.com/google/dotprompt", subpath = "packages/treesitter", rev = "main" } +``` -The grammar provides the following highlight groups: +### Zed -| Group | Description | -|-------|-------------| -| `@keyword.control` | Block helpers (if, each, role) | -| `@function.call` | Helper names | -| `@variable` | Variable references | -| `@variable.builtin` | Special vars (@index, this) | -| `@string` | String literals | -| `@number` | Number literals | -| `@comment` | Handlebars comments | -| `@keyword.directive` | Dotprompt markers | -| `@punctuation.bracket` | `{{` and `}}` | +Add to your `languages.toml`: + +```toml +[[grammars]] +name = "dotprompt" +source = { path = "packages/treesitter" } +``` ## Development +### Generate Parser + ```bash -# Generate parser npm run generate +``` + +This generates `src/parser.c` from `grammar.js`. -# Run tests +### Test Parser + +```bash npm run test +``` -# Build native module +Runs the test corpus in `test/corpus/`. + +### Build Native Module + +```bash npm run build ``` -## Query Files +## Grammar Structure + +``` +document +├── license_header? +│ └── header_comment+ +├── frontmatter? +│ ├── frontmatter_delimiter (---) +│ ├── yaml_content +│ │ └── yaml_line+ +│ └── frontmatter_delimiter (---) +└── template_body + ├── text + ├── handlebars_expression ({{ ... }}) + │ ├── expression_content + │ ├── variable_reference + │ ├── helper_name + │ └── partial_reference + ├── handlebars_block ({{#...}} ... {{/...}}) + │ ├── block_expression + │ ├── else_expression + │ └── close_block + ├── handlebars_comment ({{! ... }}) + └── dotprompt_marker (<<>>) +``` + +## Queries + +### Highlights (`queries/highlights.scm`) -- `queries/highlights.scm` - Syntax highlighting -- More query files (injections, locals, folds) can be added as needed +Provides syntax highlighting for: +- YAML frontmatter +- Handlebars expressions and blocks +- Dotprompt-specific helpers +- Comments and markers ## License diff --git a/packages/treesitter/test/corpus/basic.txt b/packages/treesitter/test/corpus/basic.txt new file mode 100644 index 000000000..1b48f16cc --- /dev/null +++ b/packages/treesitter/test/corpus/basic.txt @@ -0,0 +1,183 @@ +================================================================================ +DOCUMENT +================================================================================ + +--- +model: gemini-2.0-flash +--- +Hello world! + +-------------------------------------------------------------------------------- + +(document + (frontmatter + (frontmatter_delimiter) + (yaml_content + (yaml_line)) + (frontmatter_delimiter)) + (template_body + (text))) + +================================================================================ +HANDLEBARS EXPRESSION +================================================================================ + +Hello {{ name }}! + +-------------------------------------------------------------------------------- + +(document + (template_body + (text) + (handlebars_expression + (expression_content + (variable_reference))) + (text))) + +================================================================================ +ROLE BLOCK +================================================================================ + +{{#role "system"}} +You are helpful. +{{/role}} + +-------------------------------------------------------------------------------- + +(document + (template_body + (handlebars_block + (block_expression + (block_name) + (argument + (string_literal)))) + (text) + (handlebars_block + (close_block + (block_name))))) + +================================================================================ +EACH LOOP +================================================================================ + +{{#each items}} + - {{ this }} +{{/each}} + +-------------------------------------------------------------------------------- + +(document + (template_body + (handlebars_block + (block_expression + (block_name) + (argument + (variable_reference)))) + (text) + (handlebars_expression + (expression_content + (variable_reference))) + (text) + (handlebars_block + (close_block + (block_name))))) + +================================================================================ +PARTIAL +================================================================================ + +{{> header}} + +-------------------------------------------------------------------------------- + +(document + (template_body + (handlebars_expression + (expression_content + (partial_reference + (identifier)))))) + +================================================================================ +COMMENT +================================================================================ + +{{! This is a comment }} + +-------------------------------------------------------------------------------- + +(document + (template_body + (handlebars_comment))) + +================================================================================ +DOTPROMPT MARKER +================================================================================ + +<<>> + +-------------------------------------------------------------------------------- + +(document + (template_body + (dotprompt_marker + (marker_content)))) + +================================================================================ +FRONTMATTER WITH CONFIG +================================================================================ + +--- +model: gemini-2.0-flash +config: + temperature: 0.7 +input: + schema: + name: string +--- + +Hello {{ name }}! + +-------------------------------------------------------------------------------- + +(document + (frontmatter + (frontmatter_delimiter) + (yaml_content + (yaml_line) + (yaml_line) + (yaml_line) + (yaml_line) + (yaml_line) + (yaml_line)) + (frontmatter_delimiter)) + (template_body + (text) + (handlebars_expression + (expression_content + (variable_reference))) + (text))) + +================================================================================ +LICENSE HEADER +================================================================================ + +# Copyright 2026 Google LLC +# Some license text +--- +model: gemini-2.0-flash +--- +Hello + +-------------------------------------------------------------------------------- + +(document + (license_header + (header_comment) + (header_comment)) + (frontmatter + (frontmatter_delimiter) + (yaml_content + (yaml_line)) + (frontmatter_delimiter)) + (template_body + (text))) diff --git a/packages/vim/README.md b/packages/vim/README.md index d57d015f0..08e4708c5 100644 --- a/packages/vim/README.md +++ b/packages/vim/README.md @@ -1,37 +1,60 @@ -# Dotprompt Vim Plugin +# Dotprompt Vim/Neovim Plugin -Provides syntax highlighting and filetype detection for Dotprompt (`.prompt`) files. +Provides syntax highlighting, LSP integration, and filetype detection for Dotprompt (`.prompt`) files. + +## Features + +- **Syntax Highlighting**: YAML frontmatter, Handlebars templates, Dotprompt markers +- **LSP Integration**: Diagnostics, formatting, hover documentation via `promptly` +- **Format on Save**: Automatic formatting when saving (configurable) +- **Keymaps**: Quick access to LSP features ## Installation -### Vundle -```vim -Plugin 'google/dotprompt', {'rtp': 'packages/vim'} -``` +### Neovim (lazy.nvim) - Recommended -### Plug -```vim -Plug 'google/dotprompt', {'rtp': 'packages/vim' +```lua +{ + "google/dotprompt", + config = function() + require("dotprompt").setup({ + -- Optional: custom path to promptly binary + promptly_path = "", + -- Enable format on save + format_on_save = true, + }) + end, +} ``` -### lazy.nvim (Neovim) +### Neovim (Packer) + ```lua -{ +use { "google/dotprompt", config = function() - vim.filetype.add({ extension = { prompt = "dotprompt" } }) + require("dotprompt").setup() end, } ``` -### Manual -Copy the contents of `syntax/` and `ftdetect/` to your `~/.vim/` directory. +### Vundle -## LSP Support (Neovim) +```vim +Plugin 'google/dotprompt', {'rtp': 'packages/vim'} +``` -For diagnostics, formatting, and hover documentation, install `promptly` and configure nvim-lspconfig: +### vim-plug -### 1. Install promptly +```vim +Plug 'google/dotprompt', {'rtp': 'packages/vim'} +``` + +### Manual + +Copy the contents of `syntax/`, `ftdetect/`, and `lua/` to your `~/.vim/` or `~/.config/nvim/` directory. + +## Install promptly for LSP Features ```bash cargo install --path rs/promptly @@ -39,9 +62,26 @@ cargo install --path rs/promptly cargo build --release -p promptly ``` -### 2. Configure nvim-lspconfig +## Configuration + +### Using the Lua Module (Recommended) + +The Lua module handles everything automatically: + +```lua +require("dotprompt").setup({ + -- Path to promptly binary (empty = auto-detect) + promptly_path = "", + -- Enable format on save + format_on_save = true, + -- Enable diagnostics + diagnostics = true, +}) +``` + +### Manual LSP Configuration -Add to your Neovim configuration: +If you prefer manual setup with nvim-lspconfig: ```lua local lspconfig = require("lspconfig") @@ -69,7 +109,7 @@ lspconfig.promptly.setup({ }) ``` -## LSP Support (Vim with vim-lsp) +### Vim 8+ with vim-lsp For Vim 8+ with [vim-lsp](https://github.com/prabirshrestha/vim-lsp): @@ -83,9 +123,73 @@ if executable('promptly') endif ``` -## Features +## Tree-sitter Support + +For enhanced syntax highlighting with Tree-sitter: + +### 1. Add the parser to nvim-treesitter + +```lua +local parser_config = require("nvim-treesitter.parsers").get_parser_configs() +parser_config.dotprompt = { + install_info = { + url = "https://github.com/google/dotprompt", + files = { "packages/treesitter/src/parser.c" }, + branch = "main", + subdirectory = "packages/treesitter", + }, + filetype = "dotprompt", +} +``` + +### 2. Install the parser + +```vim +:TSInstall dotprompt +``` + +### 3. Enable highlighting + +```lua +require("nvim-treesitter.configs").setup({ + highlight = { + enable = true, + additional_vim_regex_highlighting = false, + }, +}) +``` + +## Keymaps + +When using the Lua module, these keymaps are set automatically: + +| Keymap | Action | +|--------|--------| +| `f` | Format document | +| `K` | Show hover documentation | +| `gd` | Go to definition | +| `gr` | Find references | +| `rn` | Rename symbol | +| `ca` | Code action | + +## LSP Features With LSP enabled, you get: -- **Diagnostics**: Real-time error detection for YAML and Handlebars syntax -- **Formatting**: Format buffer with `promptly fmt` rules -- **Hover**: Documentation for Handlebars helpers and frontmatter fields + +| Feature | Description | +|---------|-------------| +| **Diagnostics** | Real-time error detection for YAML and Handlebars syntax | +| **Formatting** | Format buffer with promptly fmt rules | +| **Hover** | Documentation for Handlebars helpers and frontmatter fields | +| **Go to Definition** | Jump to partial files | +| **References** | Find all uses of partials | + +## Commands + +```lua +-- Format current document +require("dotprompt").format() + +-- Restart LSP server +require("dotprompt").restart() +``` diff --git a/packages/vim/lua/dotprompt/init.lua b/packages/vim/lua/dotprompt/init.lua new file mode 100644 index 000000000..80fc2236c --- /dev/null +++ b/packages/vim/lua/dotprompt/init.lua @@ -0,0 +1,147 @@ +-- Copyright 2026 Google LLC +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- SPDX-License-Identifier: Apache-2.0 + +--- Dotprompt Neovim module for LSP setup +--- @module dotprompt +local M = {} + +--- Default configuration +M.config = { + --- Path to promptly binary (empty = auto-detect) + promptly_path = "", + --- Enable format on save + format_on_save = true, + --- Enable LSP diagnostics + diagnostics = true, +} + +--- Find promptly executable in common locations +--- @return string|nil path to promptly or nil if not found +local function find_promptly() + -- Check user config first + if M.config.promptly_path ~= "" then + if vim.fn.executable(M.config.promptly_path) == 1 then + return M.config.promptly_path + end + end + + -- Check PATH + if vim.fn.executable("promptly") == 1 then + return "promptly" + end + + -- Check cargo bin + local home = vim.env.HOME or vim.env.USERPROFILE + if home then + local cargo_path = home .. "/.cargo/bin/promptly" + if vim.fn.executable(cargo_path) == 1 then + return cargo_path + end + end + + return nil +end + +--- Setup LSP for Dotprompt files +--- @param opts table|nil Optional configuration overrides +function M.setup(opts) + -- Merge user options + M.config = vim.tbl_deep_extend("force", M.config, opts or {}) + + -- Register filetype + vim.filetype.add({ + extension = { + prompt = "dotprompt", + }, + }) + + -- Find promptly + local promptly_path = find_promptly() + if not promptly_path then + vim.notify( + "Dotprompt: promptly not found. Install with: cargo install --path rs/promptly", + vim.log.levels.WARN + ) + return + end + + -- Setup LSP using nvim-lspconfig if available + local ok, lspconfig = pcall(require, "lspconfig") + if not ok then + vim.notify( + "Dotprompt: nvim-lspconfig not found. Install it for LSP features.", + vim.log.levels.WARN + ) + return + end + + local configs = require("lspconfig.configs") + + -- Register promptly as an LSP server + if not configs.promptly then + configs.promptly = { + default_config = { + cmd = { promptly_path, "lsp" }, + filetypes = { "dotprompt" }, + root_dir = lspconfig.util.find_git_ancestor, + single_file_support = true, + settings = {}, + }, + } + end + + -- Start the server with user callbacks + lspconfig.promptly.setup({ + on_attach = function(client, bufnr) + -- Enable format on save if configured + if M.config.format_on_save then + vim.api.nvim_create_autocmd("BufWritePre", { + buffer = bufnr, + callback = function() + vim.lsp.buf.format({ async = false }) + end, + }) + end + + -- Set up keymaps + local bufopts = { noremap = true, silent = true, buffer = bufnr } + vim.keymap.set("n", "f", function() + vim.lsp.buf.format({ async = true }) + end, bufopts) + vim.keymap.set("n", "K", vim.lsp.buf.hover, bufopts) + vim.keymap.set("n", "gd", vim.lsp.buf.definition, bufopts) + vim.keymap.set("n", "gr", vim.lsp.buf.references, bufopts) + vim.keymap.set("n", "rn", vim.lsp.buf.rename, bufopts) + vim.keymap.set("n", "ca", vim.lsp.buf.code_action, bufopts) + end, + + capabilities = vim.lsp.protocol.make_client_capabilities(), + }) + + vim.notify("Dotprompt: LSP configured with " .. promptly_path, vim.log.levels.INFO) +end + +--- Manually trigger document formatting +function M.format() + vim.lsp.buf.format({ async = true }) +end + +--- Restart the LSP server +function M.restart() + vim.cmd("LspRestart promptly") +end + +return M diff --git a/packages/vscode/CHANGELOG.md b/packages/vscode/CHANGELOG.md index a3f57fa9f..ccafe72a2 100644 --- a/packages/vscode/CHANGELOG.md +++ b/packages/vscode/CHANGELOG.md @@ -5,6 +5,25 @@ All notable changes to the Dotprompt VS Code extension will be documented in thi The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.2.0] - 2026-01-24 + +### Added +- **Status Bar Indicator**: Shows LSP connection status (connected, starting, error) +- **Format on Save**: Automatically format `.prompt` files when saving (configurable) +- **Commands**: + - `Dotprompt: Format Document` - Format the current file + - `Dotprompt: Restart Language Server` - Restart the LSP connection + - `Dotprompt: Show Output` - View LSP logs +- **Improved Error Handling**: Better messages when promptly is not found with actions + +### Changed +- Status bar now shows real-time LSP state +- Error messages offer actionable options (Open Settings, Retry, Show Output) + +### Configuration +- `dotprompt.formatOnSave`: Enable/disable format on save (default: true) +- `dotprompt.trace.server`: Configure LSP message tracing + ## [0.1.0] - 2026-01-24 ### Added diff --git a/packages/vscode/package.json b/packages/vscode/package.json index a41bfe2c5..832f07a0c 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -2,7 +2,7 @@ "name": "dotprompt-vscode", "displayName": "Dotprompt", "description": "Syntax highlighting, LSP diagnostics, formatting, and code snippets for Dotprompt (.prompt) files", - "version": "0.1.0", + "version": "0.2.0", "publisher": "google", "license": "Apache-2.0", "icon": "images/icon.png", @@ -85,6 +85,23 @@ "path": "./snippets/dotprompt.snippets.json" } ], + "commands": [ + { + "command": "dotprompt.formatDocument", + "title": "Format Document", + "category": "Dotprompt" + }, + { + "command": "dotprompt.restartLsp", + "title": "Restart Language Server", + "category": "Dotprompt" + }, + { + "command": "dotprompt.showOutput", + "title": "Show Output", + "category": "Dotprompt" + } + ], "configuration": { "type": "object", "title": "Dotprompt", @@ -98,6 +115,21 @@ "type": "boolean", "default": true, "description": "Enable LSP features (diagnostics, formatting, hover). Requires promptly to be installed." + }, + "dotprompt.formatOnSave": { + "type": "boolean", + "default": true, + "description": "Automatically format .prompt files on save." + }, + "dotprompt.trace.server": { + "type": "string", + "enum": [ + "off", + "messages", + "verbose" + ], + "default": "off", + "description": "Traces the communication between VS Code and the language server." } } } diff --git a/packages/vscode/src/extension.ts b/packages/vscode/src/extension.ts index b6534d6cd..4bae820fc 100644 --- a/packages/vscode/src/extension.ts +++ b/packages/vscode/src/extension.ts @@ -22,11 +22,13 @@ import { LanguageClient, LanguageClientOptions, ServerOptions, + State, TransportKind, } from 'vscode-languageclient/node'; let client: LanguageClient | undefined; let outputChannel: vscode.OutputChannel; +let statusBarItem: vscode.StatusBarItem; export async function activate(context: vscode.ExtensionContext) { // Create output channel early for debugging @@ -38,11 +40,142 @@ export async function activate(context: vscode.ExtensionContext) { `Dotprompt: Extension path: ${context.extensionPath}` ); + // Create status bar item + statusBarItem = vscode.window.createStatusBarItem( + vscode.StatusBarAlignment.Right, + 100 + ); + statusBarItem.command = 'dotprompt.showOutput'; + context.subscriptions.push(statusBarItem); + updateStatusBar('$(loading~spin) Dotprompt', 'Initializing...'); + // Register completions (existing functionality) const completionProvider = registerCompletionProvider(); context.subscriptions.push(completionProvider); + // Register commands + context.subscriptions.push( + vscode.commands.registerCommand('dotprompt.formatDocument', formatDocument), + vscode.commands.registerCommand('dotprompt.restartLsp', () => + restartLspClient(context) + ), + vscode.commands.registerCommand('dotprompt.showOutput', () => + outputChannel.show() + ) + ); + + // Register format on save + context.subscriptions.push( + vscode.workspace.onWillSaveTextDocument(async (event) => { + const config = vscode.workspace.getConfiguration('dotprompt'); + if ( + config.get('formatOnSave') && + event.document.languageId === 'dotprompt' && + client?.state === State.Running + ) { + const edit = await formatDocumentEdit(event.document); + if (edit) { + event.waitUntil(Promise.resolve([edit])); + } + } + }) + ); + // Start LSP client if promptly is available + const config = vscode.workspace.getConfiguration('dotprompt'); + if (config.get('enableLsp', true)) { + await startLspClient(context, outputChannel); + } else { + updateStatusBar('$(circle-slash) Dotprompt', 'LSP disabled'); + } +} + +/** + * Updates the status bar with current LSP state. + */ +function updateStatusBar(text: string, tooltip: string) { + statusBarItem.text = text; + statusBarItem.tooltip = tooltip; + statusBarItem.show(); +} + +/** + * Formats the current document using the LSP. + */ +async function formatDocument() { + const editor = vscode.window.activeTextEditor; + if (!editor || editor.document.languageId !== 'dotprompt') { + vscode.window.showWarningMessage('No Dotprompt file is active.'); + return; + } + + if (!client || client.state !== State.Running) { + vscode.window.showWarningMessage( + 'Dotprompt LSP is not running. Install promptly for formatting.' + ); + return; + } + + await vscode.commands.executeCommand('editor.action.formatDocument'); +} + +/** + * Gets a text edit for formatting a document. + */ +async function formatDocumentEdit( + document: vscode.TextDocument +): Promise { + if (!client || client.state !== State.Running) { + return undefined; + } + + try { + const edits = await client.sendRequest('textDocument/formatting', { + textDocument: { uri: document.uri.toString() }, + options: { + tabSize: 2, + insertSpaces: true, + }, + }); + + if (Array.isArray(edits) && edits.length > 0) { + // Convert LSP edits to VS Code edits + const edit = edits[0] as { + range: { + start: { line: number; character: number }; + end: { line: number; character: number }; + }; + newText: string; + }; + return new vscode.TextEdit( + new vscode.Range( + edit.range.start.line, + edit.range.start.character, + edit.range.end.line, + edit.range.end.character + ), + edit.newText + ); + } + } catch (error) { + outputChannel.appendLine(`Format error: ${error}`); + } + + return undefined; +} + +/** + * Restarts the LSP client. + */ +async function restartLspClient(context: vscode.ExtensionContext) { + outputChannel.appendLine('Dotprompt: Restarting LSP client...'); + updateStatusBar('$(loading~spin) Dotprompt', 'Restarting...'); + + if (client) { + await client.stop(); + client = undefined; + } + await startLspClient(context, outputChannel); } @@ -127,9 +260,26 @@ async function startLspClient( outputChannel.appendLine( 'promptly binary not found. LSP features disabled. Install promptly for enhanced features.' ); - vscode.window.showWarningMessage( - "Promptly LSP: Binary not found. Set 'dotprompt.promptlyPath' in settings." + updateStatusBar( + '$(warning) Dotprompt', + 'promptly not found. Click to see details.' ); + vscode.window + .showWarningMessage( + "Promptly LSP: Binary not found. Install with 'cargo install promptly' or set path in settings.", + 'Open Settings', + 'Show Output' + ) + .then((selection) => { + if (selection === 'Open Settings') { + vscode.commands.executeCommand( + 'workbench.action.openSettings', + 'dotprompt.promptlyPath' + ); + } else if (selection === 'Show Output') { + outputChannel.show(); + } + }); return; } @@ -156,16 +306,46 @@ async function startLspClient( clientOptions ); + // Listen for state changes + client.onDidChangeState((event) => { + switch (event.newState) { + case State.Running: + updateStatusBar('$(check) Dotprompt', 'LSP connected'); + break; + case State.Starting: + updateStatusBar('$(loading~spin) Dotprompt', 'LSP starting...'); + break; + case State.Stopped: + updateStatusBar('$(error) Dotprompt', 'LSP stopped'); + break; + } + }); + // Start the client try { await client.start(); outputChannel.appendLine( 'Dotprompt: Promptly LSP client started successfully' ); - vscode.window.showInformationMessage('Promptly LSP connected!'); } catch (error) { outputChannel.appendLine(`Failed to start Promptly LSP client: ${error}`); - vscode.window.showErrorMessage(`Promptly LSP failed to start: ${error}`); + updateStatusBar( + '$(error) Dotprompt', + `LSP failed to start: ${error}. Click for details.` + ); + vscode.window + .showErrorMessage( + `Promptly LSP failed to start: ${error}`, + 'Show Output', + 'Retry' + ) + .then((selection) => { + if (selection === 'Show Output') { + outputChannel.show(); + } else if (selection === 'Retry') { + restartLspClient(context); + } + }); client = undefined; } }