Feat: updates to docs, extension

This commit is contained in:
Jonathan Kelley 2021-01-29 11:57:52 -05:00
parent 2e626aea51
commit a2406b33d6
25 changed files with 917 additions and 508 deletions

View file

@ -1,3 +1,3 @@
{
"rust-analyzer.inlayHints.enable": false
"rust-analyzer.inlayHints.enable": true
}

View file

@ -6,3 +6,8 @@ Dioxus
VDoms
Tauri
webview
polyfills
transpiling
fullstack
Serverless
vnode

View file

@ -10,7 +10,7 @@ members = [
"packages/router",
"packages/ssr",
"packages/webview",
# Not a rust package, :(. One day, maybe
"packages/livehost",
"packages/vscode-ext",
# "packages/macro",
# TODO @Jon, share the validation code

View file

@ -9,13 +9,15 @@
Dioxus is a new approach for creating performant cross platform user experiences in Rust. In Dioxus, the UI is represented as a tree of Virtual Nodes not bound to any specific renderer. Instead, external renderers can leverage Dioxus' virtual DOM and event system as a source of truth for rendering to a medium of their choice. Developers experienced with building react-based experiences should feel comfortable with Dioxus.
Dioxus is unique in the space of UI for Rust. Dioxus supports a renderer approach called "broadcasting" where two VDoms with separate renderers can sync their UI states remotely. Our goal as a framework is to work towards "Dioxus Liveview" where a server and client work in tandem, eliminating the need for frontend-specific APIs altogether.
Dioxus was built in a way to facilitate powerful external renderers - especially designed for the web, servers, desktop, and hybrid approaches like Dioxus Liveview.
Dioxus is supported by Dioxus Labs, a company providing end-to-end services for building, testing, deploying, and managing Dioxus apps on all supported platforms.
## Features
Dioxus' goal is to be the most advanced UI system for Rust, targeting isomorphism and hybrid approaches. Our goal is to eliminate context-switching for cross-platform development - both in UI patterns and programming language. Hooks and components should work *everywhere* without compromise.
Dioxus Core supports:
- [ ] Hooks
- [ ] Hooks for component state
- [ ] Concurrent rendering
- [ ] Context subscriptions
- [ ] State management integrations
@ -33,7 +35,7 @@ On top of these, we have several projects you can find in the `packages` folder.
- [ ] `dioxus-android`: Android apps
- [ ] `dioxus-magic`: AR/VR Apps
## Hello World
## Components
Dioxus should look and feel just like writing functional React components. In Dioxus, there are no class components with lifecycles. All state management is done via hooks. This encourages logic reusability and lessens the burden on Dioxus to maintain a non-breaking lifecycle API.
```rust
@ -42,7 +44,7 @@ struct MyProps {
name: String
}
fn Example(ctx: &Context<MyProps>) -> VNode {
async fn Example(ctx: &Context<MyProps>) -> VNode {
html! { <div> "Hello {ctx.props.name}!" </div> }
}
```
@ -52,36 +54,67 @@ Here, the `Context` object is used to access hook state, create subscriptions, a
```rust
// A very terse component!
#[fc]
fn Example(ctx: &Context, name: String) -> VNode {
async fn Example(ctx: &Context, name: String) -> VNode {
html! { <div> "Hello {name}!" </div> }
}
// or
#[functional_component]
static Example: FC = |ctx, name: String| html! { <div> "Hello {:?name}!" </div> };
static Example: FC = |ctx, name: String| async html! { <div> "Hello {name}!" </div> };
```
The final output of components must be a tree of VNodes. We provide an html macro for using JSX-style syntax to write these, though, you could use any macro, DSL, templating engine, or the constructors directly.
## Concurrency
Dioxus, using React as a reference, provides the ability to have asynchronous components. With Dioxus, this is a valid component:
In Dioxus, components are asynchronous and can their rendering can be paused at any time by awaiting a future. Hooks can combine this functionality with the Context and Subscription APIs to craft dynamic and efficient user experiences.
```rust
async fn user_data(ctx: &Context<()>) -> VNode {
let Profile { name, birthday, .. } = use_context::<UserContext>(ctx).fetch_data().await;
let Profile { name, birthday, .. } = fetch_data().await;
html! {
<div>
{"Hello, {:?name}!"}
{"Hello, {name}!"}
{if birthday === std::Instant::now() {html! {"Happy birthday!"}}}
</div>
}
}
```
Asynchronous components are powerful but can also be easy to misuse as they pause rendering for the component and its children. Refer to the concurrent guide for information on how to best use async components.
## Liveview
With the Context, Subscription, and Asynchronous APIs, we've built Dioxus Liveview: a coupling of frontend and backend to deliver user experiences that do not require dedicated API development. Instead of building and maintaining frontend-specific API endpoints, components can directly access databases, server caches, and other services directly from the component.
These set of features are still experimental. Currently, we're still working on making these components more ergonomic
```rust
async fn live_component(ctx: &Context<()>) -> VNode {
use_live_component(
ctx,
// Rendered via the client
#[cfg(target_arch = "wasm32")]
|| html! { <div> {"Loading data from server..."} </div> },
// Renderered on the server
#[cfg(not(target_arch = "wasm32"))]
|| html! { <div> {"Server Data Loaded!"} </div> },
)
}
```
## Dioxus LiveHost
Dioxus LiveHost is a paid service dedicated to hosting your Dioxus Apps - whether they be server-rendered, wasm-only, or a liveview. LiveHost enables a wide set of features:
- Versioned fronted/backend with unique links
- Builtin CI/CD for all supported Dioxus platforms (mac, windows, server, wasm, etc)
- Managed and pluggable storage database backends
- Serverless support for minimal latency
- Analytics
- Lighthouse optimization
- On-premise support (see license terms)
For small teams, LiveHost is free. Check out the pricing page to see if Dioxus LiveHost is good your team.
## Examples
We use the dedicated `dioxus-cli` to build and test dioxus web-apps. This can run examples, tests, build web workers, launch development servers, bundle, and more. It's general purpose, but currently very tailored to Dioxus for liveview and bundling. If you've not used it before, `cargo install --path pacakages/dioxus-cli` will get it installed. This CLI tool should feel like using `cargo` but with 1st party support for assets, bundling, and other important dioxus-specific features.
@ -97,5 +130,5 @@ Alternatively, `trunk` works but can't run examples.
- twitter-clone: A full-featured Twitter clone showcasing dioxus-liveview, state management patterns, and hooks. `cargo run --example twitter`
## Documentation
We have a pretty robust

View file

@ -1,20 +1,21 @@
# Dioxus Chapter 0 - Intro + Index
## Guides
------------------
| Chapter | Description |
| -------------- | ------------------------------------------ |
| 1-hello-world | Intro to Dioxus |
| 2-utilities | Tools to make writing apps easier |
| 3-hello-world | Html + functional_component macro |
| 4-hello-world | Renderer + event integration using web_sys |
| 5-hello-world | Virtual DOM, patching/diffing |
| 6-hello-world | Standard bundle of useful hooks |
| 7-hello-world | Html + functional_component macro |
| 8-hello-world | Renderer + event integration using web_sys |
| 9-hello-world | Renderer + event integration using web_sys |
| 10-hello-world | Renderer + event integration using web_sys |
| Chapter | Description |
| ------------------ | ------------------------------------------ |
| 1-hello-world | Intro to Dioxus |
| 2-utilities | Tools to make writing apps easier |
| 3-vnode-macros | VNodes and the html! macro |
| 4-hooks | Renderer + event integration using web_sys |
| 5-context-api | Virtual DOM, patching/diffing |
| 6-subscription API | Standard bundle of useful hooks |
| 7-hello-world | Html + functional_component macro |
| 8-hello-world | Blessed state management solutions |
| 9-hello-world | Renderer + event integration using web_sys |
| 10-hello-world | Renderer + event integration using web_sys |
## Development
------------------
@ -30,3 +31,7 @@
| redux | Reducer-style state management |
| bearly | Simple and idiomatic state management |
## Books
---------------
Book of patterns

32
docs/12-liveview.md Normal file
View file

@ -0,0 +1,32 @@
# Dioxus Liveview
Liveview is a configuration where a server and a client work together to render a Dioxus app. Liveview monomorphizes a web application, eliminating the need for frontend-specific APIs.
This is a developer-friendly alternative to the JAM-stack (Javascript + API + Markdown), combining the WASM-compatibility and async performance of Rust.
## Why liveview?
### No APIs necessary!
Because Liveview combines the server and the client, you'll find dedicated APIs unnecessary. You'll still want to implement a datafetching service for Live-apps, but this can be implemented as a crate and shared between apps. This approach was designed to let you model out your data requirements without needing to maintain a public versioned API.
You can find more information to data modeling and fetching for LiveApps in the "Book of Dioxus Patterns".
### Ease of deployment
There is no hassle for deploying Dioxus liveview apps - simply upload the binary to your hosting provider of choice. There simply is no need to deal with intermediate APIs. An app's frontend and backend are tightly coupled to each other, meaning that the backed and frontend are always up to date.
This is especially powerful for small teams, where fast iteration, versioned unique prototypes, and simple deployments are important. As your app grows, Dioxus will happily grow with you, serving up to 100K RPS (thanks to async Rust performance).
### Power of server rendering combined with the reactive nature of JavaScript
Liveview apps are backed by a server and enhanced with web-assembly. This completely eliminates the need to write Javascript to add dynamic content to webpages. Apps are written in only **one** language and require essentially 0 knowledge of build systems, transpiling or ECMAScript versions. Minimum browser support is guaranteed to cover 95% of users, and the Dioxus-CLI can be configured to generate polyfills for even more support.
### Support with LiveHost by Dioxus-Labs
The Dioxus-CLI comes ready for use with premium Dioxus-Labs services, like LiveHost. Simply link up your git repo to the Dioxus LiveHost and start deploying your fullstack Dioxus app today. LiveHost supports:
- Versioned fronted/backend with unique links
- Builtin CI/CD
- Managed and pluggable storage database backends
- Serverless support for minimal latency
- Site Analytics
- Lighthouse optimization
- On-premise support (see license terms)
Dioxus LiveHost is a Dioxus LiveApp management service with all supported features in a single binary. For OSS, we permit free usage and deployment of LiveHost to your favorite hosting provider.

View file

@ -20,15 +20,27 @@ anyhow = "*"
async-std = { version = "1.9.0", features = ["attributes"] }
tide = { version = "0.15.0" }
# For the livewview example
tide-websockets = "0.1.0"
serde_millis = "0.1"
serde_json = "1"
serde = { version = "1", features = ['derive'] }
# For the doc generator
pulldown-cmark = { version = "0.8.0", default-features = false }
dioxus-webview = { path = "../packages/webview", version = "0.0.0" }
dioxus-hooks = { path = "../packages/hooks", version = "0.0.0" }
# Shared functionality is done as a lib
[lib]
path = "common.rs"
# ================================
# Examples are manually keyed in
# ================================
[[example]]
path = "hello_web.rs"
name = "hello_web"
@ -52,3 +64,11 @@ name = "fc_macro"
[[example]]
path = "webview.rs"
name = "webview"
[[example]]
path = "blah.rs"
name = "blah"
[[example]]
path = "live.rs"
name = "live"

9
examples/blah.rs Normal file
View file

@ -0,0 +1,9 @@
use dioxus::prelude::*;
fn main() {
let g = html! {
<div>
<style> </style>
</div>
};
}

65
examples/live.rs Normal file
View file

@ -0,0 +1,65 @@
use dioxus::prelude::*;
use tide::{with_state, Request};
use tide_websockets::{WebSocket, WebSocketConnection};
fn main() {
let mut g = Context { props: &() };
LiveComponent(&mut g);
}
#[cfg(not(target_arch = "wasm32"))]
async fn server() -> tide::Result<()> {
// Build the API
let mut app = tide::with_state(());
app.at("/app").get(WebSocket::new(live_handler));
// Launch the server
app.listen("127.0.0.1:8080").await?;
Ok(())
}
async fn live_handler(req: Request<()>, stream: WebSocketConnection) -> tide::Result<()> {
Ok(())
}
static LiveComponent: FC<()> = |ctx| {
use_live_component(
ctx,
#[cfg(target_arch = "wasm32")]
|| {
// Always wait on the context's live component API
// suspend the component until this promise arrives, or fall back
let g = &LiveComponent;
html! {
<div>
{"Hello world!"}
</div>
}
},
#[cfg(not(target_arch = "wasm32"))]
|| {
// actually use the code originally specified in the component
// this gives use the function pointer. We don't necessarily get to hash the same pointer between two binaries
// Some key will need to be made, likely based on the function parameter
let g = &LiveComponent;
html! {
<div>
{"Hello world!"}
</div>
}
},
)
};
/// This hooks connects with the LiveContext at the top of the app.
fn use_live_component<T>(ctx: &mut Context<T>, b: fn() -> VNode) -> VNode {
todo!()
}
/// LiveContext is a special, limited form of the context api that disallows the "Context API"
/// Its purpose is to shield the original Context where calls to use_context will fail. Instead of panicing and confusing
/// users, we simply disallow usage of "use_context" and "childen". In effect, only serialiable props can be allowed.
///
/// In the future, we might try to lift these restrictions (esp children since they are virtual) but are limited via the web connection
struct LiveContext {}

View file

@ -26,5 +26,7 @@ fn main() {
}
});
let f = 10_i32;
println!("hello {}", f);
app.launch(());
}

