Skip to content

Wire autocomplete and syntax highlighting to ace editor#3828

Open
MengJit wants to merge 14 commits intomasterfrom
feat/conductor-autocomplete-highlighting
Open

Wire autocomplete and syntax highlighting to ace editor#3828
MengJit wants to merge 14 commits intomasterfrom
feat/conductor-autocomplete-highlighting

Conversation

@MengJit
Copy link
Copy Markdown

@MengJit MengJit commented May 6, 2026

Description

Sets up the conductor autocomplete plugin on the host side, including per-evaluator syntax highlighting via SyntaxHighlightData. Each evaluator registers its own ace mode id (e.g. ace/mode/PyCseEvaluator1) which the editor session subscribes to dynamically.

Key changes:

  • Host-side AutocompletePlugin receives SyntaxHighlightData from the runner and registers it with Ace via ace.define
  • Editor.tsx listens to changeSession to reapply the correct mode when react-ace swaps sessions (fixes startup highlighting)
  • directory.json evaluator ids updated to match bundle names

Paired PR: source-academy/py-slang#109

Co-authored-by: Aarav Malani aarav.malani@gmail.com

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update
  • Code quality improvements

How to test

Note: This PR requires the evaluator bundles from source-academy/py-slang#109 to be built and served locally. Clone that branch, run yarn build, and serve the dist/ folder on http://localhost:3000 before testing.

  1. Enable the conductor.enable feature flag in the app and set the directory accordingly. A sample directory.json file has been attached.
  2. Select a Python evaluator (PyCseEvaluator1, 2, 3, or 4) from the language dropdown
  3. Type Python code in the editor — keywords (def, return, if) should be highlighted immediately on page load without needing to switch evaluators
  4. Trigger autocomplete (Ctrl+Space or type a partial identifier) — suggestions should appear
  5. Switch between evaluators and confirm highlighting updates correctly each time

Checklist

  • I have tested this code
  • I have updated the documentation

directory.json

Set up the conductor autocomplete plugin host side, including per-evaluator
syntax highlighting via SyntaxHighlightData. Fixes startup highlighting by
listening to editor session swaps (changeSession) and reapplying the correct
ace mode. Eliminates duplicate conductor preload race by consolidating
preload to a single saga handler.

Co-authored-by: Aarav Malani <aarav.malani@gmail.com>
@MengJit MengJit changed the title feat(conductor): wire autocomplete and syntax highlighting to ace editor Wire autocomplete and syntax highlighting to ace editor May 6, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request integrates the Conductor framework for code evaluation and autocomplete, adding the @sourceacademy/autocomplete dependency and implementing an AutocompletePlugin. It introduces a caching mechanism for evaluators to reduce startup latency and updates the Editor and WorkspaceSaga to support these features. The review highlights the need for consistent indentation in WorkspaceSaga, suggests making the AutocompletePlugin language-agnostic by removing hardcoded imports, and advises ensuring unique IDs in ace.define to prevent potential collisions.

Comment thread src/commons/sagas/WorkspaceSaga/index.ts
import { require as acequire } from 'ace-builds/src-noconflict/ace';
import ace from 'ace-builds/src-noconflict/ace';
import { EventChannel, eventChannel, Unsubscribe } from 'redux-saga';
import 'ace-builds/src-noconflict/mode-python';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Hardcoding the import for mode-python in a generic AutoCompletePlugin seems incorrect. This plugin should ideally be language-agnostic, and specific modes should be loaded dynamically or provided by the evaluator.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed that hardcoding mode-python is not ideal for a fully language-agnostic plugin. However, this import is required because the runner's SyntaxHighlightData uses ace's existing Python mode helpers via hookFrom references (ace/mode/python, ace/mode/folding/pythonic) for indentation and folding rules. Without this import, acequire('ace/mode/python') returns undefined at runtime and loadMode throws.
A cleaner long-term solution would be for the runner to include all folding/indent logic as primitive regex rules in SyntaxHighlightData rather than referencing pre-existing ace modes via hookFrom, making the host truly language-agnostic. That is tracked as a future improvement. For now, this import is the minimal fix to make the feature work.

