diff --git a/apps/example/package.json b/apps/example/package.json index 9a90c25..59281d1 100644 --- a/apps/example/package.json +++ b/apps/example/package.json @@ -22,6 +22,7 @@ "@codemirror/view": "^0.19.45", "@puredit/codemirror-typescript": "*", "@puredit/projections": "*", + "@puredit/simple-projection": "^0.0.0", "@puredit/utils": "*", "@replit/codemirror-indentation-markers": "^0.19.1", "normalize.css": "^8.0.1", diff --git a/apps/example/src/projections/index.ts b/apps/example/src/projections/index.ts index 2db5226..91b3ac6 100644 --- a/apps/example/src/projections/index.ts +++ b/apps/example/src/projections/index.ts @@ -1,13 +1,19 @@ import type { ProjectionPluginConfig } from "@puredit/projections"; import { changeProjection } from "./changeProjection"; import { globalContextValues, globalContextVariables } from "./context"; +import { logProjection } from "./logProjection"; import { tsParser } from "./parser"; import { replaceProjection } from "./replaceProjection"; import { trimProjection } from "./trimProjection"; export const projectionPluginConfig: ProjectionPluginConfig = { parser: tsParser, - projections: [changeProjection, replaceProjection, trimProjection], + projections: [ + changeProjection, + replaceProjection, + trimProjection, + logProjection, + ], globalContextVariables, globalContextValues, }; diff --git a/apps/example/src/projections/logProjection.ts b/apps/example/src/projections/logProjection.ts new file mode 100644 index 0000000..36b1887 --- /dev/null +++ b/apps/example/src/projections/logProjection.ts @@ -0,0 +1,21 @@ +import { arg } from "@puredit/parser"; +import type { Projection } from "@puredit/projections/types"; +import { simpleProjection } from "@puredit/simple-projection"; +import { tsParser } from "./parser"; + +const message = arg("message", "string"); + +export const [pattern, draft] = tsParser.statementPattern` +console.log(${message}); +`; + +export const widget = simpleProjection(["log", message, "to console"]); + +export const logProjection: Projection = { + name: "log message", + description: "Log a message to the console", + pattern, + draft, + requiredContextVariables: [], + widgets: [widget], +}; diff --git a/exp.js b/exp.js deleted file mode 100644 index 87f4328..0000000 --- a/exp.js +++ /dev/null @@ -1,26 +0,0 @@ -function process(input) { - let parts = []; - for (let from = 0, to = 0; to < input.length; to++) { - if (input[to] === "{") { - parts.push(input.slice(from, to)); - from = to + 1; - while (from < input.length && input[from] !== "}") { - from += 1; - } - let variables = input - .slice(to + 1, from) - .split(",") - .map((name) => name.trim()); - parts.push(variables); - to = from; - from += 1; - } - } - return parts; -} - -console.log( - process( - "replace {target} in column {columnTarget, columnSource} with {replacement}" - ) -); diff --git a/package-lock.json b/package-lock.json index 971068c..e837db6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,6 +53,7 @@ "@codemirror/view": "^0.19.45", "@puredit/codemirror-typescript": "*", "@puredit/projections": "*", + "@puredit/simple-projection": "^0.0.0", "@puredit/utils": "*", "@replit/codemirror-indentation-markers": "^0.19.1", "normalize.css": "^8.0.1", @@ -73,7 +74,6 @@ "apps/generate": { "name": "@puredit/generate", "version": "0.0.0", - "extraneous": true, "dependencies": { "@puredit/parser": "*", "mdiff": "^1.4.2", @@ -81,6 +81,57 @@ "yargs": "^17.5.1" } }, + "apps/generate/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "apps/generate/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "apps/generate/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "apps/generate/node_modules/yargs": { + "version": "17.5.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", + "integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "apps/generate/node_modules/yargs-parser": { + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz", + "integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==", + "engines": { + "node": ">=12" + } + }, "apps/jupyter": { "name": "@puredit/jupyter", "version": "0.1.0", @@ -2424,7 +2475,7 @@ "link": true }, "node_modules/@puredit/generate": { - "resolved": "packages/generate", + "resolved": "apps/generate", "link": true }, "node_modules/@puredit/jupyter": { @@ -2447,6 +2498,10 @@ "resolved": "apps/python-dsl", "link": true }, + "node_modules/@puredit/simple-projection": { + "resolved": "packages/simple-projection", + "link": true + }, "node_modules/@puredit/tsconfig": { "resolved": "packages/tsconfig", "link": true @@ -15639,6 +15694,7 @@ "packages/generate": { "name": "@puredit/generate", "version": "0.0.0", + "extraneous": true, "dependencies": { "@puredit/parser": "*", "mdiff": "^1.4.2", @@ -15649,57 +15705,6 @@ "puregen": "index.js" } }, - "packages/generate/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "packages/generate/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "engines": { - "node": ">=8" - } - }, - "packages/generate/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "packages/generate/node_modules/yargs": { - "version": "17.5.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", - "integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "packages/generate/node_modules/yargs-parser": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz", - "integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==", - "engines": { - "node": ">=12" - } - }, "packages/parser": { "name": "@puredit/parser", "version": "0.0.0", @@ -15733,6 +15738,23 @@ "typescript": "^4.5.3" } }, + "packages/simple-projection": { + "name": "@puredit/simple-projection", + "version": "0.0.0", + "license": "MIT", + "dependencies": { + "@codemirror/highlight": "^0.19.7", + "@codemirror/language": "^0.19.7", + "@codemirror/state": "^0.19.9", + "@codemirror/view": "^0.19.45", + "@puredit/projections": "*", + "@puredit/utils": "*" + }, + "devDependencies": { + "@puredit/tsconfig": "*", + "typescript": "^4.5.3" + } + }, "packages/tsconfig": { "name": "@puredit/tsconfig", "version": "0.0.0" @@ -17588,6 +17610,7 @@ "@codemirror/view": "^0.19.45", "@puredit/codemirror-typescript": "*", "@puredit/projections": "*", + "@puredit/simple-projection": "*", "@puredit/utils": "*", "@replit/codemirror-indentation-markers": "^0.19.1", "@sveltejs/vite-plugin-svelte": "^1.0.0-next.30", @@ -17604,7 +17627,7 @@ } }, "@puredit/generate": { - "version": "file:packages/generate", + "version": "file:apps/generate", "requires": { "@puredit/parser": "*", "mdiff": "^1.4.2", @@ -17773,6 +17796,19 @@ "vite": "^2.8.0" } }, + "@puredit/simple-projection": { + "version": "file:packages/simple-projection", + "requires": { + "@codemirror/highlight": "^0.19.7", + "@codemirror/language": "^0.19.7", + "@codemirror/state": "^0.19.9", + "@codemirror/view": "^0.19.45", + "@puredit/projections": "*", + "@puredit/tsconfig": "*", + "@puredit/utils": "*", + "typescript": "^4.5.3" + } + }, "@puredit/tsconfig": { "version": "file:packages/tsconfig" }, diff --git a/packages/projections/svelte.ts b/packages/projections/svelte.ts index ec65fec..988a975 100644 --- a/packages/projections/svelte.ts +++ b/packages/projections/svelte.ts @@ -94,8 +94,6 @@ export const svelteProjection = (Component: ComponentClass) => } onLeaveEnd(): void { - // TODO: pass range of decoration to widget, as a match can - // consist of multiple decorations if it contains blocks. let end = this.match.node.endIndex; if (this.match.blocks.length) { end = this.match.blocks[0].from; diff --git a/packages/simple-projection/index.ts b/packages/simple-projection/index.ts new file mode 100644 index 0000000..bd397a9 --- /dev/null +++ b/packages/simple-projection/index.ts @@ -0,0 +1,137 @@ +import { EditorSelection, EditorState } from "@codemirror/state"; +import type { EditorView } from "@codemirror/view"; +import { HighlightStyle, tags } from "@codemirror/highlight"; +import type { Match, TemplateArg } from "@puredit/parser"; +import { FocusGroup, FocusGroupHandler } from "@puredit/projections/focus"; +import { ProjectionWidget } from "@puredit/projections/projection"; +import TextInput from "@puredit/projections/TextInput.svelte"; +import { isString } from "@puredit/utils"; +import type { SvelteComponent } from "@puredit/projections/svelte"; + +export const simpleProjection = ( + template: Array +) => + class extends ProjectionWidget implements FocusGroupHandler { + focusGroup!: FocusGroup; + inputs: Array<[TemplateArg[], SvelteComponent]>; + + protected initialize( + match: Match, + context: object, + state: EditorState + ): HTMLElement { + this.focusGroup = new FocusGroup(this); + this.inputs = []; + const target = document.createElement("span"); + target.className = "inline-flex"; + for (const part of template) { + const element = document.createElement("span"); + target.appendChild(element); + if (isString(part)) { + element.textContent = part; + } else { + const args = part instanceof Array ? part : [part]; + const component = new TextInput({ + target: element, + props: { + className: HighlightStyle.get(state, tags.string), + node: match.args[args[0].name], + targetNodes: + args.length > 1 + ? args.map((arg) => match.args[arg.name]) + : undefined, + placeholder: args[0].name, + state, + view: null, + focusGroup: this.focusGroup, + }, + }); + this.inputs.push([args, component]); + } + } + return target; + } + + protected update(match: Match, context: object, state: EditorState): void { + for (const [args, component] of this.inputs) { + component.$set({ + node: match.args[args[0].name], + targetNodes: + args.length > 1 + ? args.map((arg) => match.args[arg.name]) + : undefined, + context, + state, + }); + } + } + + toDOM(view: EditorView): HTMLElement { + const dom = super.toDOM(view); + let isFocused = false; + dom.addEventListener("focusin", () => { + isFocused = true; + view.dispatch({ + selection: { + anchor: this.match.node.startIndex, + }, + }); + }); + dom.addEventListener("focusout", () => { + isFocused = false; + }); + dom.addEventListener("click", () => { + if (!isFocused) { + view.dispatch({ + selection: { + anchor: this.match.node.startIndex, + head: this.match.node.endIndex, + }, + }); + } + }); + for (const [_args, component] of this.inputs) { + component.$set({ view }); + } + if (this.isNew) { + requestAnimationFrame(() => { + this.focusGroup.first(); + }); + } + return dom; + } + + destroy(dom: HTMLElement): void { + for (const [_args, component] of this.inputs) { + component.$set({ view: null }); + component.$destroy(); + } + super.destroy(dom); + } + + enterFromStart(): boolean { + return this.focusGroup.first(); + } + + enterFromEnd(): boolean { + return this.focusGroup.last(); + } + + onLeaveStart(): void { + this.view?.focus(); + this.view?.dispatch({ + selection: EditorSelection.single(this.match.node.startIndex), + }); + } + + onLeaveEnd(): void { + let end = this.match.node.endIndex; + if (this.match.blocks.length) { + end = this.match.blocks[0].from; + } + this.view?.focus(); + this.view?.dispatch({ + selection: EditorSelection.single(end), + }); + } + }; diff --git a/packages/simple-projection/package.json b/packages/simple-projection/package.json new file mode 100644 index 0000000..bdb8b17 --- /dev/null +++ b/packages/simple-projection/package.json @@ -0,0 +1,19 @@ +{ + "name": "@puredit/simple-projection", + "version": "0.0.0", + "main": "./index.ts", + "types": "./index.ts", + "license": "MIT", + "dependencies": { + "@codemirror/highlight": "^0.19.7", + "@codemirror/language": "^0.19.7", + "@codemirror/state": "^0.19.9", + "@codemirror/view": "^0.19.45", + "@puredit/projections": "*", + "@puredit/utils": "*" + }, + "devDependencies": { + "@puredit/tsconfig": "*", + "typescript": "^4.5.3" + } +}