Initial commit: Tkinter Designer extension
This commit is contained in:
parent
d3e8798c2a
commit
b79c15c8ae
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
out
|
||||
dist
|
||||
node_modules
|
||||
.vscode-test/
|
||||
*.vsix
|
||||
5
.vscode-test.mjs
Normal file
5
.vscode-test.mjs
Normal file
@ -0,0 +1,5 @@
|
||||
import { defineConfig } from '@vscode/test-cli';
|
||||
|
||||
export default defineConfig({
|
||||
files: 'out/test/**/*.test.js',
|
||||
});
|
||||
8
.vscode/extensions.json
vendored
Normal file
8
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"ms-vscode.extension-test-runner"
|
||||
]
|
||||
}
|
||||
21
.vscode/launch.json
vendored
Normal file
21
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
// A launch configuration that compiles the extension and then opens it inside a new window
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Run Extension",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/out/**/*.js"
|
||||
],
|
||||
"preLaunchTask": "${defaultBuildTask}"
|
||||
}
|
||||
]
|
||||
}
|
||||
11
.vscode/settings.json
vendored
Normal file
11
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
// Place your settings in this file to overwrite default and user settings.
|
||||
{
|
||||
"files.exclude": {
|
||||
"out": false // set this to true to hide the "out" folder with the compiled JS files
|
||||
},
|
||||
"search.exclude": {
|
||||
"out": true // set this to false to include "out" folder in search results
|
||||
},
|
||||
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
|
||||
"typescript.tsc.autoDetect": "off"
|
||||
}
|
||||
20
.vscode/tasks.json
vendored
Normal file
20
.vscode/tasks.json
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "watch",
|
||||
"problemMatcher": "$tsc-watch",
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"reveal": "never"
|
||||
},
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
11
.vscodeignore
Normal file
11
.vscodeignore
Normal file
@ -0,0 +1,11 @@
|
||||
.vscode/**
|
||||
.vscode-test/**
|
||||
src/**
|
||||
.gitignore
|
||||
.yarnrc
|
||||
vsc-extension-quickstart.md
|
||||
**/tsconfig.json
|
||||
**/eslint.config.mjs
|
||||
**/*.map
|
||||
**/*.ts
|
||||
**/.vscode-test.*
|
||||
9
CHANGELOG.md
Normal file
9
CHANGELOG.md
Normal file
@ -0,0 +1,9 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to the "tkinter-designer" extension will be documented in this file.
|
||||
|
||||
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- Initial release
|
||||
3
README.md
Normal file
3
README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# tkinter-designer README
|
||||
Тут можно создавать формы на tkinter :)
|
||||
|
||||
27
eslint.config.mjs
Normal file
27
eslint.config.mjs
Normal file
@ -0,0 +1,27 @@
|
||||
import typescriptEslint from "typescript-eslint";
|
||||
|
||||
export default [{
|
||||
files: ["**/*.ts"],
|
||||
}, {
|
||||
plugins: {
|
||||
"@typescript-eslint": typescriptEslint.plugin,
|
||||
},
|
||||
|
||||
languageOptions: {
|
||||
parser: typescriptEslint.parser,
|
||||
ecmaVersion: 2022,
|
||||
sourceType: "module",
|
||||
},
|
||||
|
||||
rules: {
|
||||
"@typescript-eslint/naming-convention": ["warn", {
|
||||
selector: "import",
|
||||
format: ["camelCase", "PascalCase"],
|
||||
}],
|
||||
|
||||
curly: "warn",
|
||||
eqeqeq: "warn",
|
||||
"no-throw-literal": "warn",
|
||||
semi: "warn",
|
||||
},
|
||||
}];
|
||||
BIN
media/fonts/icons.woff
Normal file
BIN
media/fonts/icons.woff
Normal file
Binary file not shown.
1
media/grapes.min.css
vendored
Normal file
1
media/grapes.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
3
media/grapes.min.js
vendored
Normal file
3
media/grapes.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
378
media/main.js
Normal file
378
media/main.js
Normal file
@ -0,0 +1,378 @@
|
||||
const vscode = acquireVsCodeApi();
|
||||
|
||||
// ============================================
|
||||
// 1. Инициализация
|
||||
// ============================================
|
||||
const editor = grapesjs.init({
|
||||
container: '#editor',
|
||||
height: '100%',
|
||||
storageManager: false,
|
||||
dragMode: 'absolute',
|
||||
selectorManager: { componentFirst: true },
|
||||
undoManager: { trackSelection: false },
|
||||
styleManager: { sectors: [{ name: 'Geometry', buildProps: ['left', 'top', 'width', 'height'] }] },
|
||||
|
||||
canvas: {
|
||||
styles: [
|
||||
'body { background-color: #444444; margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; }',
|
||||
'::-webkit-scrollbar { width: 10px; height: 10px; }',
|
||||
'::-webkit-scrollbar-track { background: #333; }',
|
||||
'::-webkit-scrollbar-thumb { background: #666; border-radius: 5px; }'
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// 2. Панель и Кнопки
|
||||
// ============================================
|
||||
editor.Panels.getButton('views', 'open-blocks').set('active', true);
|
||||
|
||||
|
||||
editor.Panels.addButton('options', {
|
||||
id: 'import-python-btn', className: 'fa fa-upload', command: 'import-python-cmd',
|
||||
attributes: { title: 'Import from Python File' }
|
||||
});
|
||||
editor.Commands.add('import-python-cmd', { run: () => { vscode.postMessage({ type: 'request-import' }); } });
|
||||
|
||||
|
||||
editor.Panels.addButton('options', {
|
||||
id: 'window-settings-btn', className: 'fa fa-cog', command: 'window-settings-cmd',
|
||||
attributes: { title: 'Window Settings' }
|
||||
});
|
||||
|
||||
editor.Commands.add('window-settings-cmd', {
|
||||
run: (editor, sender) => {
|
||||
sender.set('active', 0);
|
||||
const wrapper = editor.getWrapper();
|
||||
const attrs = wrapper.getAttributes();
|
||||
|
||||
const currW = attrs['data-width'] || 500;
|
||||
const currH = attrs['data-height'] || 400;
|
||||
const currTitle = attrs['data-title'] || 'Form';
|
||||
|
||||
const content = `
|
||||
<div style="padding: 10px; font-family: sans-serif; color: #333;">
|
||||
<div style="margin-bottom: 10px;">
|
||||
<label>Window Title:</label>
|
||||
<input type="text" id="win-title" value="${currTitle}" style="width: 100%; padding: 5px;">
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px; margin-bottom: 20px;">
|
||||
<div style="flex: 1;"><label>Width:</label><input type="number" id="win-width" value="${currW}" style="width: 100%; padding: 5px;"></div>
|
||||
<div style="flex: 1;"><label>Height:</label><input type="number" id="win-height" value="${currH}" style="width: 100%; padding: 5px;"></div>
|
||||
</div>
|
||||
<button id="win-save" style="background: #444; color: #fff; border: none; padding: 8px 15px; cursor: pointer;">Apply</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const modal = editor.Modal;
|
||||
modal.setTitle('Window Settings');
|
||||
modal.setContent(content);
|
||||
modal.open();
|
||||
|
||||
document.getElementById('win-save').onclick = () => {
|
||||
const newTitle = document.getElementById('win-title').value;
|
||||
const newW = parseInt(document.getElementById('win-width').value) || 500;
|
||||
const newH = parseInt(document.getElementById('win-height').value) || 400;
|
||||
|
||||
// Применяем размеры
|
||||
wrapper.addStyle({ width: newW + 'px', height: newH + 'px' });
|
||||
|
||||
const newAttrs = { ...wrapper.getAttributes() };
|
||||
newAttrs['data-title'] = newTitle;
|
||||
newAttrs['data-width'] = newW;
|
||||
newAttrs['data-height'] = newH;
|
||||
wrapper.setAttributes(newAttrs);
|
||||
|
||||
modal.close();
|
||||
sendDataToExtension();
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// 3. Загрузка (Настройка Wrapper)
|
||||
// ============================================
|
||||
editor.on('load', () => {
|
||||
editor.Keymaps.add('redo-custom', 'ctrl+y', 'core:redo');
|
||||
editor.Keymaps.add('undo-custom', 'ctrl+z', 'core:undo');
|
||||
|
||||
const wrapper = editor.getWrapper();
|
||||
|
||||
|
||||
wrapper.addStyle({
|
||||
'position': 'relative',
|
||||
'background-color': '#ffffff',
|
||||
'overflow': 'hidden',
|
||||
'border': '2px solid #333',
|
||||
'box-shadow': '0 0 30px rgba(0,0,0,0.5)',
|
||||
'width': '500px',
|
||||
'height': '400px',
|
||||
'margin': '0'
|
||||
});
|
||||
|
||||
const attrs = wrapper.getAttributes();
|
||||
if (!attrs['data-width']) {
|
||||
wrapper.setAttributes({ 'data-width': 500, 'data-height': 400, 'data-title': 'My App' });
|
||||
} else {
|
||||
|
||||
wrapper.addStyle({
|
||||
width: attrs['data-width'] + 'px',
|
||||
height: attrs['data-height'] + 'px'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const geoTraits = [
|
||||
{ type: 'number', name: 'x', label: 'X' }, { type: 'number', name: 'y', label: 'Y' },
|
||||
{ type: 'number', name: 'width', label: 'W' }, { type: 'number', name: 'height', label: 'H' },
|
||||
{ type: 'color', name: 'bg', label: 'Bg' }, { type: 'color', name: 'fg', label: 'Fg' }
|
||||
];
|
||||
|
||||
// ============================================
|
||||
// 4. Компоненты
|
||||
// ============================================
|
||||
|
||||
function addTkComponent(type, tkType, label, defaultContent = '', defW = 80, defH = 30) {
|
||||
editor.DomComponents.addType(type, {
|
||||
model: {
|
||||
defaults: {
|
||||
tagName: 'div', draggable: true, droppable: false,
|
||||
attributes: { 'data-tk-type': tkType },
|
||||
traits: [ { type: 'text', name: 'text', label: 'Text', default: defaultContent }, ...geoTraits ],
|
||||
style: {
|
||||
position: 'absolute', left: '0px', top: '0px', width: defW + 'px', height: defH + 'px',
|
||||
padding: '5px', border: '1px solid #999', background: '#f0f0f0',
|
||||
display: 'flex', 'align-items': 'center', 'justify-content': 'center', 'overflow': 'hidden',
|
||||
...(tkType === 'Entry' || tkType === 'Text' ? { background: '#ffffff', border: '2px inset #ccc', 'align-items': 'start', 'justify-content': 'start', 'white-space': 'pre-wrap' } : {})
|
||||
},
|
||||
content: defaultContent || label
|
||||
},
|
||||
updated(p) { if(p==='traits') { const text = this.getTrait('text')?.getValue(); if(text!==undefined && tkType!=='Entry') this.components(text); updateStylesAndSync(this); sendDataToExtension(); } }
|
||||
}
|
||||
});
|
||||
editor.BlockManager.add(type, { label: label, category: 'Tkinter', content: { type: type } });
|
||||
}
|
||||
|
||||
addTkComponent('tk-label', 'Label', 'Label', 'Label', 100, 30);
|
||||
addTkComponent('tk-button', 'Button', 'Button', 'Button', 100, 40);
|
||||
addTkComponent('tk-entry', 'Entry', 'Entry', 'Input here', 120, 30);
|
||||
addTkComponent('tk-check', 'Checkbutton', 'Checkbox', 'Check', 100, 30);
|
||||
addTkComponent('tk-radio', 'Radiobutton', 'Radio', 'Radio', 100, 30);
|
||||
addTkComponent('tk-text', 'Text', 'Text', 'Multiline\nText', 150, 100);
|
||||
|
||||
editor.DomComponents.addType('tk-listbox', {
|
||||
model: {
|
||||
defaults: {
|
||||
tagName: 'div', draggable: true, droppable: false,
|
||||
attributes: { 'data-tk-type': 'Listbox', 'line_count': 3 },
|
||||
style: { position: 'absolute', left: '0', top: '0', width: '120px', height: '100px', background: '#fff', border: '2px inset #ccc', overflow: 'hidden', padding: '0' },
|
||||
traits: [{ type: 'number', name: 'line_count', label: 'Rows Count', default: 3, changeProp: 1 }, ...geoTraits]
|
||||
},
|
||||
init() { this.on('change:line_count', this.updateTraits); this.updateTraits(); this.renderList(); },
|
||||
updateTraits() {
|
||||
const count = parseInt(this.get('line_count') || 0);
|
||||
const newTraits = [{ type: 'number', name: 'line_count', label: 'Rows Count', changeProp: 1 }];
|
||||
for (let i = 0; i < count; i++) newTraits.push({ type: 'text', name: `line_${i}`, label: `Line ${i + 1}`, changeProp: 1 });
|
||||
newTraits.push(...geoTraits);
|
||||
this.set('traits', newTraits);
|
||||
},
|
||||
renderList() {
|
||||
const count = parseInt(this.get('line_count') || 0);
|
||||
let htmlItems = '', dataItems = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
let val = this.get(`line_${i}`);
|
||||
if (val === undefined) { val = `Item ${i + 1}`; this.set(`line_${i}`, val); }
|
||||
htmlItems += `<div style="border-bottom:1px solid #eee; padding:2px;">${val}</div>`;
|
||||
dataItems.push(val);
|
||||
}
|
||||
this.components(htmlItems);
|
||||
const attrs = { ...this.getAttributes() };
|
||||
attrs['data-items'] = dataItems.join('\n');
|
||||
this.setAttributes(attrs);
|
||||
},
|
||||
updated(p) {
|
||||
if (p==='traits' || p.startsWith('line_') || p==='line_count') {
|
||||
this.renderList();
|
||||
updateStylesAndSync(this);
|
||||
sendDataToExtension();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
editor.BlockManager.add('tk-listbox', { label: 'Listbox', category: 'Tkinter', content: { type: 'tk-listbox' } });
|
||||
|
||||
editor.DomComponents.addType('tk-canvas', { extend: 'tk-label', model: { defaults: { attributes: { 'data-tk-type': 'Canvas' }, traits: geoTraits, style: { position: 'absolute', left:'0', top:'0', border: '1px solid black', width: '150px', height: '150px', background: '#fff' } }, updated(p){ if(p==='traits') { updateStylesAndSync(this); sendDataToExtension(); } } } });
|
||||
editor.BlockManager.add('tk-canvas', { label: 'Canvas', category: 'Tkinter', content: { type: 'tk-canvas' } });
|
||||
|
||||
editor.DomComponents.addType('tk-frame', {
|
||||
model: {
|
||||
defaults: {
|
||||
tagName: 'div', draggable: true, droppable: true,
|
||||
attributes: { 'data-tk-type': 'Frame' },
|
||||
traits: geoTraits,
|
||||
style: { position: 'absolute', left:'0', top:'0', width: '200px', height: '200px', border: '2px dashed #666', background: 'rgba(0,0,0,0.05)' }
|
||||
},
|
||||
updated(p) { if(p==='traits') { updateStylesAndSync(this); sendDataToExtension(); } }
|
||||
}
|
||||
});
|
||||
editor.BlockManager.add('tk-frame', { label: 'Frame', category: 'Tkinter', content: { type: 'tk-frame' } });
|
||||
|
||||
// ============================================
|
||||
// 5. Вложенность и Синхронизация
|
||||
// ============================================
|
||||
editor.on('component:drag:end', (model) => {
|
||||
const target = model.target;
|
||||
if (!target) return;
|
||||
if (target.getAttributes()['data-tk-type'] === 'Frame') { syncCoordinates(target); sendDataToExtension(); return; }
|
||||
|
||||
const targetEl = target.getEl();
|
||||
if (!targetEl) return;
|
||||
|
||||
const currentStyle = target.getStyle();
|
||||
let safeWidth = currentStyle.width;
|
||||
let safeHeight = currentStyle.height;
|
||||
if (!safeWidth || safeWidth === 'auto') safeWidth = targetEl.offsetWidth + 'px';
|
||||
if (!safeHeight || safeHeight === 'auto') safeHeight = targetEl.offsetHeight + 'px';
|
||||
|
||||
const targetRect = targetEl.getBoundingClientRect();
|
||||
const zoom = editor.Canvas.getZoom() / 100 * 100;
|
||||
|
||||
const wrapper = editor.getWrapper();
|
||||
const frames = wrapper.find('[data-tk-type="Frame"]');
|
||||
|
||||
// Центр
|
||||
const tCx = targetRect.left + targetRect.width / 2;
|
||||
const tCy = targetRect.top + targetRect.height / 2;
|
||||
|
||||
let bestParent = wrapper;
|
||||
let parentRect = wrapper.getEl().getBoundingClientRect();
|
||||
|
||||
// Если кинули в фрейм
|
||||
for (let frame of frames) {
|
||||
if (frame === target) continue;
|
||||
const fRect = frame.getEl().getBoundingClientRect();
|
||||
if (tCx > fRect.left && tCx < fRect.right && tCy > fRect.top && tCy < fRect.bottom) {
|
||||
bestParent = frame;
|
||||
parentRect = fRect;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const currentParent = target.parent();
|
||||
if (bestParent === currentParent) {
|
||||
syncCoordinates(target);
|
||||
sendDataToExtension();
|
||||
return;
|
||||
}
|
||||
|
||||
// Расчет новых координат
|
||||
let borderOffset = 0;
|
||||
let newX = (targetRect.left - parentRect.left - borderOffset) / zoom;
|
||||
let newY = (targetRect.top - parentRect.top - borderOffset) / zoom;
|
||||
newX = Math.round(newX); newY = Math.round(newY);
|
||||
|
||||
bestParent.append(target);
|
||||
target.addStyle({ position: 'absolute', left: newX + 'px', top: newY + 'px', width: safeWidth, height: safeHeight, margin: '0' });
|
||||
|
||||
syncCoordinates(target);
|
||||
sendDataToExtension();
|
||||
});
|
||||
|
||||
function updateStylesAndSync(component) {
|
||||
if (!component.getTrait) return;
|
||||
const x = component.getTrait('x')?.getValue();
|
||||
const y = component.getTrait('y')?.getValue();
|
||||
const w = component.getTrait('width')?.getValue();
|
||||
const h = component.getTrait('height')?.getValue();
|
||||
const bg = component.getTrait('bg')?.getValue();
|
||||
const fg = component.getTrait('fg')?.getValue();
|
||||
|
||||
if (x !== undefined && x !== "") component.addStyle({ left: x + 'px' });
|
||||
if (y !== undefined && y !== "") component.addStyle({ top: y + 'px' });
|
||||
if (w !== undefined && w !== "") component.addStyle({ width: w + 'px' });
|
||||
if (h !== undefined && h !== "") component.addStyle({ height: h + 'px' });
|
||||
if (bg) component.addStyle({ background: bg });
|
||||
if (fg) component.addStyle({ color: fg });
|
||||
|
||||
syncCoordinates(component);
|
||||
}
|
||||
|
||||
function syncCoordinates(component) {
|
||||
if (!component || !component.getStyle) return;
|
||||
const style = component.getStyle();
|
||||
const attrs = { ...component.getAttributes() };
|
||||
attrs['data-x'] = parseInt(style.left || 0);
|
||||
attrs['data-y'] = parseInt(style.top || 0);
|
||||
attrs['data-width'] = parseInt(style.width);
|
||||
attrs['data-height'] = parseInt(style.height);
|
||||
const bg = component.getTrait('bg')?.getValue();
|
||||
const fg = component.getTrait('fg')?.getValue();
|
||||
if(bg) attrs['data-bg'] = bg;
|
||||
if(fg) attrs['data-fg'] = fg;
|
||||
if (attrs['data-tk-type'] !== 'Listbox') {
|
||||
const text = component.getTrait('text')?.getValue();
|
||||
if (text) attrs['data-text'] = text;
|
||||
}
|
||||
component.setAttributes(attrs);
|
||||
}
|
||||
|
||||
function syncAllComponents() {
|
||||
const wrapper = editor.getWrapper();
|
||||
const syncRecursive = (comp) => {
|
||||
if(comp.getAttributes()['data-tk-type']) syncCoordinates(comp);
|
||||
comp.components().each(child => syncRecursive(child));
|
||||
}
|
||||
syncRecursive(wrapper);
|
||||
}
|
||||
|
||||
let isReadyToSave = false;
|
||||
function sendDataToExtension() {
|
||||
if (!isReadyToSave) return;
|
||||
vscode.postMessage({ type: 'update-code', payload: editor.getProjectData() });
|
||||
}
|
||||
|
||||
editor.on('component:add', (c) => { setTimeout(() => { syncCoordinates(c); sendDataToExtension(); }, 50); });
|
||||
editor.on('component:remove', () => { sendDataToExtension(); });
|
||||
|
||||
function handleUndoRedo() {
|
||||
editor.select(null);
|
||||
setTimeout(() => {
|
||||
const currentData = editor.getProjectData();
|
||||
editor.loadProjectData(currentData);
|
||||
syncAllComponents();
|
||||
sendDataToExtension();
|
||||
}, 150);
|
||||
}
|
||||
|
||||
editor.on('run:undo', handleUndoRedo);
|
||||
editor.on('run:redo', handleUndoRedo);
|
||||
|
||||
window.addEventListener('message', e => {
|
||||
const d = e.data;
|
||||
if (d.type === 'load-data' || d.type === 'import-data') {
|
||||
try {
|
||||
if(d.payload && Object.keys(d.payload).length > 0) {
|
||||
if (d.type === 'import-data') editor.DomComponents.clear();
|
||||
editor.loadProjectData(d.payload);
|
||||
editor.UndoManager.clear();
|
||||
|
||||
// ВОССТАНОВЛЕНИЕ РАЗМЕРОВ ОКНА
|
||||
const wrapper = editor.getWrapper();
|
||||
const wAttrs = wrapper.getAttributes();
|
||||
if (wAttrs['data-width']) {
|
||||
wrapper.addStyle({
|
||||
width: wAttrs['data-width'] + 'px',
|
||||
height: wAttrs['data-height'] + 'px'
|
||||
});
|
||||
}
|
||||
|
||||
if (d.type === 'import-data') {
|
||||
setTimeout(() => { syncAllComponents(); sendDataToExtension(); }, 200);
|
||||
}
|
||||
}
|
||||
} catch(err){ console.error(err); }
|
||||
setTimeout(() => isReadyToSave = true, 500);
|
||||
}
|
||||
});
|
||||
|
||||
setTimeout(() => { vscode.postMessage({ type: 'request-load' }); }, 100);
|
||||
8
media/styles.css
Normal file
8
media/styles.css
Normal file
@ -0,0 +1,8 @@
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
#editor {
|
||||
height: 100%;
|
||||
}
|
||||
3269
package-lock.json
generated
Normal file
3269
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
69
package.json
Normal file
69
package.json
Normal file
@ -0,0 +1,69 @@
|
||||
{
|
||||
"name": "tkinter-designer",
|
||||
"displayName": "tkinter-designer",
|
||||
"description": "VS code disigner",
|
||||
"version": "0.0.1",
|
||||
"publisher": "gr1hon",
|
||||
"engines": {
|
||||
"vscode": "^1.107.0"
|
||||
},
|
||||
"categories": [
|
||||
"Other"
|
||||
],
|
||||
"activationEvents": [
|
||||
"onCommand:tkinter-designer.start"
|
||||
],
|
||||
"main": "./out/extension.js",
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "tkinter-designer.start",
|
||||
"title": "Start Tkinter Designer"
|
||||
}
|
||||
],
|
||||
"languages": [
|
||||
{
|
||||
"id": "tkinter-form",
|
||||
"extensions": [
|
||||
".tkjson"
|
||||
],
|
||||
"aliases": [
|
||||
"Tkinter Form Design"
|
||||
]
|
||||
}
|
||||
],
|
||||
"customEditors": [
|
||||
{
|
||||
"viewType": "tkinter-designer.editor",
|
||||
"displayName": "Tkinter Visual Editor",
|
||||
"selector": [
|
||||
{
|
||||
"filenamePattern": "*.tkjson"
|
||||
}
|
||||
],
|
||||
"priority": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"vscode:prepublish": "npm run compile",
|
||||
"compile": "tsc -p ./",
|
||||
"watch": "tsc -watch -p ./",
|
||||
"pretest": "npm run compile && npm run lint",
|
||||
"lint": "eslint src",
|
||||
"test": "vscode-test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mocha": "^10.0.10",
|
||||
"@types/node": "22.x",
|
||||
"@types/vscode": "^1.107.0",
|
||||
"@vscode/test-cli": "^0.0.12",
|
||||
"@vscode/test-electron": "^2.5.2",
|
||||
"eslint": "^9.39.1",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.48.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"grapesjs": "^0.16.45"
|
||||
}
|
||||
}
|
||||
137
src/TkinterEditorProvider.ts
Normal file
137
src/TkinterEditorProvider.ts
Normal file
@ -0,0 +1,137 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { generateTkinterCode } from './generator';
|
||||
import { parsePythonToGrapes } from './pythonParser';
|
||||
|
||||
export class TkinterEditorProvider implements vscode.CustomTextEditorProvider {
|
||||
|
||||
public static readonly viewType = 'tkinter-designer.editor';
|
||||
|
||||
public static register(context: vscode.ExtensionContext): vscode.Disposable {
|
||||
const provider = new TkinterEditorProvider(context);
|
||||
return vscode.window.registerCustomEditorProvider(TkinterEditorProvider.viewType, provider);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly context: vscode.ExtensionContext
|
||||
) { }
|
||||
|
||||
private isSaving = false;
|
||||
|
||||
public async resolveCustomTextEditor(
|
||||
document: vscode.TextDocument,
|
||||
webviewPanel: vscode.WebviewPanel,
|
||||
_token: vscode.CancellationToken
|
||||
): Promise<void> {
|
||||
|
||||
webviewPanel.webview.options = {
|
||||
enableScripts: true,
|
||||
localResourceRoots: [vscode.Uri.file(path.join(this.context.extensionPath, 'media'))]
|
||||
};
|
||||
|
||||
webviewPanel.webview.html = this.getHtmlForWebview(webviewPanel.webview);
|
||||
|
||||
webviewPanel.webview.onDidReceiveMessage(e => {
|
||||
switch (e.type) {
|
||||
case 'request-load':
|
||||
this.handleRequestLoad(document, webviewPanel);
|
||||
break;
|
||||
case 'update-code':
|
||||
this.isSaving = true;
|
||||
this.handleSave(document, e.payload);
|
||||
setTimeout(() => { this.isSaving = false; }, 500);
|
||||
break;
|
||||
case 'request-import':
|
||||
this.handleImport(webviewPanel);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
const folderPath = path.dirname(document.uri.fsPath);
|
||||
const fileNameBase = path.basename(document.uri.fsPath, path.extname(document.uri.fsPath));
|
||||
const pyFilePath = path.join(folderPath, `${fileNameBase}.py`);
|
||||
|
||||
const watcher = vscode.workspace.createFileSystemWatcher(pyFilePath);
|
||||
|
||||
watcher.onDidChange(() => {
|
||||
//
|
||||
});
|
||||
|
||||
webviewPanel.onDidDispose(() => watcher.dispose());
|
||||
}
|
||||
|
||||
private handleRequestLoad(document: vscode.TextDocument, panel: vscode.WebviewPanel) {
|
||||
const text = document.getText();
|
||||
let payload = {};
|
||||
try {
|
||||
if (text.trim().length > 0) payload = JSON.parse(text);
|
||||
} catch (e) { console.error(e); }
|
||||
panel.webview.postMessage({ type: 'load-data', payload: payload });
|
||||
}
|
||||
|
||||
private async handleSave(document: vscode.TextDocument, jsonPayload: any) {
|
||||
const folderPath = path.dirname(document.uri.fsPath);
|
||||
const fileNameBase = path.basename(document.uri.fsPath, path.extname(document.uri.fsPath));
|
||||
const pyFilePath = path.join(folderPath, `${fileNameBase}.py`);
|
||||
|
||||
const pythonCode = generateTkinterCode(jsonPayload);
|
||||
try {
|
||||
fs.writeFileSync(pyFilePath, pythonCode);
|
||||
} catch (e) {
|
||||
console.error(`Python Save error: ${e}`);
|
||||
}
|
||||
|
||||
// СОХРАНЕНИЕ JSON
|
||||
const edit = new vscode.WorkspaceEdit();
|
||||
const fullRange = new vscode.Range(
|
||||
document.positionAt(0),
|
||||
document.positionAt(document.getText().length)
|
||||
);
|
||||
edit.replace(document.uri, fullRange, JSON.stringify(jsonPayload, null, 2));
|
||||
await vscode.workspace.applyEdit(edit);
|
||||
await document.save();
|
||||
}
|
||||
|
||||
private async handleImport(panel: vscode.WebviewPanel) {
|
||||
const uris = await vscode.window.showOpenDialog({
|
||||
canSelectMany: false,
|
||||
openLabel: 'Import Python',
|
||||
filters: { 'Python Files': ['py'] }
|
||||
});
|
||||
if (uris && uris[0]) {
|
||||
const content = fs.readFileSync(uris[0].fsPath, 'utf-8');
|
||||
const data = parsePythonToGrapes(content);
|
||||
panel.webview.postMessage({ type: 'import-data', payload: data });
|
||||
}
|
||||
}
|
||||
|
||||
private getHtmlForWebview(webview: vscode.Webview): string {
|
||||
const mediaPath = path.join(this.context.extensionPath, 'media');
|
||||
const scriptUri = webview.asWebviewUri(vscode.Uri.file(path.join(mediaPath, 'grapes.min.js')));
|
||||
const styleUri = webview.asWebviewUri(vscode.Uri.file(path.join(mediaPath, 'grapes.min.css')));
|
||||
const mainScriptUri = webview.asWebviewUri(vscode.Uri.file(path.join(mediaPath, 'main.js')));
|
||||
const fontUrl = "https://unpkg.com/grapesjs/dist/fonts/grapes.woff";
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource} 'unsafe-inline' https:; script-src ${webview.cspSource} 'unsafe-inline' 'unsafe-eval'; font-src ${webview.cspSource} https: data:;">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="${styleUri}" rel="stylesheet">
|
||||
<style>
|
||||
html, body { height: 100%; margin: 0; overflow: hidden; }
|
||||
#editor { height: 100%; }
|
||||
@font-face { font-family: 'GrapesJS'; src: url('${fontUrl}') format('woff'); font-weight: normal; font-style: normal; }
|
||||
</style>
|
||||
<title>Tkinter Designer</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="editor"></div>
|
||||
<script src="${scriptUri}"></script>
|
||||
<script src="${mainScriptUri}"></script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
}
|
||||
9
src/extension.ts
Normal file
9
src/extension.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { TkinterEditorProvider } from './TkinterEditorProvider';
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
// Регистрируем наш Custom Editor
|
||||
context.subscriptions.push(TkinterEditorProvider.register(context));
|
||||
}
|
||||
|
||||
export function deactivate() {}
|
||||
92
src/generator.ts
Normal file
92
src/generator.ts
Normal file
@ -0,0 +1,92 @@
|
||||
export function generateTkinterCode(json: any): string {
|
||||
|
||||
let width = 500;
|
||||
let height = 400;
|
||||
let title = 'Form';
|
||||
|
||||
// GrapesJS Wrapper data
|
||||
if (json && json.pages && json.pages[0] && json.pages[0].frames) {
|
||||
const wrapper = json.pages[0].frames[0].component;
|
||||
if (wrapper && wrapper.attributes) {
|
||||
if (wrapper.attributes['data-width']) width = wrapper.attributes['data-width'];
|
||||
if (wrapper.attributes['data-height']) height = wrapper.attributes['data-height'];
|
||||
if (wrapper.attributes['data-title']) title = wrapper.attributes['data-title'];
|
||||
}
|
||||
}
|
||||
|
||||
let pythonCode = `import tkinter as tk\n\nroot = tk.Tk()\nroot.title('${title}')\nroot.geometry('${width}x${height}')\n\n`;
|
||||
|
||||
let widgetCounter = 0;
|
||||
|
||||
function processComponent(component: any, parentName: string) {
|
||||
const attrs = component.attributes || {};
|
||||
const type = attrs['data-tk-type'];
|
||||
|
||||
if (!type) {
|
||||
if (component.components) {
|
||||
component.components.forEach((child: any) => processComponent(child, parentName));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
widgetCounter++;
|
||||
const widgetName = `${type.toLowerCase()}_${widgetCounter}`;
|
||||
|
||||
// 1. КОНСТРУКТОР
|
||||
let optionsParts = [parentName];
|
||||
|
||||
let text = attrs['data-text'];
|
||||
if (!text) text = component.traits?.find((t: any) => t.name === 'text')?.value;
|
||||
|
||||
const noTextTypes = ['Entry', 'Canvas', 'Listbox', 'Text', 'Frame'];
|
||||
if (!noTextTypes.includes(type) && text) optionsParts.push(`text="${text}"`);
|
||||
|
||||
if (attrs['data-bg']) optionsParts.push(`bg="${attrs['data-bg']}"`);
|
||||
if (attrs['data-fg']) optionsParts.push(`fg="${attrs['data-fg']}"`);
|
||||
|
||||
const optionsStr = optionsParts.join(', ');
|
||||
if (type === 'Frame') {
|
||||
pythonCode += `${widgetName} = tk.Frame(${optionsStr}, relief="groove", borderwidth=2)\n`;
|
||||
} else {
|
||||
pythonCode += `${widgetName} = tk.${type}(${optionsStr})\n`;
|
||||
}
|
||||
|
||||
// 2. ГЕОМЕТРИЯ (Place)
|
||||
const x = attrs['data-x'] || 0;
|
||||
const y = attrs['data-y'] || 0;
|
||||
const w = attrs['data-width'];
|
||||
const h = attrs['data-height'];
|
||||
|
||||
let placeParts = [`x=${x}`, `y=${y}`];
|
||||
if (w) placeParts.push(`width=${w}`);
|
||||
if (h) placeParts.push(`height=${h}`);
|
||||
pythonCode += `${widgetName}.place(${placeParts.join(', ')})\n`;
|
||||
|
||||
// 3. КОНТЕНТ
|
||||
if (type === 'Entry' && text) pythonCode += `${widgetName}.insert(0, "${text}")\n`;
|
||||
if (type === 'Text' && text) {
|
||||
const safeText = text.replace(/\n/g, '\\n');
|
||||
pythonCode += `${widgetName}.insert("1.0", "${safeText}")\n`;
|
||||
}
|
||||
if (type === 'Listbox' && attrs['data-items']) {
|
||||
const items = attrs['data-items'].split('\n');
|
||||
items.forEach((item: string) => { if(item.trim()) pythonCode += `${widgetName}.insert(tk.END, "${item}")\n`; });
|
||||
}
|
||||
|
||||
// 4. РЕКУРСИЯ
|
||||
if (component.components) {
|
||||
component.components.forEach((child: any) => processComponent(child, widgetName));
|
||||
}
|
||||
pythonCode += "\n";
|
||||
}
|
||||
|
||||
if (json && json.pages && json.pages[0] && json.pages[0].frames) {
|
||||
const rootComponents = json.pages[0].frames[0].component.components;
|
||||
if (rootComponents) {
|
||||
rootComponents.forEach((comp: any) => processComponent(comp, 'root'));
|
||||
}
|
||||
}
|
||||
|
||||
pythonCode += "\nroot.mainloop()";
|
||||
return pythonCode;
|
||||
}
|
||||
225
src/pythonParser.ts
Normal file
225
src/pythonParser.ts
Normal file
@ -0,0 +1,225 @@
|
||||
export function parsePythonToGrapes(pythonCode: string): any {
|
||||
const lines = pythonCode.split('\n');
|
||||
|
||||
// 1. Настройки главного окна
|
||||
let rootWidth = 500;
|
||||
let rootHeight = 400;
|
||||
let rootTitle = 'Form';
|
||||
let rootBg = '#ffffff';
|
||||
|
||||
const geoRegex = /root\.geometry\(['"](\d+)x(\d+)['"]\)/;
|
||||
const titleRegex = /root\.title\(['"](.*)['"]\)/;
|
||||
const bgRootRegex = /root\.configure\(bg=['"](.*)['"]\)/;
|
||||
|
||||
lines.forEach(line => {
|
||||
const geo = line.match(geoRegex);
|
||||
if (geo) { rootWidth = parseInt(geo[1]); rootHeight = parseInt(geo[2]); }
|
||||
const tit = line.match(titleRegex);
|
||||
if (tit) rootTitle = tit[1];
|
||||
const bgr = line.match(bgRootRegex);
|
||||
if (bgr) rootBg = bgr[1];
|
||||
});
|
||||
|
||||
// 2. Хранилища
|
||||
const widgets: { [key: string]: any } = {};
|
||||
const hierarchy: { [key: string]: string } = {};
|
||||
|
||||
const creationRegex = /^\s*(\w+)\s*=\s*tk\.(\w+)\s*\(\s*([^,]+)(?:,\s*(.*))?\)/;
|
||||
const placeRegex = /^\s*(\w+)\.place\s*\(([^)]*)\)/;
|
||||
const insertRegex = /^\s*(\w+)\.insert\s*\([^,]+,\s*["'](.*)["']\)/;
|
||||
|
||||
const typeMap: { [key: string]: string } = {
|
||||
'Label': 'tk-label', 'Button': 'tk-button', 'Entry': 'tk-entry',
|
||||
'Checkbutton': 'tk-check', 'Radiobutton': 'tk-radio',
|
||||
'Listbox': 'tk-listbox', 'Text': 'tk-text', 'Canvas': 'tk-canvas',
|
||||
'Frame': 'tk-frame'
|
||||
};
|
||||
|
||||
const getDefaultStyle = (className: string) => {
|
||||
const base = {
|
||||
'position': 'absolute',
|
||||
'padding': '5px',
|
||||
'border': '1px solid #999',
|
||||
'background-color': '#f0f0f0',
|
||||
'display': 'flex',
|
||||
'align-items': 'center',
|
||||
'justify-content': 'center',
|
||||
'overflow': 'hidden',
|
||||
'color': 'black'
|
||||
};
|
||||
|
||||
if (className === 'Entry' || className === 'Text') {
|
||||
return {
|
||||
...base,
|
||||
'background-color': '#ffffff',
|
||||
'border': '2px inset #ccc',
|
||||
'align-items': 'flex-start',
|
||||
'justify-content': 'flex-start',
|
||||
'white-space': 'pre-wrap'
|
||||
};
|
||||
}
|
||||
if (className === 'Listbox') {
|
||||
return {
|
||||
...base,
|
||||
'background-color': '#ffffff',
|
||||
'border': '2px inset #ccc',
|
||||
'align-items': 'flex-start',
|
||||
'justify-content': 'flex-start',
|
||||
'padding': '0'
|
||||
};
|
||||
}
|
||||
if (className === 'Frame') {
|
||||
return {
|
||||
...base,
|
||||
'border': '2px dashed #555',
|
||||
'background-color': 'rgba(0,0,0,0.05)',
|
||||
'overflow': 'visible'
|
||||
};
|
||||
}
|
||||
if (className === 'Canvas') {
|
||||
return {
|
||||
...base,
|
||||
'border': '1px solid black',
|
||||
'background-color': '#ffffff'
|
||||
};
|
||||
}
|
||||
// Label, Button, Checkbox...
|
||||
return base;
|
||||
};
|
||||
|
||||
lines.forEach(line => {
|
||||
line = line.trim();
|
||||
if (!line || line.startsWith('#')) return;
|
||||
|
||||
const createMatch = line.match(creationRegex);
|
||||
if (createMatch) {
|
||||
const varName = createMatch[1];
|
||||
const className = createMatch[2];
|
||||
const parentName = createMatch[3].trim();
|
||||
const args = createMatch[4] || '';
|
||||
|
||||
if (typeMap[className]) {
|
||||
hierarchy[varName] = parentName;
|
||||
|
||||
const traits: any[] = [];
|
||||
const attrs: any = { 'data-tk-type': className };
|
||||
|
||||
const style = getDefaultStyle(className);
|
||||
|
||||
const paramRegex = /(\w+)=(?:["'](.*?)["']|(\d+))/g;
|
||||
let m;
|
||||
while ((m = paramRegex.exec(args)) !== null) {
|
||||
const key = m[1];
|
||||
const val = m[2] || m[3];
|
||||
|
||||
if (key === 'text') traits.push({ name: 'text', value: val });
|
||||
if (key === 'command') traits.push({ name: 'command', value: val });
|
||||
|
||||
if (key === 'bg') {
|
||||
traits.push({ name: 'bg', value: val });
|
||||
attrs['data-bg'] = val;
|
||||
style['background-color'] = val;
|
||||
}
|
||||
if (key === 'fg') {
|
||||
traits.push({ name: 'fg', value: val });
|
||||
attrs['data-fg'] = val;
|
||||
style['color'] = val;
|
||||
}
|
||||
}
|
||||
|
||||
widgets[varName] = {
|
||||
tagName: 'div',
|
||||
type: typeMap[className],
|
||||
attributes: attrs,
|
||||
traits: traits,
|
||||
components: [],
|
||||
style: style
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// B. Place
|
||||
const placeMatch = line.match(placeRegex);
|
||||
if (placeMatch) {
|
||||
const varName = placeMatch[1];
|
||||
const args = placeMatch[2];
|
||||
|
||||
if (widgets[varName]) {
|
||||
const paramRegex = /(\w+)=(\d+)/g;
|
||||
let m;
|
||||
while ((m = paramRegex.exec(args)) !== null) {
|
||||
const key = m[1];
|
||||
const val = parseInt(m[2]);
|
||||
|
||||
// Обновляем CSS
|
||||
if (key === 'x') widgets[varName].style.left = val + 'px';
|
||||
if (key === 'y') widgets[varName].style.top = val + 'px';
|
||||
if (key === 'width') widgets[varName].style.width = val + 'px';
|
||||
if (key === 'height') widgets[varName].style.height = val + 'px';
|
||||
|
||||
widgets[varName].attributes[`data-${key}`] = val;
|
||||
widgets[varName].traits.push({ name: key, value: val });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// C. Insert
|
||||
const insertMatch = line.match(insertRegex);
|
||||
if (insertMatch) {
|
||||
const varName = insertMatch[1];
|
||||
const textVal = insertMatch[2].replace(/\\n/g, '\n');
|
||||
|
||||
if (widgets[varName]) {
|
||||
const w = widgets[varName];
|
||||
const t = w.attributes['data-tk-type'];
|
||||
|
||||
if (t === 'Entry' || t === 'Text') {
|
||||
let trait = w.traits.find((tr:any) => tr.name === 'text');
|
||||
if (trait) trait.value = textVal;
|
||||
else w.traits.push({ name: 'text', value: textVal });
|
||||
w.attributes['data-text'] = textVal;
|
||||
}
|
||||
else if (t === 'Listbox') {
|
||||
if (!w.attributes['data-items']) w.attributes['data-items'] = [];
|
||||
w.attributes['data-items'].push(textVal);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Сборка дерева
|
||||
const rootComponents: any[] = [];
|
||||
|
||||
Object.keys(widgets).forEach(name => {
|
||||
const widget = widgets[name];
|
||||
const parent = hierarchy[name];
|
||||
|
||||
if (widget.attributes['data-tk-type'] === 'Listbox' && Array.isArray(widget.attributes['data-items'])) {
|
||||
const itemsStr = widget.attributes['data-items'].join('\n');
|
||||
widget.attributes['data-items'] = itemsStr;
|
||||
widget.traits.push({ name: 'items', value: itemsStr });
|
||||
widget.traits.push({ name: 'line_count', value: widget.attributes['data-items'].length });
|
||||
}
|
||||
|
||||
if (parent === 'root') {
|
||||
rootComponents.push(widget);
|
||||
} else if (widgets[parent]) {
|
||||
widgets[parent].components.push(widget);
|
||||
} else {
|
||||
rootComponents.push(widget);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
pages: [{
|
||||
frames: [{
|
||||
component: {
|
||||
type: 'wrapper',
|
||||
attributes: { 'data-width': rootWidth, 'data-height': rootHeight, 'data-title': rootTitle, 'data-bg': rootBg },
|
||||
style: { 'background-color': rootBg, 'width': rootWidth + 'px', 'height': rootHeight + 'px', 'position': 'relative', 'overflow': 'hidden' },
|
||||
components: rootComponents
|
||||
}
|
||||
}]
|
||||
}]
|
||||
};
|
||||
}
|
||||
15
src/test/extension.test.ts
Normal file
15
src/test/extension.test.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import * as assert from 'assert';
|
||||
|
||||
// You can import and use all API from the 'vscode' module
|
||||
// as well as import your extension to test it
|
||||
import * as vscode from 'vscode';
|
||||
// import * as myExtension from '../../extension';
|
||||
|
||||
suite('Extension Test Suite', () => {
|
||||
vscode.window.showInformationMessage('Start all tests.');
|
||||
|
||||
test('Sample test', () => {
|
||||
assert.strictEqual(-1, [1, 2, 3].indexOf(5));
|
||||
assert.strictEqual(-1, [1, 2, 3].indexOf(0));
|
||||
});
|
||||
});
|
||||
17
tsconfig.json
Normal file
17
tsconfig.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "Node16",
|
||||
"target": "ES2022",
|
||||
"outDir": "out",
|
||||
"lib": [
|
||||
"ES2022"
|
||||
],
|
||||
"sourceMap": true,
|
||||
"rootDir": "src",
|
||||
"strict": true, /* enable all strict type-checking options */
|
||||
/* Additional Checks */
|
||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
}
|
||||
}
|
||||
44
vsc-extension-quickstart.md
Normal file
44
vsc-extension-quickstart.md
Normal file
@ -0,0 +1,44 @@
|
||||
# Welcome to your VS Code Extension
|
||||
|
||||
## What's in the folder
|
||||
|
||||
* This folder contains all of the files necessary for your extension.
|
||||
* `package.json` - this is the manifest file in which you declare your extension and command.
|
||||
* 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.
|
||||
* `src/extension.ts` - this is the main file where you will provide the implementation of your command.
|
||||
* 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`.
|
||||
* We pass the function containing the implementation of the command as the second parameter to `registerCommand`.
|
||||
|
||||
## Get up and running straight away
|
||||
|
||||
* Press `F5` to open a new window with your extension loaded.
|
||||
* Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`.
|
||||
* Set breakpoints in your code inside `src/extension.ts` to debug your extension.
|
||||
* Find output from your extension in the debug console.
|
||||
|
||||
## Make changes
|
||||
|
||||
* You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`.
|
||||
* You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes.
|
||||
|
||||
## Explore the API
|
||||
|
||||
* You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`.
|
||||
|
||||
## Run tests
|
||||
|
||||
* Install the [Extension Test Runner](https://marketplace.visualstudio.com/items?itemName=ms-vscode.extension-test-runner)
|
||||
* Run the "watch" task via the **Tasks: Run Task** command. Make sure this is running, or tests might not be discovered.
|
||||
* Open the Testing view from the activity bar and click the Run Test" button, or use the hotkey `Ctrl/Cmd + ; A`
|
||||
* See the output of the test result in the Test Results view.
|
||||
* Make changes to `src/test/extension.test.ts` or create new test files inside the `test` folder.
|
||||
* The provided test runner will only consider files matching the name pattern `**.test.ts`.
|
||||
* You can create folders inside the `test` folder to structure your tests any way you want.
|
||||
|
||||
## Go further
|
||||
|
||||
* [Follow UX guidelines](https://code.visualstudio.com/api/ux-guidelines/overview) to create extensions that seamlessly integrate with VS Code's native interface and patterns.
|
||||
* Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension).
|
||||
* [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VS Code extension marketplace.
|
||||
* Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration).
|
||||
* Integrate to the [report issue](https://code.visualstudio.com/api/get-started/wrapping-up#issue-reporting) flow to get issue and feature requests reported by users.
|
||||
Loading…
Reference in New Issue
Block a user