Comment on lines +68 to +74
ace.define(
data.id,
['exports', 'module'],
function (require: never, exports: { Mode: typeof Mode }) {
exports.Mode = Mode;
}
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using ace.define with data.id might lead to collisions if multiple evaluators use the same ID or if the ID is not sufficiently unique. Ensure that data.id is always unique per evaluator instance.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

data.id is guaranteed unique per evaluator by construction. It is set in the runner's mode.ts as ace/mode/${evaluatorName} where evaluatorName is the bundle name (e.g. PyCseEvaluator1, PyCseEvaluator2, etc.), passed explicitly through the constructor chain from each evaluator subclass. Since each bundle is a distinct worker with its own evaluator name hardcoded at build time, two evaluators cannot produce the same data.id. If the same evaluator is reloaded (e.g. language switch back), ace.define with the same id is idempotent — ace simply overwrites with an identical definition, which is harmless.

MengJit and others added 4 commits May 6, 2026 21:19
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
@coveralls
Copy link
Copy Markdown

coveralls commented May 6, 2026

Coverage Report for CI Build 25487350750

Coverage decreased (-0.3%) to 41.019%

Details

  • Coverage decreased (-0.3%) from the base build.
  • Patch coverage: 136 uncovered changes across 6 files (12 of 148 lines covered, 8.11%).
  • 3 coverage regressions across 2 files.

Uncovered Changes

File Changed Covered %
src/commons/sagas/helpers/conductorEvaluatorCache.ts 53 5 9.43%
src/features/conductor/AutocompletePlugin.ts 39 0 0.0%
src/commons/editor/Editor.tsx 34 5 14.71%
src/commons/sagas/WorkspaceSaga/index.ts 14 0 0.0%
src/commons/sagas/LanguageDirectorySaga.ts 7 2 28.57%
src/features/conductor/createConductor.ts 1 0 0.0%

Coverage Regressions

3 previously-covered lines in 2 files lost coverage.

File Lines Losing Coverage Coverage
src/commons/sagas/WorkspaceSaga/index.ts 2 50.91%
src/features/conductor/createConductor.ts 1 0.0%

Coverage Stats

Coverage Status
Relevant Lines: 14326
Covered Lines: 6339
Line Coverage: 44.25%
Relevant Branches: 7303
Covered Branches: 2533
Branch Coverage: 34.68%
Branches in Coverage %: Yes
Coverage Strength: 28.25 hits per line

💛 - Coveralls

Copy link
Copy Markdown
Member

@RichDom2185 RichDom2185 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the unused commented code

Comment thread src/commons/editor/Editor.tsx Outdated
Comment thread src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds host-side wiring for Conductor-backed autocomplete and dynamic Ace syntax highlighting modes, enabling per-evaluator highlighting (e.g. ace/mode/PyCseEvaluator1) and using a preloaded Conductor instance to reduce latency.

Changes:

  • Add @sourceacademy/autocomplete dependency and register a new host-side AutoCompletePlugin with Conductor.
  • Route editor autocomplete through the Conductor runner (with a timeout) when conductor.enable is on.
  • Introduce a Conductor evaluator preload/cache helper and update the editor to re-apply the Ace mode on changeSession.

Reviewed changes

Copilot reviewed 8 out of 9 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
yarn.lock Adds lockfile entry for @sourceacademy/autocomplete GitHub dependency.
package.json Adds @sourceacademy/autocomplete dependency pin.
src/features/conductor/createConductor.ts Registers the new autocomplete/syntax-highlighting plugin on the host side.
src/features/conductor/AutocompletePlugin.ts Implements host-side plugin that registers Ace modes from SyntaxHighlightData and exposes complete().
src/commons/sagas/WorkspaceSaga/index.ts Uses Conductor plugin for autocomplete requests when enabled.
src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts Switches evaluation to use the preloaded Conductor instance.
src/commons/sagas/LanguageDirectorySaga.ts Preloads the Conductor evaluator when a language is selected.
src/commons/sagas/helpers/conductorEvaluatorCache.ts New helper that caches/preloads a Conductor instance for the selected evaluator.
src/commons/editor/Editor.tsx Applies dynamic Ace modes on session changes while Conductor is enabled.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +492 to +498
if (!modeModule?.Mode) return false;
if ((session.getMode() as any).$id === modeId) return true;
session.setMode(new modeModule.Mode());
return true;
};

