├── src ├── code-actions │ ├── index.ts │ └── code-action-wrap.ts ├── commands │ ├── index.ts │ └── sealed-states.command.ts ├── constants │ └── index.ts ├── utils │ ├── index.ts │ ├── execute-command.ts │ ├── wrap-with.ts │ ├── sdk.ts │ └── get-selected-text.ts ├── test │ └── extension.test.ts ├── config │ └── config.ts └── extension.ts ├── assets └── logo.png ├── .vscode-test.mjs ├── .vscode ├── extensions.json ├── settings.json ├── launch.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── tsconfig.json ├── .eslintrc.json ├── .github └── workflows │ ├── deploy.yml │ └── checkout.yml ├── Makefile ├── LICENSE ├── CONTRIBUTING.md ├── snippets ├── markdown.json ├── dart.json └── flutter.json ├── esbuild.js ├── .gitignore ├── vsc-extension-quickstart.md ├── package.json └── README.md /src/code-actions/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./code-action-wrap"; 2 | -------------------------------------------------------------------------------- /src/commands/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./sealed-states.command"; 2 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/flutter-plus/HEAD/assets/logo.png -------------------------------------------------------------------------------- /.vscode-test.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@vscode/test-cli'; 2 | 3 | export default defineConfig({ 4 | files: 'out/test/**/*.test.js', 5 | }); 6 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const dartCodeExtensionIdentifier = "Dart-Code.dart-code"; 2 | export const flutterExtensionIdentifier = "Dart-Code.flutter"; -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./execute-command"; 2 | export * from "./get-selected-text"; 3 | export * from "./sdk"; 4 | export * from "./wrap-with"; 5 | 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "connor4312.esbuild-problem-matchers", 5 | "ms-vscode.extension-test-runner" 6 | ] 7 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "out": true, 4 | "dist": true 5 | }, 6 | "search.exclude": { 7 | "out": true, 8 | "dist": true 9 | }, 10 | "typescript.tsc.autoDetect": "off" 11 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/** 4 | node_modules/** 5 | src/** 6 | .gitignore 7 | .yarnrc 8 | esbuild.js 9 | vsc-extension-quickstart.md 10 | **/tsconfig.json 11 | **/.eslintrc.json 12 | **/*.map 13 | **/*.ts 14 | **/.vscode-test.* 15 | -------------------------------------------------------------------------------- /src/utils/execute-command.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | // Execute a command in the terminal 4 | export function executeCommand(command: string) { 5 | const terminal = vscode.window.createTerminal('Flutter Plus'); 6 | terminal.sendText(command); 7 | terminal.show(); 8 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "flutter-plus" extension will be documented in this file. 4 | 5 | ## 0.7.2 6 | - **CHANGED**: `ignore: unused_element` to `ignore: unused_element_parameter` in snippets 7 | 8 | 9 | ## 0.7.1 10 | - **ADDED**: Error object and StackTrace to generated state's 11 | -------------------------------------------------------------------------------- /src/test/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | import * as vscode from 'vscode'; 6 | // import * as myExtension from '../../extension'; 7 | 8 | suite('Extension Test Suite', () => { 9 | vscode.window.showInformationMessage('Start all tests.'); 10 | 11 | test('Sample test', () => { 12 | assert.strictEqual(-1, [1, 2, 3].indexOf(5)); 13 | assert.strictEqual(-1, [1, 2, 3].indexOf(0)); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Run Extension", 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "runtimeExecutable": "${execPath}", 9 | "args": [ 10 | "--extensionDevelopmentPath=${workspaceFolder}" 11 | ], 12 | "outFiles": [ 13 | "${workspaceFolder}/dist/**/*.js" 14 | ], 15 | "preLaunchTask": "${defaultBuildTask}" 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "Node16", 4 | "target": "ES2022", 5 | "lib": [ 6 | "ES2022" 7 | ], 8 | "sourceMap": true, 9 | "rootDir": "src", 10 | "strict": true /* enable all strict type-checking options */ 11 | /* Additional Checks */ 12 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 13 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 14 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/config/config.ts: -------------------------------------------------------------------------------- 1 | import { workspace } from "vscode"; 2 | 3 | export type CustomWrapConfig = { 4 | name: string, 5 | body: Array, 6 | }; 7 | 8 | export class FlutterPlusConfig { 9 | private static instance: FlutterPlusConfig; 10 | 11 | public static getInstance(): FlutterPlusConfig { 12 | if (!FlutterPlusConfig.instance) { 13 | FlutterPlusConfig.instance = new FlutterPlusConfig(); 14 | } 15 | return FlutterPlusConfig.instance; 16 | } 17 | 18 | public getCustomWraps(): Array { 19 | const config = workspace.getConfiguration('flutter-plus'); 20 | const customWraps = config.get>('wraps'); 21 | 22 | return customWraps || []; 23 | } 24 | } -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/naming-convention": [ 13 | "warn", 14 | { 15 | "selector": "import", 16 | "format": [ "camelCase", "PascalCase" ] 17 | } 18 | ], 19 | "@typescript-eslint/semi": "warn", 20 | "curly": "warn", 21 | "eqeqeq": "warn", 22 | "no-throw-literal": "warn", 23 | "semi": "off" 24 | }, 25 | "ignorePatterns": [ 26 | "out", 27 | "dist", 28 | "**/*.d.ts" 29 | ] 30 | } -------------------------------------------------------------------------------- /src/utils/wrap-with.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable curly */ 2 | 3 | import { commands, SnippetString, window } from "vscode"; 4 | import { getSelectedText } from "../utils"; 5 | 6 | const interpolatedVarRegExp = /[$]/g; 7 | const escapedCharacterRegExp = /[\\]/g; 8 | 9 | export const wrapWith = async (snippet: (widget: string) => string) => { 10 | let editor = window.activeTextEditor; 11 | if (!editor) return; 12 | const selection = getSelectedText(editor); 13 | const widget = editor.document 14 | .getText(selection) 15 | .replace(escapedCharacterRegExp, "\\\\") 16 | .replace(interpolatedVarRegExp, "\\$"); 17 | //console.log('Selection:', snippet(widget)); 18 | editor.insertSnippet(new SnippetString(snippet(widget)), selection); 19 | await commands.executeCommand("editor.action.formatDocument"); 20 | }; -------------------------------------------------------------------------------- /src/utils/sdk.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | export class SdkCommands { 4 | // TODO: Find an easy way to share the API signature class. 5 | constructor(context: vscode.ExtensionContext, private dartExtensionApi: any) { 6 | 7 | // context.subscriptions.push(vs.commands.registerCommand("flutter.createSampleProject", 8 | // (_) => this.runFunctionIfSupported(dartExtensionApi.flutterCreateSampleProject))); 9 | } 10 | 11 | private async runFunctionIfSupported(f: () => Promise): Promise { 12 | if (!f) { 13 | this.showApiMismatchError(); 14 | return undefined; 15 | } 16 | 17 | return f(); 18 | } 19 | 20 | private showApiMismatchError(): void { 21 | vscode.window.showErrorMessage("The installed version of the Dart extension does not support this feature."); 22 | } 23 | } -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy VS Code Extension 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v[0-9]+.[0-9]+.[0-9]+*" 7 | #release: 8 | # types: 9 | # - created 10 | 11 | jobs: 12 | deploy: 13 | name: "Deploy VS Code Extension" 14 | runs-on: ubuntu-latest 15 | defaults: 16 | run: 17 | working-directory: ./ 18 | timeout-minutes: 15 19 | steps: 20 | - name: Checkout 21 | id: checkout 22 | uses: actions/checkout@v4 23 | 24 | - name: Install Node.js 25 | id: node-setup 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: 18.x 29 | 30 | - name: Install Dependencies 31 | id: install 32 | run: | 33 | npm install 34 | npm install -g @vscode/vsce 35 | 36 | - name: Publish 37 | id: publish 38 | if: success() && startsWith(github.ref, 'refs/tags/') 39 | run: npm run deploy 40 | env: 41 | VSCE_PAT: ${{ secrets.VSCE_PAT }} 42 | -------------------------------------------------------------------------------- /src/code-actions/code-action-wrap.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable curly */ 2 | 3 | import { CodeAction, CodeActionKind, CodeActionProvider, window } from "vscode"; 4 | import { getSelectedText } from "../utils"; 5 | import { CodeWrap } from "../extension"; 6 | 7 | export class CodeActionWrap implements CodeActionProvider { 8 | private wraps: Array; 9 | 10 | constructor(customWraps: Array) { 11 | this.wraps = customWraps; 12 | } 13 | 14 | public provideCodeActions(): CodeAction[] { 15 | const editor = window.activeTextEditor; 16 | if (!editor) return []; 17 | 18 | const selectedText = editor.document.getText(getSelectedText(editor)); 19 | if (selectedText === "") return []; 20 | 21 | return this.wraps.map((c) => { 22 | let action = new CodeAction(c.title, CodeActionKind.Refactor); 23 | action.command = { 24 | command: c.commandId, 25 | title: c.title, 26 | }; 27 | return action; 28 | }); 29 | } 30 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL :=/bin/bash -e -o pipefail 2 | PWD := $(shell pwd) 3 | 4 | .PHONY: help 5 | help: 6 | @echo 'Usage: make ... ' 7 | @echo '' 8 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 9 | 10 | .PHONY: setup 11 | setup: ## setup environment 12 | @npm install 13 | @npm install -g @vscode/vsce 14 | 15 | .PHONY: login 16 | login: ## login to vsce 17 | @vsce login plugfox 18 | 19 | .PHONY: build 20 | build: setup ## build extension 21 | @vsce package 22 | 23 | .PHONY: publish 24 | publish: build ## publish extension 25 | @vsce publish 26 | 27 | .PHONY: minor 28 | minor: build ## publish minor version 29 | @vsce publish minor 30 | 31 | .PHONY: test 32 | test: ## run tests 33 | @npm install 34 | @npm test 35 | 36 | .PHONY: diff 37 | diff: ## git diff 38 | $(call print-target) 39 | @git diff --exit-code 40 | @RES=$$(git status --porcelain) ; if [ -n "$$RES" ]; then echo $$RES && exit 1 ; fi 41 | 42 | define print-target 43 | @printf "Executing target: \033[36m$@\033[0m\n" 44 | endef 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Plague Fox 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to 2 | 3 | ## How to add widget wrappper 4 | 5 | 1. Add command to `package.json` 6 | 7 | ```json 8 | { 9 | "contributes": { 10 | "commands": [ 11 | { 12 | "command": "flutter-plus.wrap-listenablebuilder", 13 | "title": "Wrap with ListenableBuilder" 14 | } 15 | ] 16 | } 17 | } 18 | ``` 19 | 20 | 2. Add snippet to `src/commands/wrap-with.command.ts` 21 | 22 | ```ts 23 | const snippetListenableBuilder = (widget: string) => { 24 | return `ListenableBuilder( 25 | listenable: \${1:listenable}, 26 | builder: (context, _) => 27 | ${widget}, 28 | )`; 29 | }; 30 | 31 | export const wrapWithListenableBuilder = async () => 32 | wrapWith(snippetListenableBuilder); 33 | ``` 34 | 35 | 3. Add action to `src/code-actions/code-action-wrap.ts` 36 | 37 | ```ts 38 | return [ 39 | { 40 | command: "flutter-plus.wrap-listenablebuilder", 41 | title: "Wrap with ListenableBuilder", 42 | } 43 | ] 44 | ``` 45 | 46 | 4. Add command to `src/extension.ts` 47 | 48 | ```ts 49 | export function activate(context: vscode.ExtensionContext) { 50 | context.subscriptions.push( 51 | commands.registerCommand( 52 | "flutter-plus.wrap-listenablebuilder", 53 | wrapWithListenableBuilder 54 | ) 55 | ); 56 | } 57 | ``` 58 | -------------------------------------------------------------------------------- /snippets/markdown.json: -------------------------------------------------------------------------------- 1 | { 2 | "Changelog unreleased": { 3 | "scope": "md, markdown", 4 | "prefix": [ 5 | "unreleased", 6 | "changelog_unreleased" 7 | ], 8 | "description": "Changelog unreleased", 9 | "body": [ 10 | "## Unreleased", 11 | "", 12 | "- **ADDED**: ${0}", 13 | "- **CHANGED**: ", 14 | "- **DEPRECATED**: ", 15 | "- **REMOVED**: ", 16 | "- **FIXED**: ", 17 | "- **SECURITY**: ", 18 | "- **REFACTOR**: ", 19 | "- **DOCS**: ", 20 | " " 21 | ] 22 | }, 23 | "Changelog version": { 24 | "scope": "md, markdown", 25 | "prefix": [ 26 | "version", 27 | "changelog_version" 28 | ], 29 | "description": "Changelog version section", 30 | "body": [ 31 | "## ${1:0}.${2:0}.${3:0}", 32 | "", 33 | "- **ADDED**: ${0}", 34 | "- **CHANGED**: ", 35 | "- **DEPRECATED**: ", 36 | "- **REMOVED**: ", 37 | "- **FIXED**: ", 38 | "- **SECURITY**: ", 39 | "- **REFACTOR**: ", 40 | "- **DOCS**: ", 41 | " " 42 | ] 43 | } 44 | } -------------------------------------------------------------------------------- /esbuild.js: -------------------------------------------------------------------------------- 1 | const esbuild = require("esbuild"); 2 | 3 | const production = process.argv.includes('--production'); 4 | const watch = process.argv.includes('--watch'); 5 | 6 | /** 7 | * @type {import('esbuild').Plugin} 8 | */ 9 | const esbuildProblemMatcherPlugin = { 10 | name: 'esbuild-problem-matcher', 11 | 12 | setup(build) { 13 | build.onStart(() => { 14 | console.log('[watch] build started'); 15 | }); 16 | build.onEnd((result) => { 17 | result.errors.forEach(({ text, location }) => { 18 | console.error(`✘ [ERROR] ${text}`); 19 | console.error(` ${location.file}:${location.line}:${location.column}:`); 20 | }); 21 | console.log('[watch] build finished'); 22 | }); 23 | }, 24 | }; 25 | 26 | async function main() { 27 | const ctx = await esbuild.context({ 28 | entryPoints: [ 29 | 'src/extension.ts' 30 | ], 31 | bundle: true, 32 | format: 'cjs', 33 | minify: production, 34 | sourcemap: !production, 35 | sourcesContent: false, 36 | platform: 'node', 37 | outfile: 'dist/extension.js', 38 | external: ['vscode'], 39 | logLevel: 'silent', 40 | plugins: [ 41 | /* add to the end of plugins array */ 42 | esbuildProblemMatcherPlugin, 43 | ], 44 | }); 45 | if (watch) { 46 | await ctx.watch(); 47 | } else { 48 | await ctx.rebuild(); 49 | await ctx.dispose(); 50 | } 51 | } 52 | 53 | main().catch(e => { 54 | console.error(e); 55 | process.exit(1); 56 | }); 57 | -------------------------------------------------------------------------------- /.github/workflows/checkout.yml: -------------------------------------------------------------------------------- 1 | name: Checkout VS Code Extension 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: 7 | - "main" 8 | - "master" 9 | - "dev" 10 | - "develop" 11 | - "feature/**" 12 | - "bugfix/**" 13 | - "hotfix/**" 14 | - "support/**" 15 | paths: 16 | - .github/workflows/*.yml 17 | - "README.md" 18 | - "CHANGELOG.md" 19 | - "**.json" 20 | - "**.ts" 21 | - "**.js" 22 | - "**.mjs" 23 | 24 | jobs: 25 | checkout: 26 | name: "Checkout VS Code Extension" 27 | strategy: 28 | fail-fast: false 29 | matrix: 30 | os: 31 | - ubuntu-latest 32 | #- macos-latest 33 | #- windows-latest 34 | runs-on: ${{ matrix.os }} 35 | defaults: 36 | run: 37 | working-directory: ./ 38 | timeout-minutes: 10 39 | steps: 40 | - name: Checkout 41 | id: checkout 42 | uses: actions/checkout@v4 43 | #with: 44 | # sparse-checkout: | 45 | # .github 46 | # src/ 47 | # *.json 48 | # *.js 49 | # *.mjs 50 | # README.md 51 | # CHANGELOG.md 52 | 53 | - name: Install Node.js 54 | uses: actions/setup-node@v4 55 | with: 56 | node-version: 18.x 57 | 58 | - name: Install Dependencies 59 | run: npm install 60 | 61 | - name: Run Tests 62 | run: xvfb-run -a npm test 63 | if: runner.os == 'Linux' 64 | 65 | - name: Run Tests 66 | run: npm test 67 | if: runner.os != 'Linux' 68 | 69 | - name: Lint Code 70 | run: | 71 | npm run lint 72 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "watch", 6 | "dependsOn": [ 7 | "npm: watch:tsc", 8 | "npm: watch:esbuild" 9 | ], 10 | "presentation": { 11 | "reveal": "never" 12 | }, 13 | "group": { 14 | "kind": "build", 15 | "isDefault": true 16 | } 17 | }, 18 | { 19 | "type": "npm", 20 | "script": "watch:esbuild", 21 | "group": "build", 22 | "problemMatcher": "$esbuild-watch", 23 | "isBackground": true, 24 | "label": "npm: watch:esbuild", 25 | "presentation": { 26 | "group": "watch", 27 | "reveal": "never" 28 | } 29 | }, 30 | { 31 | "type": "npm", 32 | "script": "watch:tsc", 33 | "group": "build", 34 | "problemMatcher": "$tsc-watch", 35 | "isBackground": true, 36 | "label": "npm: watch:tsc", 37 | "presentation": { 38 | "group": "watch", 39 | "reveal": "never" 40 | } 41 | }, 42 | { 43 | "type": "npm", 44 | "script": "watch-tests", 45 | "problemMatcher": "$tsc-watch", 46 | "isBackground": true, 47 | "presentation": { 48 | "reveal": "never", 49 | "group": "watchers" 50 | }, 51 | "group": "build" 52 | }, 53 | { 54 | "label": "tasks: watch-tests", 55 | "dependsOn": [ 56 | "npm: watch", 57 | "npm: watch-tests" 58 | ], 59 | "problemMatcher": [] 60 | } 61 | ] 62 | } -------------------------------------------------------------------------------- /src/utils/get-selected-text.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable curly */ 2 | 3 | import { Position, Selection, TextEditor } from "vscode"; 4 | 5 | const openBracket = "("; 6 | const closeBracket = ")"; 7 | 8 | export const getSelectedText = (editor: TextEditor): Selection => { 9 | const emptySelection = new Selection( 10 | editor.document.positionAt(0), 11 | editor.document.positionAt(0) 12 | ); 13 | const language = editor.document.languageId; 14 | if (language !== "dart") return emptySelection; 15 | 16 | const line = editor.document.lineAt(editor.selection.start); 17 | const lineText = line.text; 18 | const openBracketIndex = line.text.indexOf( 19 | openBracket, 20 | editor.selection.anchor.character 21 | ); 22 | 23 | let widgetStartIndex = 24 | openBracketIndex > 1 25 | ? openBracketIndex - 1 26 | : editor.selection.anchor.character; 27 | for (widgetStartIndex; widgetStartIndex > 0; widgetStartIndex--) { 28 | const currentChar = lineText.charAt(widgetStartIndex); 29 | const isBeginningOfWidget = 30 | currentChar === openBracket || 31 | (currentChar === " " && 32 | lineText.charAt(widgetStartIndex - 1) !== "," && 33 | lineText.substring(widgetStartIndex - 5, widgetStartIndex) !== "const"); 34 | if (isBeginningOfWidget) break; 35 | } 36 | widgetStartIndex++; 37 | 38 | if (openBracketIndex < 0) { 39 | const commaIndex = lineText.indexOf(",", widgetStartIndex); 40 | const bracketIndex = lineText.indexOf(closeBracket, widgetStartIndex); 41 | const endIndex = 42 | commaIndex >= 0 43 | ? commaIndex 44 | : bracketIndex >= 0 45 | ? bracketIndex 46 | : lineText.length; 47 | 48 | return new Selection( 49 | new Position(line.lineNumber, widgetStartIndex), 50 | new Position(line.lineNumber, endIndex) 51 | ); 52 | } 53 | 54 | let bracketCount = 1; 55 | for (let l = line.lineNumber; l < editor.document.lineCount; l++) { 56 | const currentLine = editor.document.lineAt(l); 57 | let c = l === line.lineNumber ? openBracketIndex + 1 : 0; 58 | for (c; c < currentLine.text.length; c++) { 59 | const currentChar = currentLine.text.charAt(c); 60 | if (currentChar === openBracket) bracketCount++; 61 | if (currentChar === closeBracket) bracketCount--; 62 | if (bracketCount === 0) { 63 | return new Selection( 64 | new Position(line.lineNumber, widgetStartIndex), 65 | new Position(l, c + 1) 66 | ); 67 | } 68 | } 69 | } 70 | 71 | return emptySelection; 72 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | *.vsix 133 | 134 | # Flutter & Dart playground 135 | playground/ -------------------------------------------------------------------------------- /vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your VS Code Extension 2 | 3 | ## What's in the folder 4 | 5 | * This folder contains all of the files necessary for your extension. 6 | * `package.json` - this is the manifest file in which you declare your extension and command. 7 | * The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. 8 | * `src/extension.ts` - this is the main file where you will provide the implementation of your command. 9 | * The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. 10 | * We pass the function containing the implementation of the command as the second parameter to `registerCommand`. 11 | 12 | ## Setup 13 | 14 | * install the recommended extensions (amodio.tsl-problem-matcher, ms-vscode.extension-test-runner, and dbaeumer.vscode-eslint) 15 | 16 | 17 | ## Get up and running straight away 18 | 19 | * Press `F5` to open a new window with your extension loaded. 20 | * Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`. 21 | * Set breakpoints in your code inside `src/extension.ts` to debug your extension. 22 | * Find output from your extension in the debug console. 23 | 24 | ## Make changes 25 | 26 | * You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`. 27 | * You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. 28 | 29 | 30 | ## Explore the API 31 | 32 | * You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`. 33 | 34 | ## Run tests 35 | 36 | * Install the [Extension Test Runner](https://marketplace.visualstudio.com/items?itemName=ms-vscode.extension-test-runner) 37 | * Run the "watch" task via the **Tasks: Run Task** command. Make sure this is running, or tests might not be discovered. 38 | * Open the Testing view from the activity bar and click the Run Test" button, or use the hotkey `Ctrl/Cmd + ; A` 39 | * See the output of the test result in the Test Results view. 40 | * Make changes to `src/test/extension.test.ts` or create new test files inside the `test` folder. 41 | * The provided test runner will only consider files matching the name pattern `**.test.ts`. 42 | * You can create folders inside the `test` folder to structure your tests any way you want. 43 | 44 | ## Go further 45 | 46 | * Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension). 47 | * [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VS Code extension marketplace. 48 | * Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration). 49 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { Disposable } from "vscode"; 3 | import { sealedStates } from "./commands"; 4 | import { FlutterPlusConfig } from './config/config'; 5 | import { wrapWith } from './utils'; 6 | import { dartCodeExtensionIdentifier, flutterExtensionIdentifier } from "./constants"; 7 | import { CodeActionWrap } from './code-actions'; 8 | import { SdkCommands } from './utils'; 9 | 10 | const DART_MODE = { language: "dart", scheme: "file" }; 11 | 12 | export async function activate(context: vscode.ExtensionContext): Promise { 13 | // Ensure we have a Dart extension. 14 | const dartExt = vscode.extensions.getExtension(dartCodeExtensionIdentifier); 15 | if (!dartExt) { 16 | vscode.window.showErrorMessage("The Dart extension is not installed. Flutter extension cannot be activated."); 17 | return; 18 | } 19 | await dartExt.activate(); 20 | 21 | if (!dartExt.exports) { 22 | console.error("The Dart extension did not provide an exported API. It may have failed to activate or is not the latest version."); 23 | return; 24 | } 25 | 26 | // Ensure we have a Flutter extension. 27 | const flutterExt = vscode.extensions.getExtension(flutterExtensionIdentifier); 28 | if (!flutterExt) { 29 | vscode.window.showErrorMessage("The Flutter extension is not installed. Flutter Plus extension cannot be activated."); 30 | return; 31 | } 32 | await flutterExt.activate(); 33 | 34 | // Register SDK commands. 35 | const sdkCommands = new SdkCommands(context, dartExt.exports); 36 | 37 | registerCommands(context); 38 | registerWrappers(context); 39 | } 40 | 41 | /// Register all commands. 42 | function registerCommands(context: vscode.ExtensionContext) { 43 | context.subscriptions.push( 44 | vscode.commands.registerCommand("flutter-plus.sealed-states", sealedStates), 45 | ); 46 | } 47 | 48 | /// Register all wrappers (Wrap With...). 49 | function registerWrappers(context: vscode.ExtensionContext) { 50 | let wrappers = registerWrapperCommands(context); 51 | const disposable = vscode.workspace.onDidChangeConfiguration(event => { 52 | if (event.affectsConfiguration('flutter-plus')) { 53 | unregisterWrappers(wrappers); 54 | wrappers = registerWrapperCommands(context); 55 | } 56 | }); 57 | 58 | context.subscriptions.push(disposable); 59 | } 60 | 61 | function unregisterWrappers(disposables: Disposable[]) { 62 | disposables.forEach(disposable => disposable.dispose()); 63 | } 64 | 65 | function registerWrapperCommands(context: vscode.ExtensionContext): Disposable[] { 66 | try { 67 | const configWraps = FlutterPlusConfig.getInstance().getCustomWraps(); 68 | const wraps: CodeWrap[] = configWraps.map(wrap => ({ 69 | commandId: `flutter-plus.wrapWith.${wrap.name.toLowerCase().replace(/\s/g, "-")}`, 70 | title: `Wrap with ${wrap.name}`, 71 | command: () => wrapWith(selectedText => wrap.body.join("\n").replace("${widget}", selectedText)), 72 | })); 73 | 74 | const filteredWraps = wraps.filter((wrap, index, self) => 75 | index === self.findIndex(t => t.commandId === wrap.commandId) 76 | ); 77 | 78 | if (filteredWraps.length < wraps.length) { 79 | const duplicates = wraps.filter((wrap, index, self) => 80 | index !== self.findIndex(t => t.commandId === wrap.commandId) 81 | ); 82 | 83 | vscode.window.showWarningMessage(`Multiple wraps with the same command ID found: ${duplicates.map(d => d.commandId).join(", ")}`); 84 | } 85 | 86 | const subscriptions = filteredWraps.map(wrap => 87 | vscode.commands.registerCommand(wrap.commandId, wrap.command) 88 | ); 89 | 90 | subscriptions.push(vscode.languages.registerCodeActionsProvider(DART_MODE, new CodeActionWrap(wraps))); 91 | context.subscriptions.push(...subscriptions); 92 | 93 | return subscriptions; 94 | } catch (error) { 95 | vscode.window.showErrorMessage(`Error registering wraps: ${error}`); 96 | return []; 97 | } 98 | } 99 | 100 | export function deactivate() { } 101 | 102 | export type CodeWrap = { 103 | commandId: string, 104 | title: string, 105 | command: () => void, 106 | }; 107 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flutter-plus", 3 | "displayName": "Flutter Plus", 4 | "description": "Extension with various improvements for Flutter", 5 | "icon": "assets/logo.png", 6 | "version": "0.7.2", 7 | "pricing": "Free", 8 | "engines": { 9 | "vscode": "^1.92.0" 10 | }, 11 | "extensionKind": [ 12 | "workspace" 13 | ], 14 | "capabilities": { 15 | "virtualWorkspaces": { 16 | "supported": "limited", 17 | "description": "Some functionality may be limited for remote files in virtual workspaces." 18 | }, 19 | "untrustedWorkspaces": { 20 | "supported": false 21 | } 22 | }, 23 | "license": "SEE LICENSE IN LICENSE", 24 | "publisher": "plugfox", 25 | "bugs": { 26 | "url": "https://github.com/PlugFox/flutter-plus/issues", 27 | "email": "plugfox@gmail.com" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/PlugFox/flutter-plus" 32 | }, 33 | "homepage": "https://github.com/PlugFox/flutter-plus", 34 | "categories": [ 35 | "Snippets", 36 | "Programming Languages" 37 | ], 38 | "keywords": [ 39 | "Programming Languages", 40 | "Snippets", 41 | "Linters", 42 | "Formatters", 43 | "Debuggers", 44 | "Dart", 45 | "Flutter", 46 | "Actions", 47 | "Shortcuts" 48 | ], 49 | "activationEvents": [ 50 | "onLanguage:dart", 51 | "workspaceContains:pubspec.yaml", 52 | "workspaceContains:*/pubspec.yaml", 53 | "workspaceContains:*/*/pubspec.yaml", 54 | "workspaceContains:*.dart", 55 | "workspaceContains:*/*.dart", 56 | "workspaceContains:*/*/*.dart", 57 | "onCommand:flutter.createProject", 58 | "onCommand:dart.createProject", 59 | "onUri" 60 | ], 61 | "main": "./dist/extension.js", 62 | "contributes": { 63 | "commands": [ 64 | { 65 | "command": "flutter-plus.sealed-states", 66 | "title": "Create Sealed States", 67 | "category": "Flutter Plus" 68 | } 69 | ], 70 | "views": { 71 | "flutter": [ 72 | { 73 | "id": "flutter-plus.sidebar-actions", 74 | "name": "Actions", 75 | "when": "dart-code:anyFlutterProjectLoaded" 76 | } 77 | ] 78 | }, 79 | "submenus": [ 80 | { 81 | "id": "flutter-plus.submenu", 82 | "label": "Flutter Plus", 83 | "icon": "assets/logo.png" 84 | } 85 | ], 86 | "menus": { 87 | "editor/context": [ 88 | { 89 | "when": "editorLangId == dart", 90 | "submenu": "flutter-plus.submenu", 91 | "group": "flutter-plus" 92 | } 93 | ], 94 | "flutter-plus.submenu": [ 95 | { 96 | "when": "editorLangId == dart", 97 | "command": "flutter-plus.sealed-states" 98 | } 99 | ], 100 | "commandPalette": [] 101 | }, 102 | "snippets": [ 103 | { 104 | "language": "markdown", 105 | "path": "./snippets/markdown.json" 106 | }, 107 | { 108 | "language": "dart", 109 | "path": "./snippets/dart.json" 110 | }, 111 | { 112 | "language": "dart", 113 | "path": "./snippets/flutter.json" 114 | } 115 | ], 116 | "configuration": { 117 | "title": "Wraps", 118 | "properties": { 119 | "flutter-plus.wraps": { 120 | "description": "Set up custom Wrap With here", 121 | "type": "array", 122 | "items": { 123 | "type": "object", 124 | "properties": { 125 | "name": { 126 | "type": "string", 127 | "description": "Name of the Widget" 128 | }, 129 | "body": { 130 | "type": "array", 131 | "items": { 132 | "type": "string" 133 | }, 134 | "description": "Body of the Widget, use $ for cursor position and ${widget} for selected text" 135 | } 136 | } 137 | }, 138 | "default": [ 139 | { 140 | "name": "ListenableBuilder", 141 | "body": [ 142 | "ListenableBuilder(", 143 | " listenable: ${1:listenable},", 144 | " builder: (context, _) => ${widget},", 145 | ")" 146 | ] 147 | }, 148 | { 149 | "name": "ValueListenableBuilder", 150 | "body": [ 151 | "ValueListenableBuilder(", 152 | " valueListenable: ${1:valueListenable},", 153 | " builder: (context, value, _) => ${widget},", 154 | ")" 155 | ] 156 | }, 157 | { 158 | "name": "RepaintBoundary", 159 | "body": [ 160 | "RepaintBoundary(", 161 | " child: ${widget},", 162 | ")" 163 | ] 164 | }, 165 | { 166 | "name": "SliverPadding", 167 | "body": [ 168 | "SliverPadding(", 169 | " padding: const EdgeInsets.all(${1:8.0}),", 170 | " sliver: ${widget},", 171 | ")" 172 | ] 173 | } 174 | ] 175 | } 176 | } 177 | } 178 | }, 179 | "extensionDependencies": [ 180 | "Dart-Code.dart-code", 181 | "Dart-Code.flutter" 182 | ], 183 | "scripts": { 184 | "compile": "npm run check-types && npm run lint && node esbuild.js", 185 | "watch": "npm-run-all -p watch:*", 186 | "watch:esbuild": "node esbuild.js --watch", 187 | "watch:tsc": "tsc --noEmit --watch --project tsconfig.json", 188 | "package": "npm run check-types && npm run lint && node esbuild.js --production", 189 | "compile-tests": "tsc -p . --outDir out", 190 | "watch-tests": "tsc -p . -w --outDir out", 191 | "pretest": "npm run compile-tests && npm run compile && npm run lint", 192 | "check-types": "tsc --noEmit", 193 | "lint": "eslint src --ext ts", 194 | "test": "vscode-test", 195 | "vscode:prepublish": "npm run package", 196 | "deploy": "vsce publish" 197 | }, 198 | "devDependencies": { 199 | "@vscode/vsce": "^3.0.0", 200 | "@types/vscode": "^1.92.0", 201 | "@types/mocha": "^10.0.7", 202 | "@types/node": "20.x", 203 | "@typescript-eslint/eslint-plugin": "^7.14.1", 204 | "@typescript-eslint/parser": "^7.11.0", 205 | "eslint": "^8.57.0", 206 | "esbuild": "^0.21.5", 207 | "npm-run-all": "^4.1.5", 208 | "typescript": "^5.4.5", 209 | "@vscode/test-cli": "^0.0.9", 210 | "@vscode/test-electron": "^2.4.0" 211 | } 212 | } -------------------------------------------------------------------------------- /snippets/dart.json: -------------------------------------------------------------------------------- 1 | { 2 | "Main entry point": { 3 | "scope": "flutter, dart", 4 | "prefix": "main", 5 | "description": "Generates a main function with error handling and zone management for Dart applications.", 6 | "body": [ 7 | "import 'dart:async';\n", 8 | "@pragma('vm:entry-point')", 9 | "void main([List? args]) =>", 10 | " runZonedGuarded(() async {", 11 | " ${0:// ...}", 12 | " },", 13 | " (error, stackTrace) => print('\\$error\\n\\$stackTrace'), // ignore: avoid_print", 14 | " );\n" 15 | ] 16 | }, 17 | "Try-catch-finally block": { 18 | "scope": "flutter, dart", 19 | "prefix": "try", 20 | "description": "Creates a try-catch-finally block, useful for managing exceptions and ensuring cleanup code runs.", 21 | "body": [ 22 | "try {", 23 | " // ...", 24 | "} on Object catch (error, stackTrace) {", 25 | " rethrow; // or Error.throwWithStackTrace(error, stackTrace);", 26 | "} finally {", 27 | " // ...", 28 | "}" 29 | ] 30 | }, 31 | "Timeout handler": { 32 | "scope": "flutter, dart", 33 | "prefix": "timeout", 34 | "description": "Creates a timeout handler for setting execution limits on asynchronous operations.", 35 | "body": "timeout(const Duration(milliseconds: 5000))${0:;}" 36 | }, 37 | "Stopwatch": { 38 | "scope": "flutter, dart", 39 | "prefix": "stopwatch", 40 | "description": "Initializes a Stopwatch to measure and log elapsed time for code execution.", 41 | "body": [ 42 | "final stopwatch = Stopwatch()..start();", 43 | "try {", 44 | " /* ... */", 45 | "} finally {", 46 | " log(", 47 | " '${(stopwatch..stop()).elapsedMicroseconds} μs',", 48 | " name: 'select',", 49 | " level: 100,", 50 | " );", 51 | "}" 52 | ] 53 | }, 54 | "Platform conditional imports": { 55 | "scope": "flutter, dart", 56 | "prefix": [ 57 | "conditional", 58 | "conditional_imports", 59 | "import_conditional" 60 | ], 61 | "description": "Generates platform-specific imports based on the environment (VM or JS), ensuring compatibility across different platforms.", 62 | "body": [ 63 | "import 'stub.dart'", 64 | " // ignore: uri_does_not_exist", 65 | " if (dart.library.js_interop) 'js.dart'", 66 | " // ignore: uri_does_not_exist", 67 | " if (dart.library.io) 'io.dart';\n" 68 | ] 69 | }, 70 | "Divider": { 71 | "scope": "flutter, dart", 72 | "prefix": [ 73 | "dvd", 74 | "divider_comment" 75 | ], 76 | "description": "Inserts a divider comment line, useful for visually separating sections of code.", 77 | "body": "// --- ${1} --- //\n\n$0" 78 | }, 79 | "Reverse bypass": { 80 | "scope": "flutter, dart", 81 | "prefix": [ 82 | "reverseList" 83 | ], 84 | "description": "Generates a loop for traversing a list in reverse order.", 85 | "body": [ 86 | "for (var i = list.length - 1; i >= 0; i--) $0" 87 | ] 88 | }, 89 | "Part": { 90 | "scope": "flutter, dart", 91 | "prefix": "part", 92 | "description": "Adds a part directive to include the specified Dart file as part of the current library.", 93 | "body": [ 94 | "part '${TM_FILENAME_BASE}.g.dart';$0" 95 | ] 96 | }, 97 | "Mocks": { 98 | "scope": "flutter, dart", 99 | "prefix": "mocks", 100 | "description": "Imports a Dart file containing mock implementations for testing purposes.", 101 | "body": [ 102 | "import '${TM_FILENAME_BASE}.mocks.dart';$0" 103 | ] 104 | }, 105 | "toString": { 106 | "scope": "flutter, dart", 107 | "prefix": "toStr", 108 | "description": "Overrides the `toString` method for a custom object, providing a string representation of the object for debugging or logging.", 109 | "body": [ 110 | "@override", 111 | "String toString() => '${0}'" 112 | ] 113 | }, 114 | "Hash Code": { 115 | "scope": "flutter, dart", 116 | "prefix": [ 117 | "hashCode", 118 | "equals", 119 | "==" 120 | ], 121 | "description": "Generates hash code and equals methods, overriding the `==` operator and the `hashCode` getter for custom object comparison.", 122 | "body": [ 123 | "@override", 124 | "int get hashCode => id.hashCode;\n", 125 | "@override", 126 | "bool operator ==(Object other) =>", 127 | " identical(this, other) ||", 128 | " other is ${1:${TM_FILENAME_BASE/(.)(.*)/${1:/upcase}${2:/camelcase}/}} &&", 129 | " id == other.id;${0}\n" 130 | ] 131 | }, 132 | "No Such Method": { 133 | "scope": "flutter, dart", 134 | "prefix": [ 135 | "nosm", 136 | "noSuchMethod" 137 | ], 138 | "description": "Implements the `noSuchMethod` method, handling calls to non-existent methods or properties.", 139 | "body": [ 140 | "@override", 141 | "dynamic noSuchMethod(Invocation invocation) {", 142 | " ${1:}", 143 | "}" 144 | ] 145 | }, 146 | "Test": { 147 | "scope": "flutter, dart", 148 | "prefix": [ 149 | "test", 150 | "unitTest" 151 | ], 152 | "description": "Creates a test function, setting up a basic test case using the `test` package.", 153 | "body": [ 154 | "test('${1:test description}', () {", 155 | " ${0:expect(true, isTrue);}", 156 | "});\n" 157 | ] 158 | }, 159 | "Pragma": { 160 | "scope": "flutter, dart", 161 | "prefix": [ 162 | "pragma", 163 | "@pragma" 164 | ], 165 | "description": "Inserts a pragma directive to optimize or modify Dart VM/compiler behavior based on the specified options.", 166 | "body": "@pragma(${1|'vm:entry-point','vm:never-inline','vm:prefer-inline','dart2js:tryInline','vm:notify-debugger-on-exception','vm:invisible','vm:always-consider-inlining','flutter:keep-to-string','flutter:keep-to-string-in-subtypes'|})" 167 | }, 168 | "Dart doc disable documentation": { 169 | "scope": "flutter, dart", 170 | "prefix": [ 171 | "doc-disabled", 172 | "@doc-disabled" 173 | ], 174 | "description": "Adds a comment annotation to disable documentation generation for the specified block of code.", 175 | "body": [] 176 | }, 177 | "Dart doc category": { 178 | "scope": "flutter, dart", 179 | "prefix": [ 180 | "doc-category", 181 | "@doc-category" 182 | ], 183 | "description": "Categorizes a block of documentation with the specified category or subcategory tags.", 184 | "body": [ 185 | "/// {@${1|category,subCategory|} ${0}}" 186 | ] 187 | }, 188 | "Dart doc image": { 189 | "scope": "flutter, dart", 190 | "prefix": [ 191 | "doc-image", 192 | "@doc-image" 193 | ], 194 | "description": "Embeds an image within a block of documentation, using the specified URL as the source.", 195 | "body": [ 196 | "/// {@image }" 197 | ] 198 | }, 199 | "Dart doc animation": { 200 | "scope": "flutter, dart", 201 | "prefix": [ 202 | "doc-animation", 203 | "@doc-animation" 204 | ], 205 | "description": "Embeds an animation within a block of documentation, with options for specifying the size and source URL.", 206 | "body": "/// {@animation name 100 200 ${0:https://host.tld/path/to/video.mp4}}" 207 | }, 208 | "Dart doc new template": { 209 | "scope": "flutter, dart", 210 | "prefix": [ 211 | "newtmpl", 212 | "@template", 213 | "template", 214 | "doc-new-template", 215 | "doc-new-macro", 216 | "@doc-template" 217 | ], 218 | "description": "Creates a new Dart documentation template with the current file's name as the prefix, useful for reusing content across multiple documentation blocks.", 219 | "body": [ 220 | "/// {@template ${1:$TM_FILENAME_BASE}}", 221 | "/// ${0:Body of the template}", 222 | "/// {@endtemplate}" 223 | ] 224 | }, 225 | "Dart doc use macro": { 226 | "scope": "flutter, dart", 227 | "prefix": [ 228 | "usetmpl", 229 | "@macro", 230 | "macro", 231 | "doc-use-template", 232 | "doc-use-macro", 233 | "@doc-macro" 234 | ], 235 | "description": "Inserts an existing Dart documentation macro, using the current file's name as the prefix, to maintain consistency in documentation.", 236 | "body": "/// {@macro ${0:$TM_FILENAME_BASE}}" 237 | }, 238 | "Dart doc inject html": { 239 | "scope": "flutter, dart", 240 | "prefix": [ 241 | "doc-html", 242 | "@inject-html", 243 | "inject-html", 244 | "@doc-html" 245 | ], 246 | "description": "Injects custom HTML into a documentation comment, allowing for rich formatting or content inclusion.", 247 | "body": [ 248 | "/// {@inject-html}", 249 | "/// ${0:

