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

File diff suppressed because one or more lines are too long