View file

@ -1,6 +1,6 @@
use std::future::Future;
use dioxus_core::prelude::*;
use dioxus_core::{prelude::*, virtual_dom::Properties};
// use virtual_dom_rs::Closure;
// Stop-gap while developing
@ -13,9 +13,11 @@ pub fn main() {
// let output = renderer.render();
}
#[derive(PartialEq)]
struct Props {
name: String,
}
impl Properties for Props {}
fn root(ctx: &mut Context<Props>) -> VNode {
// the regular html syntax
@ -39,10 +41,17 @@ fn root(ctx: &mut Context<Props>) -> VNode {
}
let mut node_1: IterableNodes = ("Hello world!").into();
node_1.first().insert_space_before_text();
let mut node_2 = VNode::element("button");
let node_3 = VNode::Component(VComponent {});
let node_3 = VNode::Component(VComponent::from_fn(
Head,
Props {
name: "".to_string(),
},
));
{
// let closure = Closure::wrap(Box::new(|_| {}) as Box<FnMut(_)>);
// let closure_rc = std::rc::Rc::new(closure);
@ -67,17 +76,13 @@ fn root(ctx: &mut Context<Props>) -> VNode {
fn Head(ctx: &mut Context<Props>) -> VNode {
html! {
<head>
"Head Section"
</head>
<head> "Head Section" </head>
}
}
fn Body(ctx: &mut Context<Props>) -> VNode {
html! {
<body>
{"Footer Section"}
</body>
<body> {"Footer Section"}</body>
}
}

View file

@ -6,8 +6,6 @@
//! </div>
//! Dioxus: a concurrent, functional, reactive virtual dom for any renderer in Rust.
//!
//!
//! Dioxus is an efficient virtual dom implementation for building interactive user interfaces in Rust.
//! This crate aims to maintain a uniform hook-based, renderer-agnostic UI framework for cross-platform development.
//!
//! ## Components
@ -15,14 +13,38 @@
//! ```
//! use dioxus_core::prelude::*;
//!
//! fn Example(ctx: Context<()>) -> VNode {
//! html! { <div> "Hello world!" </div> }
//! #[derive(Properties)]
//! struct Props { name: String }
//!
//! fn Example(ctx: &mut Context<Props>) -> VNode {
//! html! { <div> "Hello {ctx.props.name}!" </div> }
//! }
//! ```
//! Components need to take a "Context" parameter which is generic over some properties. This defines how the component can be used
//! and what properties can be used to specify it in the VNode output. All components in Dioxus are hook-based, which might be more
//! complex than other approaches that use traits + lifecycle events. Alternatively, we provide a "lifecycle hook" if you want more
//! granualar control with behavior similar to other UI frameworks.
//! and what properties can be used to specify it in the VNode output. Component state in Dioxus is managed by hooks - if you're new
//! to hooks, check out the hook guide in the official guide.
//!
//! Components can also be crafted as static closures, enabling type inference without all the type signature noise:
//! ```
//! use dioxus_core::prelude::*;
//!
//! #[derive(Properties)]
//! struct Props { name: String }
//!
//! static Example: FC<Props> = |ctx| {
//! html! { <div> "Hello {ctx.props.name}!" </div> }
//! }
//! ```
//!
//! If the properties struct is too noisy for you, we also provide a macro that converts variadic functions into components automatically.
//! ```
//! use dioxus_core::prelude::*;
//!
//! #[functional_component]
//! static Example: FC = |ctx, name: String| {
//! html! { <div> "Hello {name}!" </div> }
//! }
//! ```
//!
//! ## Hooks
//! Dioxus uses hooks for state management. Hooks are a form of state persisted between calls of the function component. Instead of
@ -177,7 +199,12 @@ pub mod virtual_dom {
}
}
pub trait Properties {}
pub trait Properties: PartialEq + 'static {
/*
Props cannot contain references (as of today) :(
Use a smart pointer
*/
}
// Auto derive for pure components
impl Properties for () {}
@ -187,8 +214,9 @@ pub mod virtual_dom {
}
/// Virtual Node Support
/// VNodes represent lazily-constructed VDom trees that support diffing and event handlers.
///
///
/// These VNodes should be *very* cheap and *very* fast to construct - building a full tree should be insanely quick.
///
pub mod nodes {
pub use vcomponent::VComponent;
@ -528,13 +556,52 @@ pub mod nodes {
/// Virtual Components for custom user-defined components
/// Only supports the functional syntax
mod vcomponent {
use crate::{
prelude::{Context, FC},
virtual_dom::Properties,
};
use std::{any::TypeId, fmt, future::Future};
use super::VNode;
#[derive(PartialEq)]
pub struct VComponent {}
pub struct VComponent {
props_id: TypeId,
// callerIDs are unsafely coerced to function pointers
// This is okay because #1, we store the props_id and verify and 2# the html! macro rejects components not made this way
//
// Manually constructing the VComponent is not possible from 3rd party crates
}
impl VComponent {
/// Construct a VComponent directly from a function component
/// This should be *very* fast - we store the function pointer and props type ID. It should also be small on the stack
pub fn from_fn<P: Properties>(f: FC<P>, props: P) -> Self {
// Props needs to be static
let props_id = std::any::TypeId::of::<P>();
// Cast the caller down
Self { props_id }
}
/// Construct a VComponent directly from a concurrent component
pub fn from_asyncfn<P: Properties, F>(f: fn(&mut Context<P>) -> F, props: P) -> Self
where
F: Future<Output = VNode>,
{
// Props needs to be static
let props_id = std::any::TypeId::of::<P>();
// Cast the caller down
Self { props_id }
}
}
// -----------------------------------------------
// Allow debug/display adherent to the HTML spec
// -----------------------------------------------
use std::fmt;
impl fmt::Debug for VComponent {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// TODO: @JON Implement how components should be formatted when spit out to html

View file

@ -0,0 +1,57 @@
use dioxus_core::prelude::*;
fn main() {
let mut s = Context { props: &() };
let g = Component(&mut s);
}
struct CompState {
tasks: Vec<()>,
}
enum Actions {
Add,
MoveUp,
MoveDown,
Remvoe,
}
static Component: FC<()> = |ctx| {
let (tasks, dispatch) = use_reducer(
ctx,
|| CompState { tasks: Vec::new() },
|state, action: Actions| match action {
Actions::Add => state,
Actions::MoveUp => state,
Actions::MoveDown => state,
Actions::Remvoe => state,
},
);
let tasklist = { (0..10).map(|f| html! { <li></li> }) }.collect::<Vec<_>>();
html! {
<div>
<div>
<h1>"Tasks: "</h1>
<ul>
{tasklist}
</ul>
</div>
<div>
<button onclick=|_| dispatch(Action::Add)>{"Add"}</button>
<button onclick=|_| dispatch(Action::MoveUp)>{"MoveUp"}</button>
<button onclick=|_| dispatch(Action::MoveDown)>{"MoveDown"}</button>
<button onclick=|_| dispatch(Action::Remvoe)>{"Remvoe"}</button>
</div>
</div>
}
};
fn use_reducer<Props, State, Action>(
ctx: &mut Context<Props>,
init: fn() -> State,
reducer: fn(State, Action) -> State,
) -> (State, impl Fn(Action)) {
let ii = init();
(ii, |_| {})
}

View file

@ -0,0 +1,9 @@
[package]
name = "dioxus-livehost"
version = "0.0.0"
authors = ["Jonathan Kelley <jkelleyrtp@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View file

@ -0,0 +1,3 @@
fn main() {
println!("Hello, world!");
}

View file

@ -57,6 +57,11 @@
"vscode-languageserver-types": "3.15.1"
}
},
"vscode-languageserver-textdocument": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.1.tgz",
"integrity": "sha512-UIcJDjX7IFkck7cSkNNyzIz5FyvpQfY7sdzVy+wkKN/BLaD4DQ0ppXQrKePomCxTS7RrolK1I0pey0bG9eh8dA=="
},
"vscode-languageserver-types": {
"version": "3.15.1",
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.15.1.tgz",

View file

@ -14,7 +14,8 @@
},
"dependencies": {
"vscode-html-languageservice": "^3.0.3",
"vscode-languageclient": "^6.1.3"
"vscode-languageclient": "^6.1.3",
"vscode-languageserver-textdocument": "^1.0.1"
},
"devDependencies": {
"@types/vscode": "^1.43.0"

View file

@ -3,413 +3,447 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { TextDocument, Position, Range } from 'vscode-languageclient';
import { LanguageService, TokenType } from 'vscode-html-languageservice';
import { TextDocument } from "vscode-languageserver-textdocument";
import { Position, Range } from "vscode-languageclient";
import { LanguageService, TokenType } from "vscode-html-languageservice";
export interface LanguageRange extends Range {
languageId: string | undefined;
attributeValue?: boolean;
languageId: string | undefined;
attributeValue?: boolean;
}
export interface HTMLDocumentRegions {
getEmbeddedDocument(languageId: string, ignoreAttributeValues?: boolean): TextDocument;
getLanguageRanges(range: Range): LanguageRange[];
getLanguageAtPosition(position: Position): string | undefined;
getLanguagesInDocument(): string[];
getImportedScripts(): string[];
getEmbeddedDocument(
languageId: string,
ignoreAttributeValues?: boolean
): TextDocument;
getLanguageRanges(range: Range): LanguageRange[];
getLanguageAtPosition(position: Position): string | undefined;
getLanguagesInDocument(): string[];
getImportedScripts(): string[];
}
export const CSS_STYLE_RULE = '__';
export const CSS_STYLE_RULE = "__";
interface EmbeddedRegion {
languageId: string | undefined;
start: number;
end: number;
attributeValue?: boolean;
languageId: string | undefined;
start: number;
end: number;
attributeValue?: boolean;
}
// Check if the request is coming from inside a special region
export function isInsideStyleRegion(
languageService: LanguageService,
documentText: string,
offset: number
languageService: LanguageService,
documentText: string,
offset: number
) {
let scanner = languageService.createScanner(documentText);
let scanner = languageService.createScanner(documentText);
let token = scanner.scan();
while (token !== TokenType.EOS) {
switch (token) {
case TokenType.Styles:
if (offset >= scanner.getTokenOffset() && offset <= scanner.getTokenEnd()) {
return true;
}
}
token = scanner.scan();
}
return false;
let token = scanner.scan();
while (token !== TokenType.EOS) {
switch (token) {
case TokenType.Styles:
if (
offset >= scanner.getTokenOffset() &&
offset <= scanner.getTokenEnd()
) {
return true;
}
}
token = scanner.scan();
}
return false;
}
export function getCSSVirtualContent(
languageService: LanguageService,
documentText: string
languageService: LanguageService,
documentText: string
): string {
let regions: EmbeddedRegion[] = [];
let scanner = languageService.createScanner(documentText);
let lastTagName: string = '';
let lastAttributeName: string | null = null;
let languageIdFromType: string | undefined = undefined;
let importedScripts: string[] = [];
let regions: EmbeddedRegion[] = [];
let scanner = languageService.createScanner(documentText);
let lastTagName: string = "";
let lastAttributeName: string | null = null;
let languageIdFromType: string | undefined = undefined;
let importedScripts: string[] = [];
let token = scanner.scan();
while (token !== TokenType.EOS) {
switch (token) {
case TokenType.StartTag:
lastTagName = scanner.getTokenText();
lastAttributeName = null;
languageIdFromType = 'javascript';
break;
case TokenType.Styles:
regions.push({
languageId: 'css',
start: scanner.getTokenOffset(),
end: scanner.getTokenEnd()
});
break;
case TokenType.Script:
regions.push({
languageId: languageIdFromType,
start: scanner.getTokenOffset(),
end: scanner.getTokenEnd()
});
break;
case TokenType.AttributeName:
lastAttributeName = scanner.getTokenText();
break;
case TokenType.AttributeValue:
if (lastAttributeName === 'src' && lastTagName.toLowerCase() === 'script') {
let value = scanner.getTokenText();
if (value[0] === "'" || value[0] === '"') {
value = value.substr(1, value.length - 1);
}
importedScripts.push(value);
} else if (lastAttributeName === 'type' && lastTagName.toLowerCase() === 'script') {
if (
/["'](module|(text|application)\/(java|ecma)script|text\/babel)["']/.test(
scanner.getTokenText()
)
) {
languageIdFromType = 'javascript';
} else if (/["']text\/typescript["']/.test(scanner.getTokenText())) {
languageIdFromType = 'typescript';
} else {
languageIdFromType = undefined;
}
} else {
let attributeLanguageId = getAttributeLanguage(lastAttributeName!);
if (attributeLanguageId) {
let start = scanner.getTokenOffset();
let end = scanner.getTokenEnd();
let firstChar = documentText[start];
if (firstChar === "'" || firstChar === '"') {
start++;
end--;
}
regions.push({
languageId: attributeLanguageId,
start,
end,
attributeValue: true
});
}
}
lastAttributeName = null;
break;
}
token = scanner.scan();
}
let token = scanner.scan();
while (token !== TokenType.EOS) {
switch (token) {
case TokenType.StartTag:
lastTagName = scanner.getTokenText();
lastAttributeName = null;
languageIdFromType = "javascript";
break;
case TokenType.Styles:
regions.push({
languageId: "css",
start: scanner.getTokenOffset(),
end: scanner.getTokenEnd(),
});
break;
case TokenType.Script:
regions.push({
languageId: languageIdFromType,
start: scanner.getTokenOffset(),
end: scanner.getTokenEnd(),
});
break;
case TokenType.AttributeName:
lastAttributeName = scanner.getTokenText();
break;
case TokenType.AttributeValue:
if (
lastAttributeName === "src" &&
lastTagName.toLowerCase() === "script"
) {
let value = scanner.getTokenText();
if (value[0] === "'" || value[0] === '"') {
value = value.substr(1, value.length - 1);
}
importedScripts.push(value);
} else if (
lastAttributeName === "type" &&
lastTagName.toLowerCase() === "script"
) {
if (
/["'](module|(text|application)\/(java|ecma)script|text\/babel)["']/.test(
scanner.getTokenText()
)
) {
languageIdFromType = "javascript";
} else if (/["']text\/typescript["']/.test(scanner.getTokenText())) {
languageIdFromType = "typescript";
} else {
languageIdFromType = undefined;
}
} else {
let attributeLanguageId = getAttributeLanguage(lastAttributeName!);
if (attributeLanguageId) {
let start = scanner.getTokenOffset();
let end = scanner.getTokenEnd();
let firstChar = documentText[start];
if (firstChar === "'" || firstChar === '"') {
start++;
end--;
}
regions.push({
languageId: attributeLanguageId,
start,
end,
attributeValue: true,
});
}
}
lastAttributeName = null;
break;
}
token = scanner.scan();
}
let content = documentText
.split('\n')
.map(line => {
return ' '.repeat(line.length);
}).join('\n');
let content = documentText
.split("\n")
.map((line) => {
return " ".repeat(line.length);
})
.join("\n");
regions.forEach(r => {
if (r.languageId === 'css') {
content = content.slice(0, r.start) + documentText.slice(r.start, r.end) + content.slice(r.end);
}
});
regions.forEach((r) => {
if (r.languageId === "css") {
content =
content.slice(0, r.start) +
documentText.slice(r.start, r.end) +
content.slice(r.end);
}
});
return content;
return content;
}
export function getDocumentRegions(
languageService: LanguageService,
document: TextDocument
languageService: LanguageService,
document: TextDocument
): HTMLDocumentRegions {
let regions: EmbeddedRegion[] = [];
let scanner = languageService.createScanner(document.getText());
let lastTagName: string = '';
let lastAttributeName: string | null = null;
let languageIdFromType: string | undefined = undefined;
let importedScripts: string[] = [];
let regions: EmbeddedRegion[] = [];
let scanner = languageService.createScanner(document.getText());
let lastTagName: string = "";
let lastAttributeName: string | null = null;
let languageIdFromType: string | undefined = undefined;
let importedScripts: string[] = [];
let token = scanner.scan();
while (token !== TokenType.EOS) {
switch (token) {
case TokenType.StartTag:
lastTagName = scanner.getTokenText();
lastAttributeName = null;
languageIdFromType = 'javascript';
break;
case TokenType.Styles:
regions.push({
languageId: 'css',
start: scanner.getTokenOffset(),
end: scanner.getTokenEnd()
});
break;
case TokenType.Script:
regions.push({
languageId: languageIdFromType,
start: scanner.getTokenOffset(),
end: scanner.getTokenEnd()
});
break;
case TokenType.AttributeName:
lastAttributeName = scanner.getTokenText();
break;
case TokenType.AttributeValue:
if (lastAttributeName === 'src' && lastTagName.toLowerCase() === 'script') {
let value = scanner.getTokenText();
if (value[0] === "'" || value[0] === '"') {
value = value.substr(1, value.length - 1);
}
importedScripts.push(value);
} else if (lastAttributeName === 'type' && lastTagName.toLowerCase() === 'script') {
if (
/["'](module|(text|application)\/(java|ecma)script|text\/babel)["']/.test(
scanner.getTokenText()
)
) {
languageIdFromType = 'javascript';
} else if (/["']text\/typescript["']/.test(scanner.getTokenText())) {
languageIdFromType = 'typescript';
} else {
languageIdFromType = undefined;
}
} else {
let attributeLanguageId = getAttributeLanguage(lastAttributeName!);
if (attributeLanguageId) {
let start = scanner.getTokenOffset();
let end = scanner.getTokenEnd();
let firstChar = document.getText()[start];
if (firstChar === "'" || firstChar === '"') {
start++;
end--;
}
regions.push({
languageId: attributeLanguageId,
start,
end,
attributeValue: true
});
}
}
lastAttributeName = null;
break;
}
token = scanner.scan();
}
return {
getLanguageRanges: (range: Range) => getLanguageRanges(document, regions, range),
getEmbeddedDocument: (languageId: string, ignoreAttributeValues: boolean) =>
getEmbeddedDocument(document, regions, languageId, ignoreAttributeValues),
getLanguageAtPosition: (position: Position) =>
getLanguageAtPosition(document, regions, position),
getLanguagesInDocument: () => getLanguagesInDocument(document, regions),
getImportedScripts: () => importedScripts
};
let token = scanner.scan();
while (token !== TokenType.EOS) {
switch (token) {
case TokenType.StartTag:
lastTagName = scanner.getTokenText();
lastAttributeName = null;
languageIdFromType = "javascript";
break;
case TokenType.Styles:
regions.push({
languageId: "css",
start: scanner.getTokenOffset(),
end: scanner.getTokenEnd(),
});
break;
case TokenType.Script:
regions.push({
languageId: languageIdFromType,
start: scanner.getTokenOffset(),
end: scanner.getTokenEnd(),
});
break;
case TokenType.AttributeName:
lastAttributeName = scanner.getTokenText();
break;
case TokenType.AttributeValue:
if (
lastAttributeName === "src" &&
lastTagName.toLowerCase() === "script"
) {
let value = scanner.getTokenText();
if (value[0] === "'" || value[0] === '"') {
value = value.substr(1, value.length - 1);
}
importedScripts.push(value);
} else if (
lastAttributeName === "type" &&
lastTagName.toLowerCase() === "script"
) {
if (
/["'](module|(text|application)\/(java|ecma)script|text\/babel)["']/.test(
scanner.getTokenText()
)
) {
languageIdFromType = "javascript";
} else if (/["']text\/typescript["']/.test(scanner.getTokenText())) {
languageIdFromType = "typescript";
} else {
languageIdFromType = undefined;
}
} else {
let attributeLanguageId = getAttributeLanguage(lastAttributeName!);
if (attributeLanguageId) {
let start = scanner.getTokenOffset();
let end = scanner.getTokenEnd();
let firstChar = document.getText()[start];
if (firstChar === "'" || firstChar === '"') {
start++;
end--;
}
regions.push({
languageId: attributeLanguageId,
start,
end,
attributeValue: true,
});
}
}
lastAttributeName = null;
break;
}
token = scanner.scan();
}
return {
getLanguageRanges: (range: Range) =>
getLanguageRanges(document, regions, range),
getEmbeddedDocument: (languageId: string, ignoreAttributeValues: boolean) =>
getEmbeddedDocument(document, regions, languageId, ignoreAttributeValues),
getLanguageAtPosition: (position: Position) =>
getLanguageAtPosition(document, regions, position),
getLanguagesInDocument: () => getLanguagesInDocument(document, regions),
getImportedScripts: () => importedScripts,
};
}
function getLanguageRanges(
document: TextDocument,
regions: EmbeddedRegion[],
range: Range
document: TextDocument,
regions: EmbeddedRegion[],
range: Range
): LanguageRange[] {
let result: LanguageRange[] = [];
let currentPos = range ? range.start : Position.create(0, 0);
let currentOffset = range ? document.offsetAt(range.start) : 0;
let endOffset = range ? document.offsetAt(range.end) : document.getText().length;
for (let region of regions) {
if (region.end > currentOffset && region.start < endOffset) {
let start = Math.max(region.start, currentOffset);
let startPos = document.positionAt(start);
if (currentOffset < region.start) {
result.push({
start: currentPos,
end: startPos,
languageId: 'html'
});
}
let end = Math.min(region.end, endOffset);
let endPos = document.positionAt(end);
if (end > region.start) {
result.push({
start: startPos,
end: endPos,
languageId: region.languageId,
attributeValue: region.attributeValue
});
}
currentOffset = end;
currentPos = endPos;
}
}
if (currentOffset < endOffset) {
let endPos = range ? range.end : document.positionAt(endOffset);
result.push({
start: currentPos,
end: endPos,
languageId: 'html'
});
}
return result;
let result: LanguageRange[] = [];
let currentPos = range ? range.start : Position.create(0, 0);
let currentOffset = range ? document.offsetAt(range.start) : 0;
let endOffset = range
? document.offsetAt(range.end)
: document.getText().length;
for (let region of regions) {
if (region.end > currentOffset && region.start < endOffset) {
let start = Math.max(region.start, currentOffset);
let startPos = document.positionAt(start);
if (currentOffset < region.start) {
result.push({
start: currentPos,
end: startPos,
languageId: "html",
});
}
let end = Math.min(region.end, endOffset);
let endPos = document.positionAt(end);
if (end > region.start) {
result.push({
start: startPos,
end: endPos,
languageId: region.languageId,
attributeValue: region.attributeValue,
});
}
currentOffset = end;
currentPos = endPos;
}
}
if (currentOffset < endOffset) {
let endPos = range ? range.end : document.positionAt(endOffset);
result.push({
start: currentPos,
end: endPos,
languageId: "html",
});
}
return result;
}
function getLanguagesInDocument(
_document: TextDocument,
regions: EmbeddedRegion[]
_document: TextDocument,
regions: EmbeddedRegion[]
): string[] {
let result = [];
for (let region of regions) {
if (region.languageId && result.indexOf(region.languageId) === -1) {
result.push(region.languageId);
if (result.length === 3) {
return result;
}
}
}
result.push('html');
return result;
let result = [];
for (let region of regions) {
if (region.languageId && result.indexOf(region.languageId) === -1) {
result.push(region.languageId);
if (result.length === 3) {
return result;
}
}
}
result.push("html");
return result;
}
function getLanguageAtPosition(
document: TextDocument,
regions: EmbeddedRegion[],
position: Position
document: TextDocument,
regions: EmbeddedRegion[],
position: Position
): string | undefined {
let offset = document.offsetAt(position);
for (let region of regions) {
if (region.start <= offset) {
if (offset <= region.end) {
return region.languageId;
}
} else {
break;
}
}
return 'html';
let offset = document.offsetAt(position);
for (let region of regions) {
if (region.start <= offset) {
if (offset <= region.end) {
return region.languageId;
}
} else {
break;
}
}
return "html";
}
function getEmbeddedDocument(
document: TextDocument,
contents: EmbeddedRegion[],
languageId: string,
ignoreAttributeValues: boolean
document: TextDocument,
contents: EmbeddedRegion[],
languageId: string,
ignoreAttributeValues: boolean
): TextDocument {
let currentPos = 0;
let oldContent = document.getText();
let result = '';
let lastSuffix = '';
for (let c of contents) {
if (c.languageId === languageId && (!ignoreAttributeValues || !c.attributeValue)) {
result = substituteWithWhitespace(
result,
currentPos,
c.start,
oldContent,
lastSuffix,
getPrefix(c)
);
result += oldContent.substring(c.start, c.end);
currentPos = c.end;
lastSuffix = getSuffix(c);
}
}
result = substituteWithWhitespace(
result,
currentPos,
oldContent.length,
oldContent,
lastSuffix,
''
);
return TextDocument.create(document.uri, languageId, document.version, result);
let currentPos = 0;
let oldContent = document.getText();
let result = "";
let lastSuffix = "";
for (let c of contents) {
if (
c.languageId === languageId &&
(!ignoreAttributeValues || !c.attributeValue)
) {
result = substituteWithWhitespace(
result,
currentPos,
c.start,
oldContent,
lastSuffix,
getPrefix(c)
);
result += oldContent.substring(c.start, c.end);
currentPos = c.end;
lastSuffix = getSuffix(c);
}
}
result = substituteWithWhitespace(
result,
currentPos,
oldContent.length,
oldContent,
lastSuffix,
""
);
return TextDocument.create(
document.uri,
languageId,
document.version,
result
);
}
function getPrefix(c: EmbeddedRegion) {
if (c.attributeValue) {
switch (c.languageId) {
case 'css':
return CSS_STYLE_RULE + '{';
}
}
return '';
if (c.attributeValue) {
switch (c.languageId) {
case "css":
return CSS_STYLE_RULE + "{";
}
}
return "";
}
function getSuffix(c: EmbeddedRegion) {
if (c.attributeValue) {
switch (c.languageId) {
case 'css':
return '}';
case 'javascript':
return ';';
}
}
return '';
if (c.attributeValue) {
switch (c.languageId) {
case "css":
return "}";
case "javascript":
return ";";
}
}
return "";
}
function substituteWithWhitespace(
result: string,
start: number,
end: number,
oldContent: string,
before: string,
after: string
result: string,
start: number,
end: number,
oldContent: string,
before: string,
after: string
) {
let accumulatedWS = 0;
result += before;
for (let i = start + before.length; i < end; i++) {
let ch = oldContent[i];
if (ch === '\n' || ch === '\r') {
// only write new lines, skip the whitespace
accumulatedWS = 0;
result += ch;
} else {
accumulatedWS++;
}
}
result = append(result, ' ', accumulatedWS - after.length);
result += after;
return result;
let accumulatedWS = 0;
result += before;
for (let i = start + before.length; i < end; i++) {
let ch = oldContent[i];
if (ch === "\n" || ch === "\r") {
// only write new lines, skip the whitespace
accumulatedWS = 0;
result += ch;
} else {
accumulatedWS++;
}
}
result = append(result, " ", accumulatedWS - after.length);
result += after;
return result;
}
function append(result: string, str: string, n: number): string {
while (n > 0) {
if (n & 1) {
result += str;
}
n >>= 1;
str += str;
}
return result;
while (n > 0) {
if (n & 1) {
result += str;
}
n >>= 1;
str += str;
}
return result;
}
function getAttributeLanguage(attributeName: string): string | null {
let match = attributeName.match(/^(style)$|^(on\w+)$/i);
if (!match) {
return null;
}
return match[1] ? 'css' : 'javascript';
let match = attributeName.match(/^(style)$|^(on\w+)$/i);
if (!match) {
return null;
}
return match[1] ? "css" : "javascript";
}

View file

@ -3,85 +3,145 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
* ------------------------------------------------------------------------------------------ */
import * as path from 'path';
import { commands, CompletionList, ExtensionContext, Uri, workspace } from 'vscode';
import { getLanguageService } from 'vscode-html-languageservice';
import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind } from 'vscode-languageclient';
import { getCSSVirtualContent, isInsideStyleRegion } from './embeddedSupport';
import * as path from "path";
import {
commands,
CompletionList,
ExtensionContext,
Uri,
workspace,
} from "vscode";
import { getLanguageService } from "vscode-html-languageservice";
import {
LanguageClient,
LanguageClientOptions,
ServerOptions,
TransportKind,
} from "vscode-languageclient";
import { isInsideHtmlMacro } from "./rustSupport";
// import { getCSSVirtualContent, isInsideStyleRegion } from "./embeddedSupport";
let client: LanguageClient;
const htmlLanguageService = getLanguageService();
export function activate(context: ExtensionContext) {
// The server is implemented in node
let serverModule = context.asAbsolutePath(path.join('server', 'out', 'server.js'));
// The debug options for the server
// --inspect=6009: runs the server in Node's Inspector mode so VS Code can attach to the server for debugging
let debugOptions = { execArgv: ['--nolazy', '--inspect=6009'] };
// The server is implemented in node
let serverModule = context.asAbsolutePath(
path.join("server", "out", "server.js")
);
// The debug options for the server
// --inspect=6009: runs the server in Node's Inspector mode so VS Code can attach to the server for debugging
let debugOptions = { execArgv: ["--nolazy", "--inspect=6009"] };
// If the extension is launched in debug mode then the debug server options are used
// Otherwise the run options are used
let serverOptions: ServerOptions = {
run: { module: serverModule, transport: TransportKind.ipc },
debug: {
module: serverModule,
transport: TransportKind.ipc,
options: debugOptions
}
};
// If the extension is launched in debug mode then the debug server options are used
// Otherwise the run options are used
let serverOptions: ServerOptions = {
run: { module: serverModule, transport: TransportKind.ipc },
debug: {
module: serverModule,
transport: TransportKind.ipc,
options: debugOptions,
},
};
const virtualDocumentContents = new Map<string, string>();
const virtualDocumentContents = new Map<string, string>();
workspace.registerTextDocumentContentProvider('embedded-content', {
provideTextDocumentContent: uri => {
const originalUri = uri.path.slice(1).slice(0, -4);
const decodedUri = decodeURIComponent(originalUri);
return virtualDocumentContents.get(decodedUri);
}
});
workspace.registerTextDocumentContentProvider("embedded-content", {
provideTextDocumentContent: (uri) => {
const originalUri = uri.path.slice(1).slice(0, -4);
console.error(originalUri);
const decodedUri = decodeURIComponent(originalUri);
return virtualDocumentContents.get(decodedUri);
},
});
let clientOptions: LanguageClientOptions = {
documentSelector: [{ scheme: 'file', language: 'html1' }],
middleware: {
provideCompletionItem: async (document, position, context, token, next) => {
// If not in `<style>`, do not perform request forwarding
if (!isInsideStyleRegion(htmlLanguageService, document.getText(), document.offsetAt(position))) {
return await next(document, position, context, token);
}
let clientOptions: LanguageClientOptions = {
documentSelector: [{ scheme: "file", language: "rust" }],
middleware: {
provideCompletionItem: async (
document,
position,
context,
token,
next
) => {
/*
1: Find the occurences of the html! macro using regex
2: Check if any of the occurences match the cursor offset
3: If so, direct the captured block to the html to the rsx language service
*/
const docSrc = document.getText();
const offset = document.offsetAt(position);
const matches = docSrc.matchAll(macroRegex);
const originalUri = document.uri.toString();
virtualDocumentContents.set(originalUri, getCSSVirtualContent(htmlLanguageService, document.getText()));
// Lazily loop through matches, abort early if the cursor is after the match
// let start = 0;
// let end = 0;
let matchBody: string | undefined = undefined;
const vdocUriString = `embedded-content://css/${encodeURIComponent(
originalUri
)}.css`;
const vdocUri = Uri.parse(vdocUriString);
return await commands.executeCommand<CompletionList>(
'vscode.executeCompletionItemProvider',
vdocUri,
position,
context.triggerCharacter
);
}
}
};
for (const match of matches) {
// // Check if the cursor is inbetween the previous end and the new start
// // This means the cursor is between html! invocations and we should bail early
// if (offset > end && offset < match.index) {
// // Ensure the match
// // defer to the "next?" symbol
// return await next(document, position, context, token);
// }
// Create the language client and start the client.
client = new LanguageClient(
'languageServerExample',
'Language Server Example',
serverOptions,
clientOptions
);
// Otherwise, move the counters forward
const start = match.index;
const end = start + match.length;
// Start the client. This will also launch the server
client.start();
// Ensure the cursor is within the match
// Break if so
if (offset >= start && offset <= end) {
matchBody = match[1];
break;
}
}
// If we looped through all the matches and the match wasn't defined, then bail
if (matchBody === undefined) {
return await next(document, position, context, token);
}
// If we're inside the style region, then provide CSS completions with the CSS provider
const originalUri = document.uri.toString();
virtualDocumentContents.set(originalUri, matchBody);
// getCSSVirtualContent(htmlLanguageService, document.getText())
const vdocUriString = `embedded-content://html/${encodeURIComponent(
originalUri
)}.html`;
const vdocUri = Uri.parse(vdocUriString);
return await commands.executeCommand<CompletionList>(
"vscode.executeCompletionItemProvider",
vdocUri,
position,
context.triggerCharacter
);
},
},
};
// Create the language client and start the client.
client = new LanguageClient(
"languageServerExample",
"Language Server Example",
serverOptions,
clientOptions
);
// Start the client. This will also launch the server
client.start();
}
export function deactivate(): Thenable<void> | undefined {
if (!client) {
return undefined;
}
return client.stop();
if (!client) {
return undefined;
}
return client.stop();
}
const macroRegex = /html! {([\s\S]*?)}/g;

View file

@ -0,0 +1,8 @@
const macroRegex = /html! {([\s\S]*?)}/g;
export function isInsideHtmlMacro(
match: RegExpMatchArray,
cursor: number
): boolean {
return false;
}

View file

@ -1,8 +1,8 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2019",
"lib": ["ES2019"],
"target": "ESNext",
"lib": ["ESNext"],
"outDir": "out",
"rootDir": "src",
"sourceMap": true

View file

@ -16,26 +16,9 @@
"node": "*"
},
"activationEvents": [
"onLanguage:html1"
"onLanguage:rust"
],
"main": "./client/out/extension",
"contributes": {
"languages": [
{
"id": "rsx",
"extensions": [
".rs"
]
}
],
"grammars": [
{
"language": "rsx",
"scopeName": "text.rsx.basic",
"path": "./syntaxes/rsx.tmLanguage.json"
}
]
},
"scripts": {
"vscode:prepublish": "cd client && npm install && cd .. && npm run compile",
"compile": "tsc -b",

View file

@ -3,9 +3,15 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
* ------------------------------------------------------------------------------------------ */
import { getLanguageService } from 'vscode-html-languageservice';
import { createConnection, InitializeParams, ProposedFeatures, TextDocuments, TextDocumentSyncKind } from 'vscode-languageserver';
import { TextDocument } from 'vscode-languageserver-textdocument';
import { getLanguageService } from "vscode-html-languageservice";
import {
createConnection,
InitializeParams,
ProposedFeatures,
TextDocuments,
TextDocumentSyncKind,
} from "vscode-languageserver";
import { TextDocument } from "vscode-languageserver-textdocument";
// Create a connection for the server. The connection uses Node's IPC as a transport.
// Also include all preview / proposed LSP features.
@ -18,30 +24,30 @@ let documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);
const htmlLanguageService = getLanguageService();
connection.onInitialize((_params: InitializeParams) => {
return {
capabilities: {
textDocumentSync: TextDocumentSyncKind.Full,
// Tell the client that the server supports code completion
completionProvider: {
resolveProvider: false
}
}
};
return {
capabilities: {
textDocumentSync: TextDocumentSyncKind.Full,
// Tell the client that the server supports code completion
completionProvider: {
resolveProvider: false,
},
},
};
});
connection.onInitialized(() => { });
connection.onInitialized(() => {});
connection.onCompletion(async (textDocumentPosition, token) => {
const document = documents.get(textDocumentPosition.textDocument.uri);
if (!document) {
return null;
}
const document = documents.get(textDocumentPosition.textDocument.uri);
if (!document) {
return null;
}
return htmlLanguageService.doComplete(
document,
textDocumentPosition.position,
htmlLanguageService.parseHTMLDocument(document)
);
return htmlLanguageService.doComplete(
document,
textDocumentPosition.position,
htmlLanguageService.parseHTMLDocument(document)
);
});
documents.listen(connection);

View file

@ -1,7 +1,7 @@
{
"compilerOptions": {
"target": "es2019",
"lib": ["ES2019"],
"target": "ESNext",
"lib": ["ESNext"],
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,

View file

@ -1,9 +1,9 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2019",
"target": "ESNext",
"lib": [
"ES2019"
"ESNext"
],
"outDir": "out",
"rootDir": "src",