feat(core): mix模式下使用diff去修改代码防止触发整个文件代码高亮的重新构建

This commit is contained in:
王立帮
2026-01-23 10:06:04 +08:00
parent 7fdd11b598
commit 51d5a286f7
31 changed files with 6378 additions and 46 deletions

View File

@@ -1,7 +1,5 @@
goog.loadJs('common', () => {
goog.require('ace');
goog.require('ace.ExtLanguageTools');
goog.require('Mixly.Config');
goog.require('Mixly.XML');
goog.require('Mixly.Env');
@@ -11,6 +9,8 @@ goog.require('Mixly.Menu');
goog.require('Mixly.ContextMenu');
goog.require('Mixly.IdGenerator');
goog.require('Mixly.CodeFormatter');
goog.require('Mixly.MonacoTheme');
goog.require('Mixly.MonacoTreeSitter');
goog.require('Mixly.EditorMonaco');
goog.provide('Mixly.EditorCode');
@@ -24,12 +24,16 @@ const {
ContextMenu,
IdGenerator,
CodeFormatter,
MonacoTheme,
MonacoTreeSitter,
EditorMonaco
} = Mixly;
const { USER } = Config;
class EditorCode extends EditorMonaco {
#contextMenu_ = null;
#monacoTreeSitter_ = null;
constructor() {
super();
@@ -41,6 +45,7 @@ class EditorCode extends EditorMonaco {
this.setTabSize(4);
this.#addContextMenu_();
this.setTheme(USER.theme);
this.#monacoTreeSitter_ = new MonacoTreeSitter(this);
}
onMounted() {
@@ -113,40 +118,65 @@ class EditorCode extends EditorMonaco {
setValue(data, ext) {
this.disableChangeEvent();
super.setValue(data, ext);
super.setValue(data);
const language = this.getLanguageByExt(ext);
if (MonacoTheme.supportThemes.includes(language)) {
this.setTheme(`${USER.theme}-${language}`);
} else {
this.setTheme(USER.theme);
}
this.setLanguage(language);
this.enableChangeEvent();
CodeFormatter.activateFormatter(language)
.then((formatter) => {
const menu = this.#contextMenu_.getItem('code');
if (!formatter) {
menu.remove('sep-format');
menu.remove('format');
return;
this.setCodeFormatter(language, ext).catch(Debug.error);
}
diffEdits(data, ext) {
this.disableChangeEvent();
super.diffEdits(data);
const language = this.getLanguageByExt(ext);
this.setLanguage(language);
if (MonacoTheme.supportThemes.includes(language)) {
this.setTheme(`${USER.theme}-${language}`);
} else {
this.setTheme(USER.theme);
}
this.#monacoTreeSitter_.setValue(language, USER.theme, data);
this.enableChangeEvent();
this.setCodeFormatter(language, ext).catch(Debug.error);
}
async setCodeFormatter(language, ext) {
const formatter = await CodeFormatter.activateFormatter(language);
const menu = this.#contextMenu_.getItem('code');
if (!formatter) {
menu.remove('sep-format');
menu.remove('format');
return;
}
menu.add({
weight: 6,
id: 'sep-format',
data: '---------'
});
menu.add({
weight: 6,
id: 'format',
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['editor.contextMenu.formatDocument'], ''),
callback: () => {
CodeFormatter.format(language, this.getValue())
.then((data) => {
super.setValue(data, ext);
})
.catch(Debug.error);
}
menu.add({
weight: 6,
id: 'sep-format',
data: '---------'
});
menu.add({
weight: 6,
id: 'format',
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['editor.contextMenu.formatDocument'], ''),
callback: (key, opt) => {
CodeFormatter.format(language, this.getValue())
.then((data) => {
super.setValue(data, ext);
})
.catch(Debug.error);
}
}
});
})
.catch(Debug.error);
}
});
}
getTreeSitter() {
return this.#monacoTreeSitter_;
}
getLanguageByExt(ext) {
@@ -183,7 +213,10 @@ class EditorCode extends EditorMonaco {
#addChangeEventListenerExt_() {
this.offEvent('change');
this.bind('change', () => this.addDirty());
this.bind('change', () => {
this.addDirty();
this.#monacoTreeSitter_.setValue(this.getLanguage(), USER.theme, this.getValue());
});
}
dispose() {

View File

@@ -39,7 +39,7 @@ const {
HTMLTemplate,
LayerExt
} = Mixly;
const { BOARD, SOFTWARE } = Config;
const { BOARD, SOFTWARE, USER } = Config;
const { form } = layui;
@@ -173,6 +173,7 @@ class EditorMix extends EditorBase {
codePage.offEvent('change');
codePage.bind('change', () => {
this.addDirty();
codePage.getTreeSitter().setValue(codePage.getLanguage(), USER.theme, codePage.getValue());
});
}
@@ -217,12 +218,12 @@ class EditorMix extends EditorBase {
&& typeof this.py2BlockEditor.updateBlock === 'function') {
this.py2BlockEditor.updateBlock();
} else {
codePage.setValue(blockPage.getValue(), this.#language_);
codePage.diffEdits(blockPage.getValue(), this.#language_);
}
break;
case Drag.Extend.POSITIVE:
this.#temp_ = blockPage.getValue();
codePage.setValue(this.#temp_, this.#language_);
codePage.diffEdits(this.#temp_, this.#language_);
break;
}
blockPage.resize();
@@ -363,7 +364,7 @@ class EditorMix extends EditorBase {
return;
}
this.#temp_ = code;
codePage.setValue(code, this.#language_);
codePage.diffEdits(code, this.#language_);
});
this.#addCodeChangeEventListener_();
}
@@ -549,7 +550,7 @@ class EditorMix extends EditorBase {
return;
}
this.drag.full(Drag.Extend.NEGATIVE); // 完全显示代码编辑器
codePage.setValue(code, this.#language_);
codePage.diffEdits(code, this.#language_);
endFunc('USE_CODE');
return;
}
@@ -585,7 +586,7 @@ class EditorMix extends EditorBase {
Blockly.hideChaff();
if (!useIncompleteBlocks && codeDom && xmlDom.attr('shown') === 'code') {
this.drag.full(Drag.Extend.NEGATIVE); // 完全显示代码编辑器
codePage.setValue(code, this.#language_);
codePage.diffEdits(code, this.#language_);
endFunc();
return;
}

View File

@@ -1,5 +1,6 @@
goog.loadJs('common', () => {
goog.require('Diff');
goog.require('monaco');
goog.require('Mixly.XML');
goog.require('Mixly.Env');
@@ -70,8 +71,8 @@ class EditorMonaco extends EditorBase {
}
});
editor.onDidChangeModelContent(() => {
this.events.run('change');
editor.onDidChangeModelContent((...args) => {
this.events.run('change', ...args);
});
}
@@ -121,6 +122,7 @@ class EditorMonaco extends EditorBase {
#state_ = null;
#tabSize_ = null;
#language_ = null;
#mode_ = null;
constructor() {
super();
@@ -132,6 +134,7 @@ class EditorMonaco extends EditorBase {
init() {
super.init();
this.#editor_ = monaco.editor.createModel('');
this.#editor_.setEOL(monaco.editor.EndOfLineSequence.LF);
}
onMounted() {
@@ -187,13 +190,21 @@ class EditorMonaco extends EditorBase {
}
setTheme(mode) {
if (this.#mode_ === mode) {
return;
}
const editor = EditorMonaco.getEditor();
editor.updateOptions({
theme: `vs-${mode}`
});
this.#mode_ = mode;
}
setValue(data, ext) {
getTheme() {
return this.#mode_;
}
setValue(data) {
if (this.getValue() === data) {
return;
}
@@ -209,6 +220,72 @@ class EditorMonaco extends EditorBase {
return this.#editor_.getValue();
}
diffEdits(data) {
const prevData = this.getValue();
if (prevData === data) {
return;
}
const edits = this.buildDiffEdits(prevData, data);
if (!edits.length) {
return;
}
this.#editor_.pushEditOperations([], edits, () => null);
}
buildDiffEdits(prevData, nextData) {
if (prevData === nextData) {
return [];
}
const diffs = Diff.diffChars(prevData, nextData);
const edits = [];
const state = {
index: 0,
pendingStart: null,
pendingText: ''
};
for (const d of diffs) {
if (d.added) {
if (state.pendingStart == null) {
state.pendingStart = state.index;
}
state.pendingText += d.value;
}
else if (d.removed) {
if (state.pendingStart == null) {
state.pendingStart = state.index;
}
state.index += d.value.length;
}
else {
this.flushPendingEdit(state, edits);
state.index += d.value.length;
}
}
this.flushPendingEdit(state, edits);
return edits;
}
flushPendingEdit(state, edits) {
const { pendingStart, index, pendingText } = state;
if (pendingStart == null) {
return;
}
const startPos = this.#editor_.getPositionAt(pendingStart);
const endPos = this.#editor_.getPositionAt(index);
edits.push({
range: new monaco.Range(
startPos.lineNumber,
startPos.column,
endPos.lineNumber,
endPos.column
),
text: pendingText,
forceMoveMarkers: true
});
state.pendingStart = null;
state.pendingText = '';
}
clear() {
this.setValue('', true);
}
@@ -242,7 +319,7 @@ class EditorMonaco extends EditorBase {
let selection = editor.getSelection();
let selectedText = this.#editor_.getValueInRange(selection);
if (selection) {
editor.executeEdits("cut", [{ range: selection, text: '' }]);
editor.executeEdits('cut', [{ range: selection, text: '' }]);
navigator.clipboard.writeText(selectedText);
}
this.focus();
@@ -281,6 +358,10 @@ class EditorMonaco extends EditorBase {
monaco.editor.setModelLanguage(this.#editor_, language);
}
getLanguage() {
return this.#language_;
}
setTabSize(tabSize) {
if (this.#tabSize_ === tabSize) {
return;

View File

@@ -0,0 +1,119 @@
goog.loadJs('common', () => {
goog.require('monaco');
goog.require('Mixly.Registry');
goog.provide('Mixly.MonacoTheme');
const { Registry } = Mixly;
class MonacoTheme {
static {
this.cssClassNamePrefix = 'mts';
this.themesRegistry = new Registry();
this.supportThemes = [];
// this.supportThemes = ['cpp', 'python'];
this.getClassNameOfTerm = function (type, theme, term) {
return `${this.cssClassNamePrefix}-${type}-${theme}-${term}`;
}
/*this.themesRegistry.register('vs-dark-cpp', new MonacoTheme(
'vs-dark-cpp',
'cpp',
'dark',
goog.readJsonSync('../common/templates/json/tree-sitter/themes/dark-cpp.json')
));
this.themesRegistry.register('vs-light-cpp', new MonacoTheme(
'vs-light-cpp',
'cpp',
'light',
goog.readJsonSync('../common/templates/json/tree-sitter/themes/light-cpp.json')
));
this.themesRegistry.register('vs-dark-python', new MonacoTheme(
'vs-dark-python',
'python',
'dark',
goog.readJsonSync('../common/templates/json/tree-sitter/themes/dark-python.json')
));
this.themesRegistry.register('vs-light-python', new MonacoTheme(
'vs-light-python',
'python',
'light',
goog.readJsonSync('../common/templates/json/tree-sitter/themes/light-python.json')
));*/
}
#id_ = null;
#type_ = null;
#theme_ = null;
#$tag_ = null;
#config_ = null;
constructor(id, type, theme, config) {
this.#id_ = id;
this.#type_ = type;
this.#theme_ = theme;
this.load(config);
}
load(config) {
monaco.editor.defineTheme(this.#id_, config.base);
this.#config_ = config;
if (!this.#$tag_) {
let hasStyleNode = $('head').find(`style[style-id='${this.id_}']`).length;
if (hasStyleNode) {
return;
}
this.#$tag_ = $('<style></style>');
this.#$tag_.attr('style-id', this.id);
this.#$tag_.attr('type', 'text/css')
$('head').append(this.#$tag_);
}
this.#$tag_.html(this.generateCss());
}
generateCss() {
return Object.keys(this.#config_.monacoTreeSitter)
.map(term =>
`span.${MonacoTheme.getClassNameOfTerm(this.#type_, this.#theme_, term)}{${this.generateStyleOfTerm(term)}}`
)
.join('');
}
generateStyleOfTerm(term) {
const style = this.#config_.monacoTreeSitter[term];
if (!style) {
return '';
}
if (typeof style === 'string') {
return `color:${style}`;
}
return `color:${style.color};${style.extraCssStyles || ''}`;
}
getColorOfTerm(term) {
const style = this.#config_.monacoTreeSitter[term];
if (!style) {
return undefined;
}
return typeof style === 'object' ? style.color : style;
}
dispose() {
this.#$tag_ && this.#$tag_.remove();
this.#$tag_ = null;
this.#config_ = null;
}
}
Mixly.MonacoTheme = MonacoTheme;
});

View File

@@ -0,0 +1,127 @@
goog.loadJs('common', () => {
goog.require('_');
goog.require('monaco');
goog.require('Mixly.Registry');
goog.require('Mixly.MonacoTheme');
goog.provide('Mixly.MonacoTreeSitter');
const { Registry, MonacoTheme } = Mixly;
class MonacoTreeSitter {
static {
this.workerPath = '../common/modules/mixly-modules/workers/common/tree-sitter/index.js';
this.supportTreeSitters_ = new Registry();
this.activeTreeSitters_ = new Registry();
/*this.supportTreeSitters_.register('python', {
workerName: 'pythonTreeSitterService',
wasm: 'tree-sitter-python.wasm'
});
this.supportTreeSitters_.register('cpp', {
workerName: 'cppTreeSitterService',
wasm: 'tree-sitter-cpp.wasm'
});*/
this.activateTreeSitter = async function (type) {
if (!this.supportTreeSitters_.hasKey(type)) return null;
const info = this.supportTreeSitters_.getItem(type);
if (this.activeTreeSitters_.hasKey(type)) {
const ts = this.activeTreeSitters_.getItem(type);
if (ts.loading) await ts.loading;
return ts;
}
const treeSitter = workerpool.pool(this.workerPath, {
workerOpts: { name: info.workerName },
workerType: 'web'
});
const grammar = await goog.readJson(
`../common/templates/json/tree-sitter/grammars/${type}.json`
);
treeSitter.loading = treeSitter.exec(
'init',
[info.wasm, grammar]
);
this.activeTreeSitters_.register(type, treeSitter);
await treeSitter.loading;
treeSitter.loading = null;
return treeSitter;
};
this.treeSitterPostion = function (pos) {
return {
row: pos.lineNumber - 1,
column: pos.column - 1
};
};
}
constructor(editor, opts) {
this.editor = editor;
this.seq = 0;
this.decorations = [];
this.refresh = _.debounce(
this.refresh.bind(this),
opts?.debounceUpdate ?? 15
);
}
dispose() {
this.pool.terminate(true);
this.decorations = [];
}
async updateWorker(type, theme, text) {
const treeSitter = await MonacoTreeSitter.activateTreeSitter(type);
if (!treeSitter) {
return;
}
const id = ++this.seq;
const dto = await treeSitter.exec('update', [text]);
if (id !== this.seq) return;
this.applyDecorations(type, theme, dto);
}
applyDecorations(type, theme, dto) {
const decos = [];
for (const [term, ranges] of Object.entries(dto)) {
const className = MonacoTheme.getClassNameOfTerm(type, theme, term);
for (const r of ranges) {
decos.push({
range: new monaco.Range(
r.startLineNumber,
r.startColumn,
r.endLineNumber,
r.endColumn
),
options: {
inlineClassName: className
}
});
}
}
this.decorations = this.editor.getEditor().deltaDecorations(this.decorations, decos);
}
refresh(type, theme, newText) {
this.updateWorker(type, theme, newText);
}
setValue(type, theme, newText) {
this.refresh(type, theme, newText);
}
}
Mixly.MonacoTreeSitter = MonacoTreeSitter;
});

View File

@@ -9,6 +9,7 @@ goog.require('Mixly.Debug');
goog.require('Mixly.Config');
goog.require('Mixly.StatusBar');
goog.require('Mixly.SideBarsManager');
goog.require('Mixly.RightSideBarsManager');
goog.require('Mixly.HTMLTemplate');
goog.require('Mixly.PageBase');
goog.require('Mixly.Menu');

View File

@@ -60,7 +60,7 @@ class StatusBar extends EditorAce {
} else {
editor.setOption('theme', 'ace/theme/xcode');
}
editor.getSession().setMode("ace/mode/python");
editor.getSession().setMode('ace/mode/python');
editor.setReadOnly(true);
// editor.setScrollSpeed(0.3);
editor.setShowPrintMargin(false);

View File

@@ -286,8 +286,6 @@
{
"path": "/common/editor-code.js",
"require": [
"ace",
"ace.ExtLanguageTools",
"Mixly.Config",
"Mixly.XML",
"Mixly.Env",
@@ -297,6 +295,8 @@
"Mixly.ContextMenu",
"Mixly.IdGenerator",
"Mixly.CodeFormatter",
"Mixly.MonacoTheme",
"Mixly.MonacoTreeSitter",
"Mixly.EditorMonaco"
],
"provide": [
@@ -351,6 +351,7 @@
{
"path": "/common/editor-monaco.js",
"require": [
"Diff",
"monaco",
"Mixly.XML",
"Mixly.Env",
@@ -778,6 +779,28 @@
"Mixly.MJson"
]
},
{
"path": "/common/monaco-theme.js",
"require": [
"monaco",
"Mixly.Registry"
],
"provide": [
"Mixly.MonacoTheme"
]
},
{
"path": "/common/monaco-tree-sitter.js",
"require": [
"_",
"monaco",
"Mixly.Registry",
"Mixly.MonacoTheme"
],
"provide": [
"Mixly.MonacoTreeSitter"
]
},
{
"path": "/common/msg.js",
"require": [
@@ -1115,6 +1138,7 @@
"Mixly.Config",
"Mixly.StatusBar",
"Mixly.SideBarsManager",
"Mixly.RightSideBarsManager",
"Mixly.HTMLTemplate",
"Mixly.PageBase",
"Mixly.Menu",

View File

@@ -0,0 +1,141 @@
importScripts('./tree-sitter.js');
importScripts('../../../../web-modules/workerpool.min.js');
const terms = [
'type',
'scope',
'function',
'variable',
'number',
'string',
'comment',
'constant',
'directive',
'control',
'operator',
'modifier',
'punctuation'
];
let parser;
let tree = null;
let language = null;
class Language {
constructor(grammarJson) {
this.simpleTerms = {};
this.complexTerms = [];
this.complexScopes = {};
for (const t in grammarJson.simpleTerms)
this.simpleTerms[t] = grammarJson.simpleTerms[t];
for (const t in grammarJson.complexTerms)
this.complexTerms[t] = grammarJson.complexTerms[t];
for (const t in grammarJson.complexScopes)
this.complexScopes[t] = grammarJson.complexScopes[t];
this.complexDepth = 0;
this.complexOrder = false;
for (const s in this.complexScopes) {
const depth = s.split('>').length;
if (depth > this.complexDepth)
this.complexDepth = depth;
if (s.indexOf('[') >= 0)
this.complexOrder = true;
}
this.complexDepth--;
}
}
async function init(languageWasmPath, grammarJson) {
await TreeSitter.init();
parser = new TreeSitter();
const lang = await TreeSitter.Language.load(languageWasmPath);
console.log(lang.version)
parser.setLanguage(lang);
language = new Language(grammarJson);
tree = null;
}
function buildDecorations(tree) {
const result = [];
const stack = [];
let node = tree.rootNode.firstChild;
while (stack.length > 0 || node) {
if (node) {
stack.push(node);
node = node.firstChild;
}
else {
node = stack.pop();
let type = node.type;
if (!node.isNamed())
type = '"' + type + '"';
let term = null;
if (!language.complexTerms.includes(type)) {
term = language.simpleTerms[type];
}
else {
let desc = type;
let scopes = [desc];
let parent = node.parent;
for (let i = 0; i < language.complexDepth && parent; i++) {
let parentType = parent.type;
if (!parent.isNamed())
parentType = '"' + parentType + '"';
desc = parentType + ' > ' + desc;
scopes.push(desc);
parent = parent.parent;
}
if (language.complexOrder) {
let index = 0;
let sibling = node.previousSibling;
while (sibling) {
if (sibling.type === node.type)
index++;
sibling = sibling.previousSibling;
}
let rindex = -1;
sibling = node.nextSibling;
while (sibling) {
if (sibling.type === node.type)
rindex--;
sibling = sibling.nextSibling;
}
const orderScopes = [];
for (let i = 0; i < scopes.length; i++)
orderScopes.push(scopes[i], scopes[i] + '[' + index + ']', scopes[i] + '[' + rindex + ']');
scopes = orderScopes;
}
for (const d of scopes)
if (d in language.complexScopes)
term = language.complexScopes[d];
}
if (terms.includes(term)) {
if (!result[term]) {
result[term] = [];
}
result[term].push({
startLineNumber: node.startPosition.row + 1,
startColumn: node.startPosition.column + 1,
endLineNumber: node.endPosition.row + 1,
endColumn: node.endPosition.column + 1
});
}
node = node.nextSibling;
}
}
return result;
}
function update(text) {
const tree = parser.parse(text);
return buildDecorations(tree);
}
workerpool.worker({
init,
update
});

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff