diff --git a/esbuild.config.mjs b/esbuild.config.mjs index 414b632..0bf9eff 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -25,6 +25,22 @@ const zipPlugin = { }, }; +// Custom plugin to redirect CodeMirror dependencies to Acode's global acode.require system +const codemirrorExternalPlugin = { + name: "codemirror-external", + setup(build) { + build.onResolve({ filter: /^@codemirror\/(state|view|language|autocomplete|commands|lint|search)$|^codemirror$/ }, (args) => { + return { path: args.path, namespace: "codemirror-external" }; + }); + build.onLoad({ filter: /.*/, namespace: "codemirror-external" }, (args) => { + return { + contents: `module.exports = acode.require('${args.path}');`, + loader: "js", + }; + }); + }, +}; + // Base build configuration let buildConfig = { entryPoints: ["src/main.ts"], @@ -33,7 +49,7 @@ let buildConfig = { logLevel: "info", color: true, outdir: "dist", - plugins: [zipPlugin, sassPlugin()], + plugins: [codemirrorExternalPlugin, zipPlugin, sassPlugin()], resolveExtensions: ['.ts', '.d.ts'] }; diff --git a/package.json b/package.json index d1e475e..efd0729 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,11 @@ "author": "Diki Djatar", "license": "MIT", "dependencies": { + "@codemirror/language": "^6.12.3", + "@codemirror/merge": "^6.7.0", + "@codemirror/state": "^6.6.0", + "@codemirror/view": "^6.43.4", + "codemirror": "^6.0.2", "diff": "5.1.0" }, "devDependencies": { diff --git a/src/git/diff.scss b/src/git/diff.scss index 388de91..39b1f5a 100644 --- a/src/git/diff.scss +++ b/src/git/diff.scss @@ -18,4 +18,24 @@ .ace_gutter-cell.gh-deleted-gutter { background-color: rgba(248, 81, 73, 0.302) !important; +} + +.codemirror-merge-view-container { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + overflow: hidden; + + .cm-editor { + height: 100% !important; + width: 100% !important; + + .cm-scroller { + overflow: auto; + } + } } \ No newline at end of file diff --git a/src/git/diff.ts b/src/git/diff.ts index 42df2b2..4168134 100644 --- a/src/git/diff.ts +++ b/src/git/diff.ts @@ -1,3 +1,7 @@ +import { EditorState } from '@codemirror/state'; +import { EditorView, lineNumbers, highlightActiveLineGutter, highlightActiveLine, drawSelection } from '@codemirror/view'; +import { foldGutter } from '@codemirror/language'; +import { unifiedMergeView } from '@codemirror/merge'; import * as Diff from 'diff'; import { App } from '../base/app'; import { UnsupportedError } from '../base/errors'; @@ -7,6 +11,9 @@ import { getModeForFile } from './utils'; const EditorFile = acode.require('EditorFile'); const Url = acode.require('Url'); const fsOperation = acode.require('fsOperation'); +const settings = acode.require('settings'); +const editorThemes = acode.require('editorThemes'); +const editorLanguages = acode.require('editorLanguages'); const Range = ace.require('ace/range')?.Range; type DiffEditorFile = Acode.EditorFile & { diff: { additions: number, deletions: number }; }; @@ -42,12 +49,6 @@ function isDiffEditorFile(file: unknown): file is DiffEditorFile { typeof (file).diff.deletions === 'number' } -function assertNotCodeMirror(): void { - if (App.isCodeMirror()) { - throw new UnsupportedError('UnifiedDiff is not supported in CodeMirror'); - } -} - export class UnifiedDiff { private readonly oldUri: string; @@ -61,7 +62,6 @@ export class UnifiedDiff { private deletions: number = 0; constructor(options: DiffOptions) { - assertNotCodeMirror(); this.oldUri = options.oldUri; this.newUri = options.newUri; this.title = options.title; @@ -70,9 +70,120 @@ export class UnifiedDiff { public async show(): Promise { const oldText = await fsOperation(this.oldUri).readFile('utf-8'); const newText = await fsOperation(this.newUri).readFile('utf-8'); - this.generateDiff(oldText, newText); - this.renderEditor(); + + if (App.isCodeMirror()) { + await this.showCodeMirrorDiff(oldText, newText); + } else { + this.generateDiff(oldText, newText); + this.renderEditor(); + this.updateStats(); + } + } + + private async showCodeMirrorDiff(oldText: string, newText: string): Promise { + const container = document.createElement('div'); + container.className = 'codemirror-merge-view-container'; + + // Calculate additions and deletions for stats + const diffs = Diff.diffLines(oldText, newText, { newlineIsToken: false }); + this.additions = 0; + this.deletions = 0; + diffs.forEach(diff => { + if (diff.added) this.additions += diff.count || 0; + if (diff.removed) this.deletions += diff.count || 0; + }); + + const settingsValue = settings.value as any; + const activeThemeId = settingsValue.editorTheme; + const themeEntry = editorThemes?.get(activeThemeId); + const themeExtensions = themeEntry && typeof (themeEntry as any).getExtension === 'function' ? (themeEntry as any).getExtension() : []; + + const getFontSettingsExtension = () => { + const fontSize = settingsValue.fontSize || '12px'; + const lineHeight = settingsValue.lineHeight || 1.5; + return EditorView.theme({ + '&': { fontSize, lineHeight: String(lineHeight) }, + '.cm-scroller': { + fontFamily: 'var(--editor-font-family, inherit)', + } + }); + }; + + const wrapExtension = settingsValue.textWrap ? [EditorView.lineWrapping] : []; + + const showLineNumbers = settingsValue.linenumbers !== false; + const lineNumberExtensions = showLineNumbers ? [lineNumbers(), highlightActiveLineGutter()] : []; + + const showFolding = settingsValue.codeFolding !== false; + const foldingExtensions = showFolding ? [foldGutter()] : []; + + const showActiveLine = settingsValue.highlightActiveLine !== false; + const activeLineExtensions = showActiveLine ? [highlightActiveLine()] : []; + + // Resolve language support for syntax highlighting + let languageExt: any = []; + const mode = editorLanguages?.getForPath(this.oldUri); + if (mode && typeof mode.languageExtension === 'function') { + try { + languageExt = await Promise.resolve(mode.languageExtension()); + } catch (e) { + console.error('Failed to resolve language extension for diff:', e); + } + } + + const editorExtensions = [ + ...(themeExtensions as any), + getFontSettingsExtension(), + ...wrapExtension, + ...lineNumberExtensions, + ...foldingExtensions, + ...activeLineExtensions, + drawSelection(), + ...(Array.isArray(languageExt) ? languageExt : [languageExt]), + unifiedMergeView({ + original: oldText, + mergeControls: false, + collapseUnchanged: { + margin: 3, + minSize: 4 + } + }), + EditorState.readOnly.of(true) + ]; + + const editorView = new EditorView({ + state: EditorState.create({ + doc: newText, + extensions: editorExtensions + }), + parent: container + }); + + const diffEditorFile = createDiffEditorFile(this.title, { + type: 'terminal', + content: container, + render: true, + isUnsaved: false, + editable: false + }); + + diffEditorFile.diff = { additions: this.additions, deletions: this.deletions }; this.updateStats(); + + const onSwitchFile = (file: any) => { + if (file === diffEditorFile) { + setTimeout(() => this.updateStats(), 0); + } + }; + + const onClose = () => { + editorView.destroy(); + editorManager.off('switch-file', onSwitchFile); + diffEditorFile.off('close', onClose); + }; + + editorManager.on('switch-file', onSwitchFile); + diffEditorFile.on('close', onClose); } private renderEditor(): void {