[The HTML to inject.]()

}", 250 | "/// {@end-inject-html}" 251 | ] 252 | }, 253 | "Deprecated": { 254 | "scope": "flutter, dart", 255 | "prefix": [ 256 | "deprecated", 257 | "@deprecated" 258 | ], 259 | "description": "Marks a class, method, or property as deprecated, indicating that it should no longer be used and may be removed in future versions.", 260 | "body": "@Deprecated('${0:Reason}')" 261 | }, 262 | "Meta": { 263 | "scope": "flutter, dart", 264 | "prefix": [ 265 | "meta", 266 | "@meta", 267 | "@annotation", 268 | "annotation" 269 | ], 270 | "description": "Applies a meta annotation to a class, method, or property, providing additional metadata for tooling or code analysis purposes.", 271 | "body": "@${1|immutable,useResult,internal,protected,literal,mustCallSuper,sealed,alwaysThrows,factory,visibleForOverriding,visibleForTesting,experimental,nonVirtual,doNotStore,optionalTypeArgs|}" 272 | }, 273 | "Coverage": { 274 | "scope": "flutter, dart", 275 | "prefix": [ 276 | "coverage" 277 | ], 278 | "description": "Adds a coverage annotation to mark lines or blocks of code that should be ignored by test coverage tools.", 279 | "body": "// coverage:${1|ignore-line,ignore-start,ignore-end,ignore-file|}" 280 | } 281 | } -------------------------------------------------------------------------------- /src/commands/sealed-states.command.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as vscode from 'vscode'; 3 | import { Uri } from "vscode"; 4 | 5 | export const sealedStates = async (uri: Uri) => { 6 | // If no URI is provided, use the active text editor 7 | if (!uri) { 8 | const activeEditor = vscode.window.activeTextEditor; 9 | if (activeEditor) { 10 | uri = activeEditor.document.uri; 11 | } else { 12 | const selectedFiles = await vscode.window.showOpenDialog({ 13 | canSelectMany: false, 14 | openLabel: 'Select a Dart file', 15 | filters: { 16 | 'Dart Files': ['dart'] 17 | } 18 | }); 19 | 20 | if (!selectedFiles || selectedFiles.length === 0) { 21 | vscode.window.showErrorMessage('No file selected.'); 22 | return; 23 | } 24 | 25 | uri = selectedFiles[0]; 26 | } 27 | } 28 | 29 | let document = await vscode.workspace.openTextDocument(uri); 30 | let editor = vscode.window.activeTextEditor; 31 | 32 | if (!editor || editor.document.uri.fsPath !== uri.fsPath) { 33 | editor = await vscode.window.showTextDocument(document); 34 | } 35 | 36 | if (!editor) { 37 | vscode.window.showErrorMessage('Could not open the file.'); 38 | return; 39 | } 40 | 41 | // Extract the file name in a cross-platform way 42 | const fileName = path.basename(uri.fsPath, '.dart'); 43 | if (!fileName) { 44 | vscode.window.showErrorMessage('Invalid file name.'); 45 | return; 46 | } 47 | 48 | // Convert the file name to CamelCase 49 | let camelCaseName = fileName 50 | .split('_') 51 | .map(word => word.charAt(0).toUpperCase() + word.slice(1)) 52 | .join(''); 53 | 54 | // Ensure the name ends with "State" 55 | if (camelCaseName.endsWith('State') || camelCaseName.endsWith('States')) { 56 | camelCaseName = camelCaseName.replace(/States?$/, 'State'); 57 | } else { 58 | camelCaseName += 'State'; 59 | } 60 | 61 | // Prompt the user for the class name with a default value of CamelCase file name 62 | const classNameInput = await vscode.window.showInputBox({ 63 | prompt: 'Enter the class name', 64 | value: camelCaseName, 65 | }); 66 | 67 | if (!classNameInput) { 68 | vscode.window.showErrorMessage('Class name input was cancelled.'); 69 | return; 70 | } 71 | 72 | // Convert the classNameInput to snake_case 73 | const snakeCaseName = classNameInput 74 | .replace(/([a-z])([A-Z])/g, '$1_$2') 75 | .replace(/[\s-]/g, '_') 76 | .toLowerCase(); 77 | 78 | // Prompt the user for the list of states, defaulting to common states 79 | const statesInput = await vscode.window.showInputBox({ 80 | prompt: 'Enter the states (camelCase) separated by commas', 81 | value: 'idle, processing, succeeded, failed', 82 | }); 83 | 84 | if (!statesInput) { 85 | vscode.window.showErrorMessage('Input was cancelled.'); 86 | return; 87 | } 88 | 89 | // Prepare a dictionary with different state formats by "," and ";". 90 | const states = Array.from(new Set(statesInput.split(/,|;/) 91 | .map(state => state.replace(/\s/g, '').trim()) 92 | .filter(state => state.length !== 0) 93 | .filter(state => /^[a-zA-Z]/.test(state)) 94 | .filter(state => /^[A-Za-z0-9\s]+$/.test(state)) 95 | .map(state => state.charAt(0).toLowerCase() + state.slice(1)) 96 | )); 97 | 98 | if (states.length === 0) { 99 | vscode.window.showErrorMessage('Invalid states input.'); 100 | return; 101 | } 102 | 103 | const stateFormats = states.reduce((acc, state) => { 104 | const words = state.split(/(?=[A-Z])|_|-|\s/).filter(word => word.length > 0); 105 | 106 | const pascalCase = words.map((word) => { 107 | if (word.length === 1) { 108 | return word.toUpperCase(); 109 | } else { 110 | return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); 111 | } 112 | }).join(''); 113 | 114 | const camelCase = words.map((word, index) => { 115 | if (index === 0) { 116 | return word.toLowerCase(); 117 | } else if (word.length === 1) { 118 | return word.toUpperCase(); 119 | } else { 120 | return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); 121 | } 122 | }).join(''); 123 | 124 | const snakeCase = words.map(word => word.toLowerCase()).join('_'); 125 | 126 | acc[state] = { 127 | original: state, 128 | pascalCase: pascalCase, 129 | camelCase: camelCase, 130 | snakeCase: snakeCase 131 | }; 132 | 133 | return acc; 134 | }, {} as Record); 135 | 136 | const options = [ 137 | { label: "Nullable data", picked: true, id: 'nullableData' }, 138 | { label: "Generate pattern matching", picked: true, id: 'patternMatching' }, 139 | { label: "Generate toString method", picked: true, id: 'toStringMethod' }, 140 | { label: "Generate Initial state", picked: true, id: 'initialState' }, 141 | { label: "Generate property getters", picked: true, id: 'propertyGetters' }, 142 | { label: "Generate type alias", picked: true, id: 'typeAlias' }, 143 | { label: "Generate equality operator (==)", picked: true, id: 'equalityOperator' }, 144 | { label: "Add error object", picked: false, id: 'error' }, 145 | { label: "Add stackTrace", picked: false, id: 'stackTrace' }, 146 | ]; 147 | 148 | const selectedOptions = await vscode.window.showQuickPick(options, { 149 | canPickMany: true, 150 | placeHolder: 'Select the options you want to generate', 151 | }) ?? []; 152 | 153 | let nullableDataOption = selectedOptions.find(option => option.id === 'nullableData') !== undefined; 154 | let patternMatchingOption = selectedOptions.find(option => option.id === 'patternMatching') !== undefined; 155 | let equalityOperatorOption = selectedOptions.find(option => option.id === 'equalityOperator') !== undefined; 156 | let toStringMethodOption = selectedOptions.find(option => option.id === 'toStringMethod') !== undefined; 157 | let propertyGettersOption = selectedOptions.find(option => option.id === 'propertyGetters') !== undefined; 158 | let typeAliasOption = selectedOptions.find(option => option.id === 'typeAlias') !== undefined; 159 | let initialStateOption = selectedOptions.find(option => option.id === 'initialState') !== undefined; 160 | let errorOption = selectedOptions.find(option => option.id === 'error') !== undefined; 161 | let stackTraceOption = selectedOptions.find(option => option.id === 'stackTrace') !== undefined; 162 | 163 | const dataType = nullableDataOption ? '\${1}Entity?' : '\${1}Entity'; 164 | 165 | // Generate the code using a StringBuilder approach 166 | let codeBuilder: string[] = []; 167 | 168 | // Import statements 169 | codeBuilder.push(`import 'package:meta/meta.dart';`); 170 | codeBuilder.push(''); 171 | codeBuilder.push(`/// Entity placeholder`); 172 | codeBuilder.push(`typedef \${1:${classNameInput}}Entity = \${0:Object};`); 173 | codeBuilder.push(''); 174 | codeBuilder.push(`/// {@template \${2:${snakeCaseName}}}`); 175 | codeBuilder.push(`/// \${1}.`); 176 | codeBuilder.push(`/// {@endtemplate}`); 177 | codeBuilder.push(`sealed class \${1} extends _\\$\${1}Base {`); 178 | 179 | // Constructor 180 | codeBuilder.push(` /// {@macro \${2}}`); 181 | codeBuilder.push(` const \${1}({required super.data, required super.message${errorOption ? ', super.error' : ''}${stackTraceOption ? ', super.stackTrace' : ''}});`); 182 | 183 | // Generate the factory constructors for each state 184 | Object.values(stateFormats).forEach(({ pascalCase, camelCase }) => { 185 | codeBuilder.push(''); 186 | codeBuilder.push(` /// ${pascalCase}`); 187 | codeBuilder.push(` /// {@macro \${2}}`); 188 | codeBuilder.push(` const factory \${1}.${camelCase}({`); 189 | if (nullableDataOption) { 190 | codeBuilder.push(` ${dataType} data,`); 191 | } else { 192 | codeBuilder.push(` required ${dataType} data,`); 193 | } 194 | codeBuilder.push(` String message,`); 195 | if (errorOption) { 196 | codeBuilder.push(` Object? error,`); 197 | } 198 | if (stackTraceOption) { 199 | codeBuilder.push(` StackTrace? stackTrace,`); 200 | } 201 | codeBuilder.push(` }) = \${1}\\$${pascalCase};`); 202 | }); 203 | 204 | // Initial state 205 | if (initialStateOption && Object.values(stateFormats).every(({ camelCase }) => camelCase !== 'initial')) { 206 | codeBuilder.push(''); 207 | codeBuilder.push(` /// Initial`); 208 | codeBuilder.push(` /// {@macro \${2}}`); 209 | codeBuilder.push(` factory \${1}.initial({`); 210 | if (nullableDataOption) { 211 | codeBuilder.push(` ${dataType} data,`); 212 | } else { 213 | codeBuilder.push(` required ${dataType} data,`); 214 | } 215 | codeBuilder.push(` String? message,`); 216 | if (errorOption) { 217 | codeBuilder.push(` Object? error,`); 218 | } 219 | if (stackTraceOption) { 220 | codeBuilder.push(` StackTrace? stackTrace,`); 221 | } 222 | codeBuilder.push(` }) =>`); 223 | if (Object.values(stateFormats).find(({ camelCase }) => camelCase === 'idle')) { 224 | codeBuilder.push(` \${1}\\$Idle(`); 225 | } else { 226 | codeBuilder.push(` \${1}\\$${Object.values(stateFormats)[0].pascalCase}(`); 227 | } 228 | codeBuilder.push(` data: data,`); 229 | codeBuilder.push(` message: message ?? 'Initial',`); 230 | if (errorOption) { 231 | codeBuilder.push(` error: error,`); 232 | } 233 | if (stackTraceOption) { 234 | codeBuilder.push(` stackTrace: stackTrace,`); 235 | } 236 | codeBuilder.push(` );`); 237 | } 238 | 239 | codeBuilder.push(`}`); 240 | 241 | // Generate the classes for each state 242 | Object.values(stateFormats).forEach(({ pascalCase, snakeCase }) => { 243 | codeBuilder.push(''); 244 | codeBuilder.push(`/// ${pascalCase}`); 245 | codeBuilder.push(`final class \${1}\\$${pascalCase} extends \${1} {`); 246 | 247 | if (nullableDataOption) { 248 | codeBuilder.push(` const \${1}\\$${pascalCase}({super.data, super.message = '${pascalCase}'${errorOption ? ', super.error' : ''}${stackTraceOption ? ', super.stackTrace' : ''}`); 249 | } else { 250 | codeBuilder.push(` const \${1}\\$${pascalCase}({required super.data, super.message = '${pascalCase}'${errorOption ? ', super.error' : ''}${stackTraceOption ? ', super.stackTrace' : ''}`); 251 | } 252 | codeBuilder.push(`});`); 253 | 254 | if (typeAliasOption) { 255 | codeBuilder.push(''); 256 | codeBuilder.push(` @override`); 257 | codeBuilder.push(` String get type => '${snakeCase}';`); 258 | } 259 | 260 | codeBuilder.push(`}`); 261 | }); 262 | 263 | // Base class definition with pattern matching methods 264 | if (patternMatchingOption) { 265 | codeBuilder.push(''); 266 | codeBuilder.push(`/// Pattern matching for [\${1}].`); 267 | codeBuilder.push(`typedef \${1}Match = R Function(S element);`); 268 | } 269 | 270 | // Base class definition 271 | codeBuilder.push(''); 272 | codeBuilder.push('@immutable'); 273 | codeBuilder.push(`abstract base class _\\$\${1}Base {`); 274 | codeBuilder.push(` const _\\$\${1}Base({required this.data, required this.message${errorOption ? ', this.error' : ''}${stackTraceOption ? ', this.stackTrace' : ''}`); 275 | codeBuilder.push(`});`); 276 | 277 | // Type alias 278 | if (typeAliasOption) { 279 | codeBuilder.push(''); 280 | codeBuilder.push(` /// Type alias for [\${1}].`); 281 | codeBuilder.push(` abstract final String type;`); 282 | } 283 | 284 | // Data entity payload 285 | codeBuilder.push(''); 286 | codeBuilder.push(` /// Data entity payload.`); 287 | codeBuilder.push(` @nonVirtual`); 288 | codeBuilder.push(` final ${dataType} data;`); 289 | 290 | // Message or description 291 | codeBuilder.push(''); 292 | codeBuilder.push(` /// Message or description.`); 293 | codeBuilder.push(` @nonVirtual`); 294 | codeBuilder.push(` final String message;`); 295 | 296 | // Error object 297 | if (errorOption) { 298 | codeBuilder.push(''); 299 | codeBuilder.push(` /// Error object.`); 300 | codeBuilder.push(` @nonVirtual`); 301 | codeBuilder.push(` final Object? error;`); 302 | } 303 | 304 | // Stack trace object 305 | if (stackTraceOption) { 306 | codeBuilder.push(''); 307 | codeBuilder.push(` /// Stack trace object.`); 308 | codeBuilder.push(` @nonVirtual`); 309 | codeBuilder.push(` final StackTrace? stackTrace;`); 310 | } 311 | 312 | // Check existence of data 313 | if (nullableDataOption) { 314 | codeBuilder.push(''); 315 | codeBuilder.push(` /// Has data?`); 316 | codeBuilder.push(` bool get hasData => data != null;`); 317 | } 318 | 319 | // Property getters 320 | if (propertyGettersOption) { 321 | Object.values(stateFormats).forEach(({ pascalCase, snakeCase }) => { 322 | codeBuilder.push(''); 323 | codeBuilder.push(` /// Check if is ${pascalCase}.`); 324 | codeBuilder.push(` bool get is${pascalCase} => this is \${1}\\$${pascalCase};`); 325 | }); 326 | } 327 | 328 | // Pattern matching methods 329 | if (patternMatchingOption) { 330 | codeBuilder.push(''); 331 | codeBuilder.push(` /// Pattern matching for [\${1}].`); 332 | codeBuilder.push(` R map({`); 333 | Object.values(stateFormats).forEach(({ pascalCase, camelCase }) => { 334 | codeBuilder.push(` required \${1}Match ${camelCase},`); 335 | }); 336 | codeBuilder.push(` }) =>`); 337 | codeBuilder.push(` switch (this) {`); 338 | Object.values(stateFormats).forEach(({ pascalCase, camelCase }) => { 339 | codeBuilder.push(` \${1}\\$${pascalCase} s => ${camelCase}(s),`); 340 | }); 341 | codeBuilder.push(` _ => throw AssertionError(),`); 342 | codeBuilder.push(` };`); 343 | codeBuilder.push(''); 344 | codeBuilder.push(` /// Pattern matching for [\${1}].`); 345 | codeBuilder.push(` R maybeMap({`); 346 | codeBuilder.push(` required R Function() orElse,`); 347 | Object.values(stateFormats).forEach(({ pascalCase, camelCase }) => { 348 | codeBuilder.push(` \${1}Match? ${camelCase},`); 349 | }); 350 | codeBuilder.push(` }) =>`); 351 | codeBuilder.push(` map(`); 352 | Object.values(stateFormats).forEach(({ camelCase }) => { 353 | codeBuilder.push(` ${camelCase}: ${camelCase} ?? (_) => orElse(),`); 354 | }); 355 | codeBuilder.push(` );`); 356 | codeBuilder.push(''); 357 | codeBuilder.push(` /// Pattern matching for [\${1}].`); 358 | codeBuilder.push(` R? mapOrNull({`); 359 | Object.values(stateFormats).forEach(({ pascalCase, camelCase }) => { 360 | codeBuilder.push(` \${1}Match? ${camelCase},`); 361 | }); 362 | codeBuilder.push(` }) =>`); 363 | codeBuilder.push(` map(`); 364 | Object.values(stateFormats).forEach(({ camelCase }) => { 365 | codeBuilder.push(` ${camelCase}: ${camelCase} ?? (_) => null,`); 366 | }); 367 | codeBuilder.push(` );`); 368 | } 369 | 370 | // Equality operator 371 | if (equalityOperatorOption) { 372 | codeBuilder.push(''); 373 | if (typeAliasOption) { 374 | codeBuilder.push(' @override'); 375 | codeBuilder.push(` int get hashCode => Object.hash(type, data);`); 376 | codeBuilder.push(''); 377 | codeBuilder.push(' @override'); 378 | codeBuilder.push(` bool operator ==(Object other) => identical(this, other)`); 379 | codeBuilder.push(` || (other is _\\$\${1}Base && type == other.type && identical(data, other.data));`); 380 | } else { 381 | codeBuilder.push(' @override'); 382 | codeBuilder.push(` int get hashCode => data.hashCode;`); 383 | codeBuilder.push(''); 384 | codeBuilder.push(' @override'); 385 | codeBuilder.push(` bool operator ==(Object other) => identical(this, other)`); 386 | codeBuilder.push(` || (other is _\\$\${1}Base && runtimeType == other.runtimeType && identical(data, other.data));`); 387 | } 388 | } 389 | 390 | // Generate toString method 391 | if (toStringMethodOption) { 392 | codeBuilder.push(''); 393 | codeBuilder.push(' @override'); 394 | if (typeAliasOption) { 395 | codeBuilder.push(` String toString() => '\${1}.\\$type{message: \\$message}';`); 396 | } else { 397 | codeBuilder.push(` String toString() => '\${1}{message: \\$message}';`); 398 | } 399 | } 400 | codeBuilder.push('}'); 401 | codeBuilder.push(''); 402 | 403 | // Insert the generated code into the current document 404 | //const editor = vscode.window.activeTextEditor; 405 | if (editor) { 406 | editor.insertSnippet(new vscode.SnippetString(codeBuilder.join('\n'))); 407 | /* editor.edit(editBuilder => { 408 | editBuilder.insert(new vscode.Position(editor.document.lineCount, 0), codeBuilder.join('\n')); 409 | }); */ 410 | } else { 411 | vscode.window.showErrorMessage('No active editor found.'); 412 | } 413 | }; 414 | 415 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flutter Plus 2 | 3 | Extension add some useful commands to Flutter development in Visual Studio Code. 4 | 5 | ## Wrap with... 6 | 7 | This package extends your Flutter development experience by providing convenient snippets and commands for wrapping widgets with other commonly used Flutter widgets, similar to the "Wrap with..." functionality in Flutter's built-in extension for VS Code. These additions streamline the process of encapsulating your widgets within other widgets that enhance functionality or control, such as state management, repaint isolation, and more. 8 | 9 | Simply select the widget you want to wrap, and choose the appropriate "Wrap with..." command from the command palette, or use the provided snippets to quickly insert the desired wrapper code into your widget tree. 10 | 11 | This extension includes the following standard "Wrap with..." commands: 12 | 13 | - **Wrap with ListenableBuilder**: Easily wrap any widget with a `ListenableBuilder` to rebuild the widget based on changes in a `Listenable` object. 14 | - **Wrap with ValueListenableBuilder**: Automatically wrap your widget with a `ValueListenableBuilder` to react to changes in a `ValueListenable`. 15 | - **Wrap with RepaintBoundary**: Encapsulate your widget within a `RepaintBoundary` to isolate its repaint process, improving performance in complex UIs. 16 | - **Wrap with SliverPadding**: Wrap your widget with a `SliverPadding` to add padding around a sliver widget in a `CustomScrollView`. 17 | 18 | In order to add custom "Wrap with" commands, you can change the configuration in `flutter-plus.wraps` settings. The configuration is an array of objects with the following properties: 19 | 20 | - **name**: The name of the command that will appear in the command palette. 21 | - **body**: The snippet body that will be inserted into the editor when the command is executed. Use `${0...N}` to indicate the position of the selected text. In order to insert the selected text, use `${widget}`. 22 | 23 | Example configuration: 24 | 25 | ```json 26 | { 27 | "wraps": [ 28 | { 29 | "name": "Wrap with CustomWidget", 30 | "body": [ 31 | "CustomWidget(", 32 | " child: ${widget},", 33 | ")" 34 | ] 35 | } 36 | ] 37 | } 38 | ``` 39 | 40 | ## Markdown snippets 41 | 42 | | Shortcut | Description | 43 | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 44 | | unreleased | Create a new unreleased section. Used to list features that are not yet released. | 45 | | version | Create a new version section. Used to list features that are released in a specific version. | 46 | 47 | 48 | ## Dart snippets 49 | 50 | | Shortcut | Description | 51 | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 52 | | main | Generates a main function with error handling and zone management for Dart applications. | 53 | | try | Creates a try-catch-finally block, useful for managing exceptions and ensuring cleanup code runs. | 54 | | timeout | Creates a timeout handler for setting execution limits on asynchronous operations. | 55 | | stopwatch | Initializes a Stopwatch to measure and log elapsed time for code execution. | 56 | | conditional | Generates platform-specific imports based on the environment (VM or JS), ensuring compatibility across different platforms. | 57 | | dvd | Inserts a divider comment line, useful for visually separating sections of code. | 58 | | reverseList | Generates a loop for traversing a list in reverse order. | 59 | | part | Adds a part directive to include the specified Dart file as part of the current library. | 60 | | mocks | Imports a Dart file containing mock implementations for testing purposes. | 61 | | toStr | Overrides the `toString` method for a custom object, providing a string representation of the object for debugging or logging. | 62 | | equals | Generates hash code and equals methods, overriding the `==` operator and the `hashCode` getter for custom object comparison. | 63 | | nosm | Implements the `noSuchMethod` method, handling calls to non-existent methods or properties. | 64 | | test | Creates a test function, setting up a basic test case using the `test` package. | 65 | | pragma | Inserts a pragma directive to optimize or modify Dart VM/compiler behavior based on the specified options. | 66 | | doc-disabled | Adds a comment annotation to disable documentation generation for the specified block of code. | 67 | | doc-category | Categorizes a block of documentation with the specified category or subcategory tags. | 68 | | doc-image | Embeds an image within a block of documentation, using the specified URL as the source. | 69 | | doc-animation | Embeds an animation within a block of documentation, with options for specifying the size and source URL. | 70 | | doc-html | Injects custom HTML into a documentation comment, allowing for rich formatting or content inclusion. | 71 | | newtmpl | Creates a new Dart documentation template with the current file's name as the prefix, useful for reusing content across multiple documentation blocks. | 72 | | usetmpl | Inserts an existing Dart documentation macro, using the current file's name as the prefix, to maintain consistency in documentation. | 73 | | deprecated | Marks a class, method, or property as deprecated, indicating that it should no longer be used and may be removed in future versions. | 74 | | meta | Applies a meta annotation to a class, method, or property, providing additional metadata for tooling or code analysis purposes. | 75 | | coverage | Adds a coverage annotation to mark lines or blocks of code that should be ignored by test coverage tools. | 76 | 77 | 78 | ## Flutter snippets 79 | 80 | | Shortcut | Description | 81 | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 82 | | mateapp | Creates a new `MaterialApp` widget, an application that uses Material Design components and theming. | 83 | | cupeapp | Creates a new `CupertinoApp` widget, an application that uses Cupertino (iOS-style) design components and theming. | 84 | | scaffold | Creates a new `Scaffold` widget, implementing the basic Material Design visual layout structure including app bar, drawer, and floating action button. | 85 | | stl | Generates a new `StatelessWidget` class, a widget that does not require mutable state and is rebuilt only when the configuration changes. | 86 | | stlChild | Generates a new `StatelessWidget` class with a child widget, for cases where a single child widget needs to be passed and rendered. | 87 | | stf | Generates a new `StatefulWidget` class, a widget that has mutable state that can change over time, and its associated `State` class. | 88 | | stfChild | Generates a new `StatefulWidget` class with a child widget, allowing for a stateful widget to have a single child widget passed in and managed. | 89 | | inh | Creates a new `InheritedWidget` class, which efficiently propagates information down the widget tree to descendants. | 90 | | of | Generates a static `of` method for an `InheritedWidget`, returning the nearest widget of the specified type and creating a dependency on it. | 91 | | stateOf | Generates a static `stateOf` method for a `State` object, returning the `State` object of the nearest ancestor `StatefulWidget` of the specified type. | 92 | | widgetOf | Generates a static `widgetOf` method for a `Widget` object, returning the nearest ancestor widget of the specified type. | 93 | | painter | Creates a new `CustomPainter` class, which provides a canvas on which to draw during the paint phase, used for drawing custom shapes and graphics. | 94 | | clipper | Creates a new `CustomClipper` class, used for defining custom clipping shapes for widgets. | 95 | | debugFillProperties | Generates a `debugFillProperties` method to add additional properties associated with the widget for debugging purposes. | 96 | | initS | Creates an `initState` method, called when this object is inserted into the widget tree for the first time, typically used to initialize state. | 97 | | dispose | Creates a `dispose` method, called when this object is permanently removed from the widget tree, used for cleanup of resources. | 98 | | reassemble | Creates a `reassemble` method, called during debugging whenever the application is reassembled, for example, during a hot reload. | 99 | | didChangeD | Creates a `didChangeDependencies` method, called when a dependency of the `State` object changes. | 100 | | didUpdateW | Creates a `didUpdateWidget` method, called whenever the widget’s configuration changes. | 101 | | build | Generates a `build` method, describing the part of the user interface represented by this widget. | 102 | | listViewBuilder | Creates a `ListView.builder`, a scrollable list that lazily builds its children as they scroll into view, useful for large or infinite lists. | 103 | | listViewSeparated | Creates a `ListView.separated`, a scrollable list that displays a separator widget between each child widget, ideal for lists with visually distinct sections. | 104 | | gridViewBuilder | Creates a `GridView.builder`, a scrollable grid of widgets that are created on demand. | 105 | | gridViewCount | Creates a `GridView.count`, a scrollable grid of widgets with a fixed number of tiles in the cross axis. | 106 | | gridViewE | Creates a `GridView.extent`, a scrollable grid of widgets with tiles that each have a maximum cross-axis extent. | 107 | | customScrollV | Creates a `CustomScrollView`, a scrollable area that creates custom scroll effects using slivers. | 108 | | builder | Creates a `Builder` widget, which allows you to create a child widget in a way that depends on the `BuildContext`. | 109 | | wrapWithBuilder | Wraps the selected widget with a `Builder` widget, delegating the widget building process to a callback. | 110 | | stfBuilder | Creates a `StatefulBuilder` widget, which allows for state management and rebuilding a specific portion of the widget tree. | 111 | | strBuilder | Creates a `StreamBuilder` widget, which rebuilds itself based on the latest snapshot of interaction with a `Stream`. | 112 | | lisBuilder | Creates a `ListenableBuilder` widget, which rebuilds itself based on the latest value of a `Listenable` it listens to. | 113 | | valLisBuilder | Creates a `ValueListenableBuilder` widget, which rebuilds itself based on the latest value of a `ValueListenable`. | 114 | | animBuilder | Creates an `AnimatedBuilder` widget, which rebuilds itself based on an animation. | 115 | | switcher | Creates an `AnimatedSwitcher` widget, which switches between two children and animates the transition between them. | 116 | | orientBuilder | Creates an `OrientationBuilder` widget, which rebuilds itself based on the latest orientation of the device (portrait or landscape). | 117 | | layBuilder | Creates a `LayoutBuilder` widget, which rebuilds itself based on the latest layout constraints, useful for responsive layouts. | 118 | | repBound | Creates a `RepaintBoundary` widget, which isolates repaints so that a repaint of one child is separate from others, improving performance. | 119 | | singleChildSV | Creates a `SingleChildScrollView` widget, which allows a single child to be scrolled. | 120 | | futBuilder | Creates a `FutureBuilder` widget, which rebuilds itself based on the latest snapshot of interaction with a `Future`. | 121 | | tweenAnimBuilder | Creates a `TweenAnimationBuilder` widget, which animates a property of a widget to a target value whenever the target value changes. | 122 | | testWidget | Creates a `testWidgets` function for testing a widget, typically used in Flutter's unit testing framework. | 123 | | row | Creates a `Row` widget, which displays its children in a horizontal array. | 124 | | col | Creates a `Column` widget, which displays its children in a vertical array. | 125 | | wrap | Creates a `Wrap` widget, which displays its children in multiple horizontal or vertical runs, wrapping to the next line when necessary. | 126 | | stack | Creates a `Stack` widget, which allows you to place widgets on top of each other in a z-order. | 127 | | fittedbox | Creates a `FittedBox` widget, which scales and positions its child within itself according to the specified fit. | 128 | -------------------------------------------------------------------------------- /snippets/flutter.json: -------------------------------------------------------------------------------- 1 | { 2 | "Material App": { 3 | "scope": "flutter, dart", 4 | "prefix": "mateapp", 5 | "description": "Creates a MaterialApp widget, a base application structure using Material Design components.", 6 | "body": [ 7 | "import 'dart:async';\n", 8 | "import 'package:flutter/material.dart';\n", 9 | "void main() => runZonedGuarded(", 10 | " () => runApp(const App()),", 11 | " (error, stackTrace) => print('Top level exception: \\$error\\n\\$stackTrace'), // ignore: avoid_print", 12 | " );\n", 13 | "/// {@template app}", 14 | "/// App widget.", 15 | "/// {@endtemplate}", 16 | "class App extends StatelessWidget {", 17 | " /// {@macro app}", 18 | " const App({super.key});\n", 19 | " @override", 20 | " Widget build(BuildContext context) => MaterialApp(", 21 | " title: 'Material App',", 22 | " home: Scaffold(", 23 | " appBar: AppBar(", 24 | " title: const Text('Material App Bar'),", 25 | " ),", 26 | " body: const SafeArea(", 27 | " child: Center(", 28 | " child: Text('Hello World'),", 29 | " ),", 30 | " ),", 31 | " ),", 32 | " );", 33 | "}\n" 34 | ] 35 | }, 36 | "Cupertino App": { 37 | "scope": "flutter, dart", 38 | "prefix": "cupeapp", 39 | "description": "Creates a CupertinoApp widget, a base application structure using Cupertino (iOS-style) design components.", 40 | "body": [ 41 | "import 'dart:async';\n", 42 | "import 'package:flutter/cupertino.dart';\n\n", 43 | "void main() => runZonedGuarded(", 44 | " () => runApp(const App()),", 45 | " (error, stackTrace) => print('Top level exception: \\$error\\n\\$stackTrace'), // ignore: avoid_print", 46 | " );\n", 47 | "/// {@template app}", 48 | "/// App widget.", 49 | "/// {@endtemplate}", 50 | "class App extends StatelessWidget {", 51 | " /// {@macro app}", 52 | " const App({super.key});\n", 53 | " @override", 54 | " Widget build(BuildContext context) => const CupertinoApp(", 55 | " title: 'Cupertino App',", 56 | " home: CupertinoPageScaffold(", 57 | " navigationBar: CupertinoNavigationBar(", 58 | " middle: Text('Cupertino App Bar'),", 59 | " ),", 60 | " child: SafeArea(", 61 | " child: Center(", 62 | " child: Text('Hello World'),", 63 | " ),", 64 | " ),", 65 | " ),", 66 | " );", 67 | "}\n" 68 | ] 69 | }, 70 | "Scaffold": { 71 | "scope": "flutter, dart", 72 | "prefix": "scaffold", 73 | "description": "Creates a Scaffold widget, the basic structure for Material Design visual layout.", 74 | "body": [ 75 | "Scaffold(", 76 | " appBar: AppBar(", 77 | " title: const Text('Title'),", 78 | " ),", 79 | " body: SafeArea(", 80 | " child: Center(", 81 | " child: Placeholder(),", 82 | " ),", 83 | " ),", 84 | ");" 85 | ] 86 | }, 87 | "Stateless Widget": { 88 | "scope": "flutter, dart", 89 | "prefix": [ 90 | "stl", 91 | "statelessWidget" 92 | ], 93 | "description": "Generates a new StatelessWidget class.", 94 | "body": [ 95 | "import 'package:flutter/widgets.dart';\n\n", 96 | "/// {@template ${2:$TM_FILENAME_BASE}}", 97 | "/// ${1:${TM_FILENAME_BASE/(.)(.*)/${1:/upcase}${2:/camelcase}/}} widget.", 98 | "/// {@endtemplate}", 99 | "class $1 extends StatelessWidget {", 100 | " /// {@macro $2}", 101 | " const $1({", 102 | " super.key, // ignore: unused_element_parameter", 103 | " });", 104 | " ", 105 | " @override", 106 | " Widget build(BuildContext context) =>", 107 | " const Placeholder();${0}", 108 | "}\n" 109 | ] 110 | }, 111 | "Stateless Widget with child": { 112 | "scope": "flutter, dart", 113 | "prefix": [ 114 | "stlChild", 115 | "statelessWidgetWithChild" 116 | ], 117 | "description": "Generates a new StatelessWidget class with a child widget.", 118 | "body": [ 119 | "import 'package:flutter/widgets.dart';\n\n", 120 | "/// {@template ${2:$TM_FILENAME_BASE}}", 121 | "/// ${1:${TM_FILENAME_BASE/(.)(.*)/${1:/upcase}${2:/camelcase}/}} widget.", 122 | "/// {@endtemplate}", 123 | "class $1 extends StatelessWidget {", 124 | " /// {@macro $2}", 125 | " const $1({", 126 | " required this.child,", 127 | " super.key, // ignore: unused_element_parameter", 128 | " });", 129 | " ", 130 | " /// The widget below this widget in the tree.", 131 | " final Widget child;", 132 | " ", 133 | " @override", 134 | " Widget build(BuildContext context) =>", 135 | " child;${0}", 136 | "}\n" 137 | ] 138 | }, 139 | "Stateful Widget": { 140 | "scope": "flutter, dart", 141 | "prefix": [ 142 | "stf", 143 | "statefulWidget" 144 | ], 145 | "description": "Generates a new StatefulWidget class.", 146 | "body": [ 147 | "import 'package:flutter/widgets.dart';", 148 | "import 'package:meta/meta.dart';\n\n", 149 | "/// {@template ${2:$TM_FILENAME_BASE}}", 150 | "/// ${1:${TM_FILENAME_BASE/(.)(.*)/${1:/upcase}${2:/camelcase}/}} widget.", 151 | "/// {@endtemplate}", 152 | "class ${1} extends StatefulWidget {", 153 | " /// {@macro $2}", 154 | " const ${1}({", 155 | " super.key, // ignore: unused_element_parameter", 156 | " });", 157 | " ", 158 | " /// The state from the closest instance of this class", 159 | " /// that encloses the given context, if any.", 160 | " @internal", 161 | " static _${1}State? maybeOf(BuildContext context) =>", 162 | " context.findAncestorStateOfType<_${1}State>();", 163 | " ", 164 | " @override", 165 | " State<${1}> createState() => _${1}State();", 166 | "}\n", 167 | "", 168 | "/// State for widget ${1}.", 169 | "class _${1}State extends State<${1}> {", 170 | "", 171 | " /* #region Lifecycle */", 172 | " @override", 173 | " void initState() {", 174 | " super.initState();", 175 | " // Initial state initialization", 176 | " }", 177 | " ", 178 | " @override", 179 | " void didUpdateWidget(covariant ${1} oldWidget) {", 180 | " super.didUpdateWidget(oldWidget);", 181 | " // Widget configuration changed", 182 | " }", 183 | " ", 184 | " @override", 185 | " void didChangeDependencies() {", 186 | " super.didChangeDependencies();", 187 | " // The configuration of InheritedWidgets has changed", 188 | " // Also called after initState but before build", 189 | " }", 190 | " ", 191 | " @override", 192 | " void dispose() {", 193 | " // Permanent removal of a tree stent", 194 | " super.dispose();", 195 | " }", 196 | " /* #endregion */", 197 | " ", 198 | " @override", 199 | " Widget build(BuildContext context) =>", 200 | " const Placeholder();${0}", 201 | "}\n" 202 | ] 203 | }, 204 | "Stateful Widget with child": { 205 | "scope": "flutter, dart", 206 | "prefix": [ 207 | "stfChild", 208 | "statefulfWithChild" 209 | ], 210 | "description": "Generates a new StatefulWidget class with a child widget.", 211 | "body": [ 212 | "import 'package:flutter/widgets.dart';", 213 | "import 'package:meta/meta.dart';\n\n", 214 | "/// {@template ${2:$TM_FILENAME_BASE}}", 215 | "/// ${1:${TM_FILENAME_BASE/(.)(.*)/${1:/upcase}${2:/camelcase}/}} widget.", 216 | "/// {@endtemplate}", 217 | "class ${1} extends StatefulWidget {", 218 | " /// {@macro $2}", 219 | " const $1({", 220 | " required this.child,", 221 | " super.key, // ignore: unused_element_parameter", 222 | " });", 223 | " ", 224 | " /// The widget below this widget in the tree.", 225 | " final Widget child;", 226 | " ", 227 | " /// The state from the closest instance of this class", 228 | " /// that encloses the given context, if any.", 229 | " static ${1}Controller? maybeOf(BuildContext context) =>", 230 | " context.findAncestorStateOfType<_${1}State>();", 231 | " ", 232 | " @override", 233 | " State<${1}> createState() => _${1}State();", 234 | "}\n", 235 | "", 236 | "/// State for widget ${1}.", 237 | "class _${1}State extends State<${1}> with ${1}Controller {", 238 | "", 239 | " /* #region Lifecycle */", 240 | " @override", 241 | " void initState() {", 242 | " super.initState();", 243 | " // Initial state initialization", 244 | " }", 245 | " ", 246 | " @override", 247 | " void didUpdateWidget(covariant ${1} oldWidget) {", 248 | " super.didUpdateWidget(oldWidget);", 249 | " // Widget configuration changed", 250 | " }", 251 | " ", 252 | " @override", 253 | " void didChangeDependencies() {", 254 | " super.didChangeDependencies();", 255 | " // The configuration of InheritedWidgets has changed", 256 | " // Also called after initState but before build", 257 | " }", 258 | " ", 259 | " @override", 260 | " void dispose() {", 261 | " // Permanent removal of a tree stent", 262 | " super.dispose();", 263 | " }", 264 | " /* #endregion */", 265 | " ", 266 | " @override", 267 | " Widget build(BuildContext context) =>", 268 | " widget.child;", 269 | "}\n", 270 | "/// Controller for widget ${1}", 271 | "mixin ${1}Controller {", 272 | " ${0}", 273 | "}\n" 274 | ] 275 | }, 276 | "Inherited Widget": { 277 | "scope": "flutter, dart", 278 | "prefix": [ 279 | "inh", 280 | "inheritedWidget" 281 | ], 282 | "description": "Generates an InheritedWidget class.", 283 | "body": [ 284 | "import 'package:flutter/widgets.dart';\n\n", 285 | "/// {@template ${2:$TM_FILENAME_BASE}}", 286 | "/// ${1:${TM_FILENAME_BASE/(.)(.*)/${1:/upcase}${2:/camelcase}/}} widget.", 287 | "/// {@endtemplate}", 288 | "class ${1} extends InheritedWidget {", 289 | " /// {@macro $2}", 290 | " const ${1}({", 291 | " required super.child,", 292 | " super.key, // ignore: unused_element_parameter", 293 | " });", 294 | " ", 295 | " @override", 296 | " bool updateShouldNotify(covariant ${1} oldWidget) =>", 297 | " false;${0}", 298 | "}\n" 299 | ] 300 | }, 301 | "of": { 302 | "scope": "flutter, dart", 303 | "prefix": [ 304 | "of", 305 | "maybeOf" 306 | ], 307 | "description": "Creates a static `of` method for an InheritedWidget.", 308 | "body": [ 309 | "/// The state from the closest instance of this class", 310 | "/// that encloses the given context, if any.", 311 | "/// e.g. `${1:${TM_FILENAME_BASE/(.)(.*)/${1:/upcase}${2:/camelcase}/}}.maybeOf(context)`.", 312 | "static ${1}? maybeOf(BuildContext context, {bool listen = true}) => listen", 313 | " ? context.dependOnInheritedWidgetOfExactType<${1}>()?.controller", 314 | " : context.getInheritedWidgetOfExactType<${1}>()?.controller", 315 | "", 316 | "static Never _notFoundInheritedWidgetOfExactType() =>", 317 | " throw ArgumentError(", 318 | " 'Out of scope, not found inherited widget '", 319 | " 'a ${1} of the exact type',", 320 | " 'out_of_scope',", 321 | " );", 322 | "", 323 | "/// The state from the closest instance of this class", 324 | "/// that encloses the given context.", 325 | "/// e.g. `${1}.of(context)`", 326 | "static ${1} of(BuildContext context, {bool listen = true}) =>", 327 | " maybeOf(context, listen: listen) ?? _notFoundInheritedWidgetOfExactType();${0}\n" 328 | ] 329 | }, 330 | "stateOf": { 331 | "scope": "flutter, dart", 332 | "prefix": "stateOf", 333 | "description": "Creates a static `stateOf` method for a State object.", 334 | "body": [ 335 | "/// The state from the closest instance of this class", 336 | "/// that encloses the given context, if any.", 337 | "/// e.g. `${1:${TM_FILENAME_BASE/(.)(.*)/${1:/upcase}${2:/camelcase}/}State}.maybeOf(context)`.", 338 | "static ${2:${1}State}? maybeOf(BuildContext context) =>", 339 | " context.findAncestorStateOfType<${2}>();", 340 | "", 341 | "static Never _notFoundStateOfType() =>", 342 | " throw ArgumentError(", 343 | " 'Out of scope, not found state of type ${2}',", 344 | " 'out_of_scope',", 345 | " );", 346 | "", 347 | "/// The state from the closest instance of this class", 348 | "/// that encloses the given context.", 349 | "/// e.g. `${1}.of(context)`", 350 | "static ${2}? of(BuildContext context) =>", 351 | " maybeOf(context) ?? _notFoundStateOfType();${0}\n" 352 | ] 353 | }, 354 | "widgetOf": { 355 | "scope": "flutter, dart", 356 | "prefix": "widgetOf", 357 | "description": "Creates a static `widgetOf` method for a Widget object.", 358 | "body": [ 359 | "/// The state from the closest instance of this class", 360 | "/// that encloses the given context, if any.", 361 | "/// e.g. `${1:${TM_FILENAME_BASE/(.)(.*)/${1:/upcase}${2:/camelcase}/}State}.maybeOf(context)`.", 362 | "static ${1}? maybeOf(BuildContext context) =>", 363 | " context.findAncestorWidgetOfExactType<${1}>();", 364 | "", 365 | "static Never _notFoundStateOfType() =>", 366 | " throw ArgumentError(", 367 | " 'Out of scope, not found widget of type ${1}',", 368 | " 'out_of_scope',", 369 | " );", 370 | "", 371 | "/// The state from the closest instance of this class", 372 | "/// that encloses the given context.", 373 | "/// e.g. `${1}.of(context)`", 374 | "static ${1}? of(BuildContext context) =>", 375 | " maybeOf(context) ?? _notFoundStateOfType();${0}\n" 376 | ] 377 | }, 378 | "Build Method": { 379 | "scope": "flutter, dart", 380 | "prefix": "build", 381 | "description": "Creates a `build` method, describing the part of the UI represented by this widget.", 382 | "body": [ 383 | "@override", 384 | "Widget build(BuildContext context) =>", 385 | " ${0:const Placeholder()};" 386 | ] 387 | }, 388 | "Custom Painter": { 389 | "scope": "flutter, dart", 390 | "prefix": [ 391 | "painter", 392 | "customPainter" 393 | ], 394 | "description": "Creates a CustomPainter class for custom drawing.", 395 | "body": [ 396 | "/// {@template ${2:$TM_FILENAME_BASE}}", 397 | "/// ${1:${TM_FILENAME_BASE/(.)(.*)/${1:/upcase}${2:/camelcase}/}}Painter.", 398 | "/// {@endtemplate}", 399 | "class ${1}Painter extends CustomPainter {", 400 | " /// {@macro ${2}}", 401 | " const ${1}Painter({super.repaint});", 402 | "", 403 | " @override", 404 | " void paint(Canvas canvas, Size size) {", 405 | " ${0}", 406 | " }", 407 | "", 408 | " @override", 409 | " bool shouldRepaint(${1}Painter oldDelegate) => false;", 410 | "", 411 | " @override", 412 | " bool shouldRebuildSemantics(${1}Painter oldDelegate) => false;", 413 | "}\n" 414 | ] 415 | }, 416 | "Custom Clipper": { 417 | "scope": "flutter, dart", 418 | "prefix": [ 419 | "clipper", 420 | "customClipper" 421 | ], 422 | "description": "Creates a CustomClipper class for defining custom clipping shapes.", 423 | "body": [ 424 | "/// {@template ${2:$TM_FILENAME_BASE}}", 425 | "/// ${1:${TM_FILENAME_BASE/(.)(.*)/${1:/upcase}${2:/camelcase}/}}Clipper.", 426 | "/// {@endtemplate}", 427 | "class ${1}Clipper extends CustomClipper {", 428 | " /// {@macro ${2}}", 429 | " const ${1}Clipper({super.reclip});", 430 | "", 431 | " @override", 432 | " Path getClip(Size size) {", 433 | " ${0}", 434 | " }", 435 | "", 436 | " @override", 437 | " bool shouldReclip(${1}Clipper oldClipper) => false;", 438 | "}\n" 439 | ] 440 | }, 441 | "debugFillProperties": { 442 | "scope": "flutter, dart", 443 | "prefix": "debugFillProperties", 444 | "description": "Creates a `debugFillProperties` method for adding debug properties.", 445 | "body": [ 446 | "@override", 447 | "void debugFillProperties(DiagnosticPropertiesBuilder properties) =>", 448 | " super.debugFillProperties(", 449 | " properties", 450 | " ..add(", 451 | " StringProperty(", 452 | " 'description',", 453 | " 'description',", 454 | " ),", 455 | " ),", 456 | " );", 457 | "" 458 | ] 459 | }, 460 | "initState": { 461 | "scope": "flutter, dart", 462 | "prefix": "initS", 463 | "description": "Creates an `initState` method, initializing state when the widget is inserted into the tree.", 464 | "body": [ 465 | "@override", 466 | "void initState() {", 467 | " super.initState();", 468 | " ${0:}", 469 | "}" 470 | ] 471 | }, 472 | "dispose": { 473 | "scope": "flutter, dart", 474 | "prefix": "dispose", 475 | "description": "Creates a `dispose` method, cleaning up resources when the widget is removed from the tree.", 476 | "body": [ 477 | "@override", 478 | "void dispose() {", 479 | " ${0:}", 480 | " super.dispose();", 481 | "}" 482 | ] 483 | }, 484 | "reassemble": { 485 | "scope": "flutter, dart", 486 | "prefix": "reassemble", 487 | "description": "Creates a `reassemble` method, called during hot reload.", 488 | "body": [ 489 | "@override", 490 | "void reassemble(){", 491 | " super.reassemble();", 492 | " ${0:}", 493 | "}" 494 | ] 495 | }, 496 | "didChangeDependencies": { 497 | "scope": "flutter, dart", 498 | "prefix": "didChangeD", 499 | "description": "Creates a `didChangeDependencies` method, called when a dependency of this `State` object changes.", 500 | "body": [ 501 | "@override", 502 | "void didChangeDependencies() {", 503 | " super.didChangeDependencies();", 504 | " ${0:}", 505 | "}" 506 | ] 507 | }, 508 | "didUpdateWidget": { 509 | "scope": "flutter, dart", 510 | "prefix": "didUpdateW", 511 | "description": "Creates a `didUpdateWidget` method, called when the widget’s configuration changes.", 512 | "body": [ 513 | "@override", 514 | "void didUpdateWidget (covariant ${1:Type} ${2:oldWidget}) {", 515 | " super.didUpdateWidget(${2:oldWidget});", 516 | " ${0:}", 517 | "}" 518 | ] 519 | }, 520 | "ListView.builder": { 521 | "scope": "flutter, dart", 522 | "prefix": "listViewBuilder", 523 | "description": "Creates a `ListView.builder`, a scrollable list that lazily builds its children as they scroll into view.", 524 | "body": [ 525 | "ListView.builder(", 526 | " itemCount: ${1:1},", 527 | " itemBuilder: (context, index) =>", 528 | " ${2:const Placeholder()},${0}", 529 | ")," 530 | ] 531 | }, 532 | "ListView.separated": { 533 | "scope": "flutter, dart", 534 | "prefix": "listViewSeparated", 535 | "description": "Creates a `ListView.separated`, a scrollable list with separators between items.", 536 | "body": [ 537 | "ListView.separated(", 538 | " itemCount: ${1:1},", 539 | " separatorBuilder: (context, index) {", 540 | " return ${2:};", 541 | " },", 542 | " itemBuilder: (context, index) {", 543 | " return ${0:};", 544 | " },", 545 | ")," 546 | ] 547 | }, 548 | "GridView.builder": { 549 | "scope": "flutter, dart", 550 | "prefix": "gridViewBuilder", 551 | "description": "Creates a `GridView.builder`, a scrollable grid of widgets that are created on demand.", 552 | "body": [ 553 | "GridView.builder(", 554 | " gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(", 555 | " crossAxisCount: ${1:2},", 556 | " ),", 557 | " itemCount: ${2:2},", 558 | " itemBuilder: (BuildContext context, int index) {", 559 | " return ${3:};", 560 | " },", 561 | ")," 562 | ] 563 | }, 564 | "GridView.count": { 565 | "scope": "flutter, dart", 566 | "prefix": "gridViewCount", 567 | "description": "Creates a `GridView.count`, a scrollable grid of widgets with a fixed number of tiles in the cross axis.", 568 | "body": [ 569 | "GridView.count(", 570 | " crossAxisSpacing: ${1:1},", 571 | " mainAxisSpacing: ${2:2},", 572 | " crossAxisCount: ${3:2},", 573 | " children: [", 574 | " ${0:}", 575 | " ],", 576 | ")," 577 | ] 578 | }, 579 | "GridView.extent": { 580 | "scope": "flutter, dart", 581 | "prefix": "gridViewE", 582 | "description": "Creates a `GridView.extent`, a scrollable grid of widgets with tiles that each have a maximum cross-axis extent.", 583 | "body": [ 584 | "GridView.extent(", 585 | " maxCrossAxisExtent: ${1:2},", 586 | " children: [", 587 | " ${0:}", 588 | " ],", 589 | ")," 590 | ] 591 | }, 592 | "Custom Scroll View": { 593 | "scope": "flutter, dart", 594 | "prefix": "customScrollV", 595 | "description": "Creates a `CustomScrollView`, a scrollable area that creates custom scroll effects using slivers.", 596 | "body": [ 597 | "CustomScrollView(", 598 | " slivers: [", 599 | " ${0}", 600 | " ],", 601 | ")," 602 | ] 603 | }, 604 | "Builder": { 605 | "scope": "flutter, dart", 606 | "prefix": "builder", 607 | "description": "Creates a `Builder` widget, delegating the widget building process to a callback.", 608 | "body": [ 609 | "Builder(", 610 | " builder: (context) =>", 611 | " ${0:const Placeholder()},", 612 | ")," 613 | ] 614 | }, 615 | "Wrap with Builder": { 616 | "scope": "flutter, dart", 617 | "prefix": [ 618 | "wrapWithBuilder" 619 | ], 620 | "description": "Wraps a widget with a `Builder` widget.", 621 | "body": [ 622 | "Builder(", 623 | " builder: (context) =>", 624 | " ${TM_SELECTED_TEXT},", 625 | ")" 626 | ] 627 | }, 628 | "Stateful Builder": { 629 | "scope": "flutter, dart", 630 | "prefix": [ 631 | "stfBuilder", 632 | "statefulBuilder" 633 | ], 634 | "description": "Creates a `StatefulBuilder` widget, allowing for state management and rebuilding a specific portion of the widget tree.", 635 | "body": [ 636 | "StatefulBuilder(", 637 | " builder: (context, setState) =>", 638 | " ${0:const Placeholder()},", 639 | ")," 640 | ] 641 | }, 642 | "Stream Builder": { 643 | "scope": "flutter, dart", 644 | "prefix": [ 645 | "strBuilder", 646 | "streamBuilder" 647 | ], 648 | "description": "Creates a `StreamBuilder` widget, rebuilding itself based on the latest snapshot of interaction with a `Stream`.", 649 | "body": [ 650 | "StreamBuilder<${1:int}>(", 651 | " initialData: ${2:initialData},", 652 | " stream: ${3:stream},", 653 | " builder: (context, snapshot) =>", 654 | " ${0:const Placeholder()},", 655 | ")," 656 | ] 657 | }, 658 | "Animated Builder": { 659 | "scope": "flutter, dart", 660 | "prefix": [ 661 | "animBuilder", 662 | "animatedBuilder" 663 | ], 664 | "description": "Creates an `AnimatedBuilder` widget, rebuilding itself based on an animation.", 665 | "body": [ 666 | "AnimatedBuilder(", 667 | " animation: ${1:animation},", 668 | " builder: (context, child) =>", 669 | " ${0:const Placeholder()},", 670 | " child: ${2:child},", 671 | ")," 672 | ] 673 | }, 674 | "Listenable Builder": { 675 | "scope": "flutter, dart", 676 | "prefix": [ 677 | "lisBuilder", 678 | "listenableBuilder" 679 | ], 680 | "description": "Creates a `ListenableBuilder` widget, rebuilding itself based on the latest value of a `Listenable` it listens to.", 681 | "body": [ 682 | "ListenableBuilder(", 683 | " listenable: ${1: null},", 684 | " builder: (context, child) =>", 685 | " ${0:const Placeholder()},", 686 | " child: ${2:child},", 687 | ")," 688 | ] 689 | }, 690 | "Value Listenable Builder": { 691 | "scope": "flutter, dart", 692 | "prefix": [ 693 | "valLisBuilder", 694 | "valueListenableBuilder" 695 | ], 696 | "description": "Creates a `ValueListenableBuilder` widget, rebuilding itself based on the latest value of a `ValueListenable`.", 697 | "body": [ 698 | "ValueListenableBuilder<${1:int}>(", 699 | " valueListenable: ${2: null},", 700 | " builder: (context, value, child) =>", 701 | " ${0:const Placeholder()},", 702 | " child: ${2:child},", 703 | ")," 704 | ] 705 | }, 706 | "Animated Switcher": { 707 | "scope": "flutter, dart", 708 | "prefix": [ 709 | "switcher", 710 | "animatedSwitcher" 711 | ], 712 | "description": "Creates an `AnimatedSwitcher` widget, switching between two children and animating the transition.", 713 | "body": [ 714 | "AnimatedSwitcher(", 715 | " duration: const Duration(milliseconds: 350),", 716 | " child: cond ? ${1:child} : const SizedBox.shrink(),", 717 | ")," 718 | ] 719 | }, 720 | "Orientation Builder": { 721 | "scope": "flutter, dart", 722 | "prefix": [ 723 | "orientBuilder", 724 | "orientationBuilder" 725 | ], 726 | "description": "Creates an `OrientationBuilder` widget, rebuilding itself based on the latest orientation of the device (portrait or landscape).", 727 | "body": [ 728 | "OrientationBuilder(", 729 | " builder: (context, orientation) {", 730 | " return ${0:const Placeholder()};", 731 | " },", 732 | ")," 733 | ] 734 | }, 735 | "Layout Builder": { 736 | "scope": "flutter, dart", 737 | "prefix": [ 738 | "layBuilder", 739 | "layoutBuilder" 740 | ], 741 | "description": "Creates a `LayoutBuilder` widget, rebuilding itself based on the latest layout constraints, useful for responsive layouts.", 742 | "body": [ 743 | "LayoutBuilder(", 744 | " builder: (context, constraints) {", 745 | " return ${0:const Placeholder()};", 746 | " },", 747 | ")," 748 | ] 749 | }, 750 | "Repaint boundary": { 751 | "scope": "flutter, dart", 752 | "prefix": [ 753 | "repBound", 754 | "repaintBoundary" 755 | ], 756 | "description": "Creates a `RepaintBoundary` widget, isolating repaints to improve performance.", 757 | "body": [ 758 | "RepaintBoundary(", 759 | " key: ValueKey(${1:name}),", 760 | " child: ${0:const Placeholder()},", 761 | ")," 762 | ] 763 | }, 764 | "Single Child ScrollView": { 765 | "scope": "flutter, dart", 766 | "prefix": "singleChildSV", 767 | "description": "Creates a `SingleChildScrollView` widget, allowing a single child to be scrolled.", 768 | "body": [ 769 | "SingleChildScrollView(", 770 | " controller: ${1:controller,}", 771 | " child: Column(", 772 | " ${0:}", 773 | " ),", 774 | ")," 775 | ] 776 | }, 777 | "Future Builder": { 778 | "scope": "flutter, dart", 779 | "prefix": [ 780 | "futBuilder", 781 | "futureBuilder" 782 | ], 783 | "description": "Creates a `FutureBuilder` widget, rebuilding itself based on the latest snapshot of interaction with a `Future`.", 784 | "body": [ 785 | "FutureBuilder(", 786 | " future: ${1:Future},", 787 | " initialData: ${2:InitialData},", 788 | " builder: (context, snapshot) {", 789 | " return ${3:};", 790 | " },", 791 | ")," 792 | ] 793 | }, 794 | "Tween Animation Builder": { 795 | "scope": "flutter, dart", 796 | "prefix": [ 797 | "tweenAnimBuilder", 798 | "tweenAnimationBuilder" 799 | ], 800 | "description": "Creates a `TweenAnimationBuilder` widget, animating a property of a widget to a target value whenever the target value changes.", 801 | "body": [ 802 | "TweenAnimationBuilder<${3:Object?}>(", 803 | " duration: ${1:const Duration(),}", 804 | " tween: ${2:Tween(),}", 805 | " builder: (context, value, child) =>", 806 | " ${4:const Placeholder()},", 807 | ")," 808 | ] 809 | }, 810 | "Test Widgets": { 811 | "scope": "flutter, dart", 812 | "prefix": [ 813 | "testWidget", 814 | "widgetTest" 815 | ], 816 | "description": "Creates a `testWidgets` function for testing a widget.", 817 | "body": [ 818 | "testWidgets(", 819 | " \"${1:test description}\",", 820 | " (tester) async {", 821 | " ${0}", 822 | " },", 823 | ");" 824 | ] 825 | }, 826 | "Row": { 827 | "scope": "flutter, dart", 828 | "prefix": "row", 829 | "description": "Creates a `Row` widget, which displays its children in a horizontal array.", 830 | "body": [ 831 | "Row(", 832 | " mainAxisSize: MainAxisSize.${1|min,max|},", 833 | " mainAxisAlignment: MainAxisAlignment.${2|start,end,center,spaceBetween,spaceAround,spaceEvenly|},", 834 | " crossAxisAlignment: CrossAxisAlignment.${3|start,end,center,stretch,baseline|},", 835 | " children: [", 836 | " // SizedBox, Expanded, Flexible, Spacer, VerticalDivider", 837 | " ${0:Placeholder()},", 838 | " ],", 839 | ")," 840 | ] 841 | }, 842 | "Column": { 843 | "scope": "flutter, dart", 844 | "prefix": [ 845 | "col", 846 | "column" 847 | ], 848 | "description": "Creates a `Column` widget, which displays its children in a vertical array.", 849 | "body": [ 850 | "Column(", 851 | " mainAxisSize: MainAxisSize.${1|min,max|},", 852 | " mainAxisAlignment: MainAxisAlignment.${2|start,end,center,spaceBetween,spaceAround,spaceEvenly|},", 853 | " crossAxisAlignment: CrossAxisAlignment.${3|start,end,center,stretch,baseline|},", 854 | " children: [", 855 | " // SizedBox, Expanded, Flexible, Spacer, Divider", 856 | " ${0:Placeholder()},", 857 | " ],", 858 | ")," 859 | ] 860 | }, 861 | "Wrap": { 862 | "scope": "flutter, dart", 863 | "prefix": "wrap", 864 | "description": "Creates a `Wrap` widget, which displays its children in multiple horizontal or vertical runs.", 865 | "body": [ 866 | "Wrap(", 867 | " direction: Axis.${1|horizontal,vertical|},", 868 | " spacing: ${2:8.0},", 869 | " runSpacing: ${3:4.0},", 870 | " alignment: WrapAlignment.${4|start,end,center,spaceBetween,spaceAround,spaceEvenly|},", 871 | " crossAxisAlignment: WrapCrossAlignment.${5|start,end,center|},", 872 | " verticalDirection: VerticalDirection.${6|down,up|},", 873 | " children: [", 874 | " ${0:Placeholder()},", 875 | " ],", 876 | ")," 877 | ] 878 | }, 879 | "Stack": { 880 | "scope": "flutter, dart", 881 | "prefix": "stack", 882 | "description": "Creates a `Stack` widget, which allows you to place widgets on top of each other in a z-order.", 883 | "body": [ 884 | "Stack(", 885 | " alignment: Alignment.${1|topLeft,topCenter,center|},", 886 | " children: [", 887 | " ${0:Placeholder()},", 888 | " ],", 889 | ")," 890 | ] 891 | }, 892 | "FittedBox": { 893 | "scope": "flutter, dart", 894 | "prefix": "fittedbox", 895 | "description": "Creates a `FittedBox` widget, which scales and positions its child within itself according to the specified fit.", 896 | "body": [ 897 | "FittedBox(", 898 | " fit: BoxFit.${1|scaleDown,fill,contain,cover,fitWidth,fitHeight,none|},", 899 | " alignment: Alignment.${2:center},", 900 | " child: ${0:Placeholder()},", 901 | ")," 902 | ] 903 | } 904 | } --------------------------------------------------------------------------------