const attachToSession = (session: any) => {
Comment on lines +171 to +210
if (yield select(selectConductorEnable)) {
const { conduit }: { hostPlugin: BrowserHostPlugin; conduit: IConduit } =
yield call(getPreparedConductorSaga);

const plugin = conduit.lookupPlugin('__autocomplete_plugin_web') as AutoCompletePlugin;
if (plugin) {
const channel: EventChannel<AutoCompleteEntry[]> = yield call(
[plugin, 'complete'],
autocompleteCode,
action.payload.row + prependLength,
action.payload.column
);
//const names: AutoCompleteEntry[] = yield take(channel);
const { names, timeout }: { names?: AutoCompleteEntry[]; timeout?: true } = yield race({
names: take(channel),
timeout: delay(3000)
});

if (timeout || !names) {
console.warn('autocomplete channel timed out — runner never replied');
channel.close();
return;
}

yield call(
action.payload.callback,
null,
names.map(name => ({
meta: name.meta,
value: name.name,
caption: name.name,
docHTML: name.docHTML,
score: name.score ? name.score + 1000 : 1000, // Prioritize suggestions from code
name: undefined
}))
);
channel.close();
}
return;
}
Comment on lines +89 to +105
// A new evaluator path is requested, so release the old preloaded conductor first.
yield call(cleanupPreparedConductorSaga);

loadingConductorPath = path;
loadingConductorPromise = createPreparedConductor(path)
.then(prepared => {
preparedConductorPath = path;
preparedConductor = prepared;
return prepared;
})
.finally(() => {
loadingConductorPath = null;
loadingConductorPromise = null;
});

return yield call(() => loadingConductorPromise as Promise<PreparedConductor>);
}
Comment on lines +132 to +140
if (files) {
prepared.setFiles(files);
}

// Consume only when requested (e.g. for program evaluation, not autocomplete requests).
if (consume && preparedConductor === prepared) {
resetPreparedConductor();
}

Comment on lines +120 to +129
export function* getPreparedConductorSaga(
options?: GetPreparedConductorOptions
): SagaIterator<{ hostPlugin: BrowserHostPlugin; conduit: IConduit }> {
if (!currentEvaluatorPath) {
throw Error('no evaluator path selected');
}

const path = currentEvaluatorPath;
const prepared: PreparedConductor = yield call(ensurePreparedConductorSaga, path);
const files = options?.files;
Comment on lines 47 to 65
[LanguageDirectoryActions.setSelectedLanguage.type]: function* () {
const language = yield call(getLanguageDefinitionSaga);
if (!language) return;
if (language.evaluators.length > 0) {
yield put(LanguageDirectoryActions.setSelectedEvaluator(language.evaluators[0].id));
}

const conductorEnabled: boolean = yield select(selectConductorEnable);
if (!conductorEnabled) return;

const evaluator = yield call(getEvaluatorDefinitionSaga);
if (!evaluator?.path) return;

try {
yield call(preloadConductorEvaluatorSaga, evaluator.path);
} catch (error) {
console.error('Failed to preload:', error);
}
}
Comment thread src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts
Comment on lines +86 to +89
this.autocomplete(code, row, column, entries => {
emit(entries.declarations);
});
return () => {};
Comment thread src/commons/sagas/WorkspaceSaga/index.ts Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@RichDom2185
Copy link
Copy Markdown
Member

@MengJit I think some of the comments may be valid, please check and resolve if invalid, thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants