A desktop code editor can render syntax colors after loading Monaco, but that does not make it an IDE. Useful language intelligence depends on project context, unsaved buffers, local toolchains, cross-file edits, and failure states that the user can understand.
While building PAUITerm, I wanted the editor inside the terminal workspace to remain lightweight without pretending Monaco could provide every language feature by itself. The solution is a layered system: Monaco handles the languages it knows in the browser context, while project-scoped local Language Server Protocol processes provide semantic features for other toolchains.
This article explains that bridge and the decisions that keep it predictable.
Monaco and a language server solve different problems
Monaco Editor is the editor used by VS Code, but VS Code's complete extension host and language ecosystem are not automatically included when Monaco is embedded in another application.
Monaco ships useful workers for JavaScript, TypeScript, JSON, HTML, CSS, SCSS, and Less. It can tokenize many other formats, but syntax highlighting is not the same as understanding a project. Go-to-definition in Go, a Rust compiler diagnostic, or a Python auto-import normally comes from a language server or extension.
Language Server Protocol provides a common message format between an editor and a language-specific server. PAUITerm uses that boundary instead of implementing a parser and semantic engine for every language.
The project root is part of every request
A language server needs more than the current filename. It resolves modules, dependencies, build configuration, and symbols relative to a workspace.
PAUITerm starts the local server with the selected project's root and keeps the process scoped to that project. The editor sends document-open and document-change events, including text that has not been saved yet. That allows diagnostics and completion to reflect what the user currently sees rather than only the last version on disk.
Project scoping also prevents one workspace from leaking symbol results or configuration into another. When the project changes or the server exits, the associated client state is cleaned up instead of being silently reused.
Server discovery must explain failure
The application searches the user's login-shell PATH for an appropriate executable. Examples include rust-analyzer, gopls, clangd, sourcekit-lsp, Python language servers, and language servers for Java, C#, Kotlin, Dart, Lua, Shell, Ruby, PHP, YAML, and Dockerfiles.
Finding an executable is not proof that it can start. A Rust toolchain shim may exist while the actual component is missing; a server may exit because the project configuration is invalid.
The editor status therefore names the active server or shows Syntax only with a reason. That small piece of UI matters. It distinguishes “this file has no configured semantic provider” from “the feature is loading” and from “the server failed.” Without that distinction, users repeatedly invoke a completion command that can never succeed.
Capability negotiation prevents fake features
Not every language server implements every LSP method. After initialization, the server advertises what it supports. PAUITerm only wires up commands for capabilities that are actually available.
Depending on the server, the bridge can provide:
- semantic completions and additional auto-import edits,
- hover details and signature help,
- definition, type definition, and implementation navigation,
- references, document highlights, and outline symbols,
- diagnostics,
- document and range formatting,
- prepare-rename and rename edits,
- code actions that contain concrete workspace edits.
This avoids showing a feature as successful when the server never supported it. It also leaves a clean path for syntax-only editing when no local server is installed.
Cross-file edits require a stricter boundary
Navigation is relatively safe: when a definition points to another file inside the selected project, PAUITerm opens that file and moves to the target range.
Rename is more consequential. A server may return edits for several files, including files that are not open. PAUITerm applies text edits inside the project and writes changes to unopened files, similar to VS Code's refactoring auto-save behavior. Edits that would create, rename, move, or delete files are not applied by this bridge, and server actions that contain only an opaque command are also excluded.
Those limits are intentional. Supporting a protocol method is not enough; the host application must understand the full consequence of an edit before applying it. A smaller, explicit set of workspace operations is safer than partially interpreting a complex response.
Formatting uses a visible order of precedence
Different projects expect different formatters. Running whichever binary happens to be installed first creates surprising diffs.
PAUITerm uses this order:
- The active language server, when it advertises formatting.
- A language-specific command-line formatter, such as
gofmt,rustfmt, Ruff or Black,clang-format,shfmt, or a project-local Prettier. - Monaco's built-in formatter for supported web languages.
Command-line formatters receive the unsaved buffer through standard input. A syntax error or formatter failure leaves the buffer unchanged and produces an error the user can act on. Nothing is written to disk until the editor saves.
Format on Save also runs only for an explicit save, not for automatic background saves. This prevents an auto-save loop from repeatedly rewriting a document while the user is in the middle of an edit.
Syntax support still has independent value
Not every language needs a live server to be readable. PAUITerm extends Monaco tokenization for formats and languages commonly encountered in infrastructure work: Makefiles and Justfiles, ignore files, Diff and Patch, TOML, nginx configuration, CMake, Nix, Zig, LaTeX, logs, and several others.
Files without an extension can be recognized from their shebang. JSON variants such as tsconfig.json and files under .vscode can allow comments where the surrounding toolchain expects them.
This layered approach means the editor degrades usefully. If a semantic server is unavailable, the file still has appropriate syntax support and the status tells the user what is missing.
The embedded browser exposed a related desktop problem
PAUITerm also includes an in-app browser built on the native web view. That work reinforced the same lesson: a desktop shell must translate platform state into understandable product state.
An unreachable local origin needs a real error surface, not a blank pane. “Open externally” must be separate from in-app navigation. When a browser overlay or dialog closes, keyboard focus has to return to the expected terminal or editor surface. These are not browser-engine features; they are integration responsibilities owned by the desktop application.
What I would reuse in another editor
The implementation produced a few rules that apply beyond PAUITerm:
- Do not describe syntax highlighting as language intelligence. Name the active provider and its limits.
- Send the current buffer, not only the saved file. Users expect diagnostics to match the text on screen.
- Scope servers and navigation to the selected project. Workspace boundaries are also safety boundaries.
- Negotiate capabilities. A command should exist because the server can perform it, not because another language server can.
- Treat workspace edits as file operations. Cross-file rename deserves stricter validation than a single-buffer text edit.
- Make fallback behavior explicit. “Syntax only” is a valid mode when the interface explains why.
Embedding Monaco was the beginning of the editor, not the end. The useful experience comes from coordinating local processes, unsaved state, project boundaries, formatter precedence, and honest error reporting. That coordination is the real language-intelligence feature.
Comments
0 verified commentsNo comments yet. Use “Comment now” to begin the discussion.