diff --git a/README.md b/README.md index ac043add9..507ffbab3 100644 --- a/README.md +++ b/README.md @@ -209,7 +209,7 @@ Note: the replacement value **must be boolean literals** and cannot be strings, - Intlify message format compiler - [x] vue-i18n message format - - [ ] sourcemap + - [x] sourcemap - [x] HTML format handling - [ ] more unit (fuzzing) tests - [x] performance tests (benchmark) @@ -324,14 +324,15 @@ Note: the replacement value **must be boolean literals** and cannot be strings, - [x] vite-plugin-vue-i18n - [ ] vue-cli-plugin-i18n - [ ] eslint-plugin-vue-i18n - - [ ] message format pre-compilation tools - Others - [ ] documentation - [x] fallback localization (bubble up) - [x] SSR - General tasks - [x] error handlings +- Next Tasks (v9.1) - [ ] monorepo + - [ ] message format pre-compilation tools diff --git a/devtools-test/yarn.lock b/devtools-test/yarn.lock index 8ec0cf20b..b1a90577e 100644 --- a/devtools-test/yarn.lock +++ b/devtools-test/yarn.lock @@ -1865,6 +1865,11 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +source-map@^0.7.3: + version "0.7.3" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" + integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== + sourcemap-codec@^1.4.4: version "1.4.8" resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" diff --git a/format-explorer/package.json b/format-explorer/package.json index 262ed7856..34f3ccf9b 100644 --- a/format-explorer/package.json +++ b/format-explorer/package.json @@ -7,27 +7,28 @@ "license": "MIT", "scripts": { "build": "webpack", - "dev": "webpack-dev-server", + "dev": "webpack serve", "clean": "rm -rf ./dist" }, "devDependencies": { "@vue/compiler-sfc": "^3.0.0", "css-loader": "^4.3.0", "file-loader": "^6.1.0", + "html-webpack-plugin": "^4.3.0", + "monaco-editor-webpack-plugin": "^2.0.0", + "style-loader": "^2.0.0", "ts-loader": "^8.0.0", - "url-loader": "^4.1.0", "typescript": "^4.0.3", + "url-loader": "^4.1.0", "vue-loader": "^16.0.0-beta.8", - "webpack": "^4.44.0", - "webpack-cli": "^3.3.12", - "webpack-dev-server": "^3.11.0", - "html-webpack-plugin": "^4.3.0", - "monaco-editor-webpack-plugin": "^2.0.0", - "style-loader": "^2.0.0" + "webpack": "^5.1.0", + "webpack-cli": "^4.0.0", + "webpack-dev-server": "^3.11.0" }, "dependencies": { + "monaco-editor": "^0.21.2", + "source-map": "^0.6.1", "vue": "^3.0.0", - "vue-i18n": "link:..", - "monaco-editor": "^0.21.2" + "vue-i18n": "link:.." } } diff --git a/format-explorer/src/App.vue b/format-explorer/src/App.vue index 577bfc865..8411fbfe7 100644 --- a/format-explorer/src/App.vue +++ b/format-explorer/src/App.vue @@ -3,6 +3,9 @@ import { defineComponent, ref } from 'vue' import Navigation from './components/Navigation.vue' import Editor from './components/Editor.vue' import { baseCompile } from 'vue-i18n' +import * as monaco from 'monaco-editor' +import { debounce } from './utils' +import { SourceMapConsumer } from 'source-map' import type { CompileError, CompileOptions } from 'vue-i18n' interface PersistedState { @@ -35,16 +38,19 @@ export default defineComponent({ * utilties */ let lastSuccessCode: string - function compile(message: string): string { + let lastSuccessfulMap: SourceMapConsumer | undefined + async function compile(message: string): Promise { console.clear() try { const start = performance.now() const errors: CompileError[] = [] - const { code, ast } = baseCompile(message, { - onError: err => errors.push(err) - }) + const options = { + sourceMap: true, + onError: (err: CompileError) => errors.push(err) + } + const { code, ast, map } = baseCompile(message, options) if (errors.length > 0) { console.error(errors) } @@ -52,10 +58,13 @@ export default defineComponent({ console.log(`Compiled in ${(performance.now() - start).toFixed(2)}ms.`) compileErrors.value = errors console.log(`AST: `, ast) + console.log('sourcemap', map) const evalCode = new Function(`return ${code}`)() lastSuccessCode = evalCode.toString() + `\n\n// Check the console for the AST` + lastSuccessfulMap = await new SourceMapConsumer(map!) + lastSuccessfulMap!.computeColumnSpans() } catch (e) { lastSuccessCode = `/* ERROR: ${e.message} (see console for more info) */` console.error(e) @@ -67,11 +76,121 @@ export default defineComponent({ /** * envet handlers */ - const onChange = (message: string): void => { + + let inputEditor: monaco.editor.IStandaloneCodeEditor | null = null + let outputEditor: monaco.editor.IStandaloneCodeEditor | null = null + + // input editor model change event + const onChangeModel = async (message: string): Promise => { const state = JSON.stringify({ src: message } as PersistedState) localStorage.setItem('state', state) window.location.hash = encodeURIComponent(state) - genCodes.value = compile(message) + genCodes.value = await compile(message) + } + + // highlight output codes + let prevOutputDecos: string[] = [] + function clearOutputDecos() { + if (!outputEditor) { + return + } + prevOutputDecos = outputEditor.deltaDecorations(prevOutputDecos, []) + } + + let prevInputDecos: string[] = [] + function clearInputDecos() { + if (!inputEditor) { + return + } + prevInputDecos = inputEditor.deltaDecorations(prevInputDecos, []) + } + + // input editor ready event + const onReadyInput = (editor: monaco.editor.IStandaloneCodeEditor) => { + inputEditor = editor + inputEditor.onDidChangeCursorPosition( + debounce(e => { + clearInputDecos() + if (lastSuccessfulMap) { + const pos = lastSuccessfulMap.generatedPositionFor({ + source: 'message.intl', + line: e.position.lineNumber, + column: e.position.column + }) + if (pos.line != null && pos.column != null) { + prevOutputDecos = outputEditor!.deltaDecorations( + prevOutputDecos, + [ + { + range: new monaco.Range( + pos.line, + pos.column + 1, + pos.line, + pos.lastColumn ? pos.lastColumn + 2 : pos.column + 2 + ), + options: { + inlineClassName: `highlight` + } + } + ] + ) + outputEditor!.revealPositionInCenter({ + lineNumber: pos.line, + column: pos.column + 1 + }) + } else { + clearOutputDecos() + } + } + }, 100) + ) + } + + // output editor ready event + const onReadyOutput = (editor: monaco.editor.IStandaloneCodeEditor) => { + outputEditor = editor + editor.onDidChangeCursorPosition( + debounce(e => { + clearOutputDecos() + if (lastSuccessfulMap) { + const pos = lastSuccessfulMap.originalPositionFor({ + line: e.position.lineNumber, + column: e.position.column + }) + console.log('onReadyOutput', e.position, pos) + if ( + pos.line != null && + pos.column != null && + !( + // ignore mock location + (pos.line === 1 && pos.column === 0) + ) + ) { + const translatedPos = { + column: pos.column + 1, + lineNumber: pos.line + } + prevInputDecos = inputEditor!.deltaDecorations(prevInputDecos, [ + { + range: new monaco.Range( + pos.line, + pos.column + 1, + pos.line, + pos.column + 1 + ), + options: { + isWholeLine: true, + className: `highlight` + } + } + ]) + inputEditor!.revealPositionInCenter(translatedPos) + } else { + clearInputDecos() + } + } + }, 100) + ) } // setup context @@ -79,7 +198,9 @@ export default defineComponent({ initialCodes, genCodes, compileErrors, - onChange + onChangeModel, + onReadyInput, + onReadyOutput } } }) @@ -87,9 +208,10 @@ export default defineComponent({