├── .gitignore ├── Cargo.toml ├── Makefile ├── README.md ├── client └── nvim-lspconfig │ ├── example.vim │ └── lua │ └── web_browser_lsp.lua ├── mapping_lsp.md └── src ├── lib.rs ├── lsp_ext.rs ├── main.rs ├── transport.rs ├── worker.rs └── worker └── lsp.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "web-browser-lsp" 3 | version = "0.0.0" 4 | edition = "2018" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | [dependencies] 8 | playwright = "0.0.17" 9 | structopt = "0.3.21" 10 | anyhow = "1.0.40" 11 | thiserror = "1.0.24" 12 | tokio = { version = "1.5.0", features = ["macros", "rt-multi-thread"] } 13 | async-trait = "0.1.50" 14 | serde = { version = "1.0.125", features = ["derive"] } 15 | serde_json = { version = "1.0.64", features = ["raw_value"] } 16 | lsp-types = { version = "0.89.0", features = ["proposed"] } 17 | lsp-server = "0.5.1" 18 | log = "0.4.14" 19 | crossbeam-channel = "0.5.1" 20 | futures = "0.3.14" 21 | log4rs = "1.0.0" 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | release: 2 | cargo build --release 3 | 4 | dev: format lint 5 | make test & 6 | make doc 7 | cargo build 8 | 9 | d: 10 | cargo watch -c -s 'echo " " && make dev' 11 | 12 | format: 13 | cargo fmt 14 | 15 | lint: 16 | cargo clippy --all-targets 17 | 18 | test: 19 | cargo test --all-targets 20 | 21 | doc: 22 | cargo doc 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # web-browser-lsp 2 | A toy program that implements a text-based web browser as a language server. 3 | 4 | ## Motivation 5 | My favorite progrmming tools are neovim, tmux on a fast terminal emulator and firefox. R.I.P. vimperator. 6 | I've tried w3m, lynx, browsh, libsixel, vim-bind webextension, keyboard-driven browser, etc. 7 | but I can't find anything that goes beyond vimerator. 8 | 9 | ## Features 10 | There is no implementation 11 | * initialize, shutdown, exit 12 | * `web-browser-lsp/tab` creates first tab but not connect 13 | * `textDocument/formatting` shows tab contents in text editor 14 | 15 | ![demo](https://user-images.githubusercontent.com/7942952/117168466-21cab600-ae03-11eb-9f8c-5b4736bc60d9.gif) 16 | 17 | ## Wish and Concept 18 | * text-based web browser 19 | * Using a full-featured browser 20 | - Connecting via Chrome DevTools Protocol 21 | - It's impossible to develop a full-featured browser by myself 22 | * and you can switch to the GUI 23 | - Non-textual rich contents may be required 24 | * Human-readable text mode and Raw (html) mode 25 | - Web developer friendly 26 | - Nested [onclick] can be clicked precisely 27 | * zoom in/out html tags 28 | - Focus on the main content of a page with multiple columns 29 | 30 | I'm considering about [operations required for the browser and corresponding lsp methods](./mapping_lsp.md). 31 | 32 | ## Credits 33 | * [rust-analyzer/rust-analyzer](https://github.com/rust-analyzer/rust-analyzer) [MIT](https://github.com/rust-analyzer/rust-analyzer/blob/master/LICENSE-MIT) 34 | -------------------------------------------------------------------------------- /client/nvim-lspconfig/example.vim: -------------------------------------------------------------------------------- 1 | 2 | function! s:start_lsp() abort 3 | lua < Result; 27 | 28 | async fn handle_notification(&mut self, msg: Self::Notification) 29 | -> Result; 30 | async fn handle_request(&mut self, msg: Self::Request) 31 | -> Result; 32 | } 33 | 34 | trait Transport { 35 | type Message; 36 | type InitializeParams; 37 | type InitializeResult; 38 | 39 | fn wait_initial_message(&mut self) -> Result; 40 | fn respond_initial_message( 41 | &mut self, 42 | result: Self::InitializeResult 43 | ) -> Result<(), anyhow::Error>; 44 | 45 | fn next_message(&mut self) -> Result; 46 | fn send(&mut self, msg: Self::Message) -> Result<(), anyhow::Error>; 47 | 48 | fn close(self) -> Result<(), anyhow::Error>; 49 | } 50 | 51 | pub async fn run_server(temp_dir: TempDir) -> anyhow::Result<()> { 52 | use self::{transport::Stdio, worker::Worker}; 53 | let transport = Stdio::new(); 54 | let playwright = Playwright::initialize().await?; 55 | playwright.prepare()?; 56 | let mut worker = Worker::new(transport, playwright); 57 | worker.initialize().await?; 58 | worker.run().await?; 59 | // client may kill on stdio closed 60 | temp_dir.remove(); 61 | log::info!("exit success"); 62 | worker.close()?; 63 | Ok(()) 64 | } 65 | 66 | pub struct TempDir { 67 | path: PathBuf 68 | } 69 | 70 | impl TempDir { 71 | /// # Panics 72 | /// Panics if io::Error is unwrapped 73 | pub fn new() -> Self { 74 | let id = process::id(); 75 | let base = env::temp_dir(); 76 | let path = base.join(format!("web-browser-lsp-{}", id)); 77 | fs::create_dir(&path).unwrap(); 78 | Self { path } 79 | } 80 | 81 | pub fn as_path(&self) -> &Path { &self.path } 82 | 83 | fn remove(self) { fs::remove_dir_all(self.as_path()).ok(); } 84 | } 85 | -------------------------------------------------------------------------------- /src/lsp_ext.rs: -------------------------------------------------------------------------------- 1 | use lsp_types::{ 2 | notification::Notification, request::Request, CodeActionKind, Position, Range, 3 | TextDocumentIdentifier 4 | }; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | pub enum Tab {} 8 | 9 | impl Request for Tab { 10 | type Params = TabParams; 11 | type Result = (); 12 | const METHOD: &'static str = "web-browser-lsp/tab"; 13 | } 14 | 15 | #[derive(Deserialize, Serialize, Debug)] 16 | #[serde(rename_all = "camelCase")] 17 | pub struct TabParams {} 18 | 19 | // enum ConnectBrowser 20 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{path::Path, process}; 2 | use structopt::StructOpt; 3 | use web_browser_lsp::{run_server, TempDir}; 4 | 5 | #[tokio::main] 6 | async fn main() { 7 | let temp_dir = TempDir::new(); 8 | init_log(temp_dir.as_path()).unwrap(); 9 | let opt: Opt = Opt::from_args(); 10 | if let Err(err) = run(opt, temp_dir).await { 11 | log::error!("{:?}", err); 12 | process::exit(101); 13 | } 14 | } 15 | 16 | fn init_log(temp_dir: &Path) -> anyhow::Result<()> { 17 | use log::LevelFilter; 18 | use log4rs::{ 19 | append::file::FileAppender, 20 | config::{Appender, Config, Root}, 21 | encode::pattern::PatternEncoder 22 | }; 23 | let logfile = FileAppender::builder() 24 | .encoder(Box::new(PatternEncoder::new("{l} - {m}\n"))) 25 | .build(temp_dir.join("debug.log"))?; 26 | let config = Config::builder() 27 | .appender(Appender::builder().build("logfile", Box::new(logfile))) 28 | .build( 29 | Root::builder() 30 | .appender("logfile") 31 | .build(LevelFilter::Trace) 32 | )?; 33 | log4rs::init_config(config)?; 34 | Ok(()) 35 | } 36 | 37 | async fn run(opt: Opt, temp_dir: TempDir) -> anyhow::Result<()> { 38 | match opt.sub_command.as_ref() { 39 | Some(SubCommand::Server(_)) => run_server(temp_dir).await?, 40 | _ => run_server(temp_dir).await? 41 | } 42 | Ok(()) 43 | } 44 | 45 | #[derive(Debug, StructOpt)] 46 | struct Opt { 47 | #[structopt(subcommand)] 48 | pub sub_command: Option 49 | } 50 | 51 | #[derive(Debug, StructOpt)] 52 | enum SubCommand { 53 | Server(Server) 54 | } 55 | 56 | #[derive(Debug, StructOpt)] 57 | struct Server {} 58 | -------------------------------------------------------------------------------- /src/transport.rs: -------------------------------------------------------------------------------- 1 | use super::Transport; 2 | use lsp_server::{Connection, IoThreads, RequestId}; 3 | 4 | pub(super) struct Stdio { 5 | conn: Connection, 6 | io_threads: IoThreads, 7 | initialize_id: Option 8 | } 9 | 10 | impl Stdio { 11 | pub(super) fn new() -> Self { 12 | let (conn, io_threads) = Connection::stdio(); 13 | Self { 14 | conn, 15 | io_threads, 16 | initialize_id: None 17 | } 18 | } 19 | } 20 | 21 | #[async_trait] 22 | impl Transport for Stdio { 23 | type Message = lsp_server::Message; 24 | type InitializeParams = lsp_types::InitializeParams; 25 | type InitializeResult = lsp_types::InitializeResult; 26 | 27 | fn wait_initial_message(&mut self) -> Result { 28 | let (initialize_id, initialize_params) = self.conn.initialize_start()?; 29 | log::info!("InitializeParams: {}", initialize_params); 30 | self.initialize_id = Some(initialize_id); 31 | let initialize_params: lsp_types::InitializeParams = 32 | serde_json::from_value(initialize_params)?; 33 | Ok(initialize_params) 34 | } 35 | 36 | fn respond_initial_message( 37 | &mut self, 38 | result: Self::InitializeResult 39 | ) -> Result<(), anyhow::Error> { 40 | let initialize_id = self 41 | .initialize_id 42 | .take() 43 | .ok_or_else(|| anyhow::anyhow!("initialize_id is None"))?; 44 | let initialize_result = serde_json::to_value(result)?; 45 | self.conn 46 | .initialize_finish(initialize_id, initialize_result)?; 47 | Ok(()) 48 | } 49 | 50 | fn send(&mut self, msg: Self::Message) -> Result<(), anyhow::Error> { 51 | log::debug!("SEND: {:?}", &msg); 52 | Ok(self.conn.sender.send(msg)?) 53 | } 54 | 55 | fn next_message(&mut self) -> Result { 56 | Ok(self.conn.receiver.recv()?) 57 | } 58 | 59 | fn close(self) -> Result<(), anyhow::Error> { Ok(self.io_threads.join()?) } 60 | } 61 | -------------------------------------------------------------------------------- /src/worker.rs: -------------------------------------------------------------------------------- 1 | mod lsp; 2 | 3 | use super::{LspServer, Transport}; 4 | use playwright::{ 5 | api::{BrowserContext, Page}, 6 | Playwright 7 | }; 8 | use std::time::Instant; 9 | 10 | // pub(crate) type ResponseHandler = fn(&mut Worker, lsp_server::Response); 11 | type ResponseHandler = fn(&mut Worker, lsp_server::Response) -> anyhow::Result<()>; 12 | type TransportQueue = lsp_server::ReqQueue<(String, Instant), ResponseHandler>; 13 | 14 | pub(super) struct Worker { 15 | transport: T, 16 | transport_queue: TransportQueue, 17 | shutdown_requested: bool, 18 | playwright: Playwright, 19 | active_tab: Option, 20 | content: String 21 | } 22 | 23 | impl Worker 24 | where 25 | T: Transport< 26 | InitializeParams = ::InitializeParams, 27 | InitializeResult = ::InitializeResult, 28 | Message = lsp_server::Message 29 | > + Send 30 | { 31 | pub(super) fn new(transport: T, playwright: Playwright) -> Self { 32 | Self { 33 | transport, 34 | transport_queue: TransportQueue::default(), 35 | shutdown_requested: false, 36 | playwright, 37 | active_tab: None, 38 | content: "".into() 39 | } 40 | } 41 | 42 | pub(super) async fn initialize(&mut self) -> anyhow::Result<()> { 43 | let req = self.transport.wait_initial_message()?; 44 | let result = ::initialize(self, req).await?; 45 | self.transport.respond_initial_message(result)?; 46 | Ok(()) 47 | } 48 | 49 | pub(super) fn close(self) -> anyhow::Result<()> { self.transport.close() } 50 | 51 | pub(super) async fn run(&mut self) -> anyhow::Result<()> 52 | where 53 | T: Transport 54 | { 55 | loop { 56 | let msg = self.transport.next_message()?; 57 | // Should I make errors silent? 58 | if self.handle_message(msg).await? { 59 | break; 60 | } 61 | } 62 | Ok(()) 63 | } 64 | 65 | async fn handle_message(&mut self, msg: lsp_server::Message) -> anyhow::Result 66 | where 67 | T: Transport 68 | { 69 | log::debug!("RECV: {:?}", msg); 70 | let should_close = match msg { 71 | lsp_server::Message::Request(req) => { 72 | let response = self.handle_request(req).await?; 73 | self.transport 74 | .send(lsp_server::Message::Response(response))?; 75 | false 76 | } 77 | lsp_server::Message::Notification(notif) => self.handle_notification(notif).await?, 78 | lsp_server::Message::Response(resp) => { 79 | let handler = self.transport_queue.outgoing.complete(resp.id.clone()); 80 | handler(self, resp)?; 81 | false 82 | } 83 | }; 84 | Ok(should_close) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/worker/lsp.rs: -------------------------------------------------------------------------------- 1 | use super::{ResponseHandler, Worker}; 2 | use crate::{lsp_ext, LspServer, Transport}; 3 | use lsp_server::{ErrorCode, RequestId}; 4 | use lsp_types::{ 5 | notification::Notification as _, request::Request as _, ClientCapabilities, InitializeParams, 6 | InitializeResult, Position, Range, ServerCapabilities, TextEdit 7 | }; 8 | use serde::{de::DeserializeOwned, Serialize}; 9 | use std::{fmt, future::Future, panic}; 10 | 11 | #[derive(Debug)] 12 | struct ErrorBody { 13 | code: ErrorCode, 14 | message: String 15 | } 16 | 17 | fn _impl_lsp_error() { 18 | impl ErrorBody { 19 | fn new(code: ErrorCode, message: String) -> ErrorBody { ErrorBody { code, message } } 20 | 21 | fn into_response(self, id: RequestId) -> lsp_server::Response { 22 | lsp_server::Response::new_err(id, self.code as i32, self.message) 23 | } 24 | } 25 | 26 | impl fmt::Display for ErrorBody { 27 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 28 | write!( 29 | f, 30 | "Language Server request failed with {}. ({})", 31 | self.code as i32, self.message 32 | ) 33 | } 34 | } 35 | 36 | impl std::error::Error for ErrorBody {} 37 | } 38 | 39 | #[async_trait] 40 | impl LspServer for Worker 41 | where 42 | T: Transport + Send 43 | { 44 | type Notification = lsp_server::Notification; 45 | type Request = lsp_server::Request; 46 | type Response = lsp_server::Response; 47 | type InitializeParams = InitializeParams; 48 | type InitializeResult = InitializeResult; 49 | 50 | async fn handle_notification( 51 | &mut self, 52 | msg: Self::Notification 53 | ) -> Result { 54 | match msg.method.as_str() { 55 | lsp_types::notification::Exit::METHOD => return Ok(true), 56 | lsp_types::notification::DidChangeConfiguration::METHOD => { 57 | self.send_request::( 58 | lsp_types::ConfigurationParams { 59 | items: vec![lsp_types::ConfigurationItem { 60 | scope_uri: None, 61 | section: Some("web-browser-lsp".to_string()) 62 | }] 63 | }, 64 | |worker, resp| { 65 | log::debug!("config update response: '{:?}", resp); 66 | let Self::Response { error, result, .. } = resp; 67 | match (error, result) { 68 | (Some(err), _) => { 69 | log::error!("failed to fetch the server settings: {:?}", err) 70 | } 71 | (None, Some(mut configs)) => { 72 | if let Some(json) = configs.get_mut(0) { 73 | // TODO 74 | } 75 | } 76 | (None, None) => log::error!( 77 | "received empty server settings response from the client" 78 | ) 79 | } 80 | Ok(()) 81 | } 82 | )?; 83 | } 84 | lsp_types::notification::DidChangeTextDocument::METHOD => { 85 | let params = 86 | parse_notification::(msg)?; 87 | // NOTE: setting full sync 88 | let first = params.content_changes.into_iter().next(); 89 | if let Some(changed) = first { 90 | self.content = changed.text; 91 | } 92 | } 93 | _ => {} 94 | } 95 | Ok(false) 96 | } 97 | 98 | async fn handle_request( 99 | &mut self, 100 | msg: Self::Request 101 | ) -> Result { 102 | if self.shutdown_requested { 103 | let response = ErrorBody::new( 104 | ErrorCode::InvalidRequest, 105 | "Shutdown already requested.".into() 106 | ) 107 | .into_response(msg.id); 108 | return Ok(response); 109 | } 110 | let response = match msg.method.as_str() { 111 | lsp_types::request::Shutdown::METHOD => { 112 | self.lift_request::(msg, |worker, ()| async move { 113 | worker.shutdown_requested = true; 114 | Ok(()) 115 | }) 116 | .await? 117 | } 118 | lsp_ext::Tab::METHOD => { 119 | self.lift_request::(msg, |worker, params| async move { 120 | let browser = worker 121 | .playwright 122 | .chromium() 123 | .launcher() 124 | .headless(false) 125 | .launch() 126 | .await?; 127 | let context = browser.context_builder().build().await?; 128 | let page = context.new_page().await?; 129 | worker.active_tab = Some(page); 130 | Ok(()) 131 | }) 132 | .await? 133 | } 134 | lsp_types::request::Formatting::METHOD => { 135 | self.lift_request::( 136 | msg, 137 | |worker, _params| async move { 138 | // FIXME: count char 139 | if let Some(tab) = &worker.active_tab { 140 | let s = tab.inner_text("body", None).await?; 141 | let n = s.lines().count(); 142 | let last = s.lines().last().unwrap_or_default(); 143 | Ok(Some(vec![TextEdit { 144 | range: Range { 145 | start: Position { 146 | line: 0, 147 | character: 0 148 | }, 149 | end: Position { 150 | line: n as u32, 151 | character: last.len() as u32 152 | } 153 | }, 154 | new_text: s 155 | }])) 156 | } else { 157 | let n = worker.content.lines().count(); 158 | let last = worker.content.lines().last().unwrap_or_default(); 159 | Ok(Some(vec![TextEdit { 160 | range: Range { 161 | start: Position { 162 | line: 0, 163 | character: 0 164 | }, 165 | end: Position { 166 | line: n as u32, 167 | character: last.len() as u32 168 | } 169 | }, 170 | new_text: "".into() 171 | }])) 172 | } 173 | } 174 | ) 175 | .await? 176 | } 177 | _ => { 178 | log::error!("unknown request: {:?}", msg); 179 | lsp_server::Response::new_err( 180 | msg.id, 181 | lsp_server::ErrorCode::MethodNotFound as i32, 182 | "unknown request".to_string() 183 | ) 184 | } 185 | }; 186 | Ok(response) 187 | } 188 | 189 | async fn initialize( 190 | &mut self, 191 | params: Self::InitializeParams 192 | ) -> Result { 193 | let server_capabilities = server_capabilities(¶ms.capabilities); 194 | 195 | let initialize_result = InitializeResult { 196 | capabilities: server_capabilities, 197 | server_info: Some(lsp_types::ServerInfo { 198 | name: env!("CARGO_PKG_NAME").into(), 199 | version: Some(env!("CARGO_PKG_VERSION").into()) 200 | }), 201 | offset_encoding: if supports_utf8(¶ms.capabilities) { 202 | Some("utf-8".to_string()) 203 | } else { 204 | None 205 | } 206 | }; 207 | Ok(initialize_result) 208 | } 209 | } 210 | 211 | impl Worker 212 | where 213 | T: Transport + Send 214 | { 215 | async fn lift_request<'a, R, F>( 216 | &'a mut self, 217 | msg: lsp_server::Request, 218 | f: fn(&'a mut Self, R::Params) -> F 219 | ) -> anyhow::Result<::Response> 220 | where 221 | R: lsp_types::request::Request + 'static, 222 | R::Params: DeserializeOwned + panic::UnwindSafe + fmt::Debug + 'static, 223 | R::Result: Serialize + 'static, 224 | F: Future> 225 | { 226 | let (id, params) = match parse_request::(msg) { 227 | Ok((id, params)) => (id, params), 228 | Err(response) => return Ok(response) 229 | }; 230 | // rust-analyzer handles panics 231 | let result = f(self, params).await; 232 | let response = result_to_response::(id, result); 233 | Ok(response) 234 | } 235 | 236 | fn send_request( 237 | &mut self, 238 | params: R::Params, 239 | handler: ResponseHandler 240 | ) -> anyhow::Result<()> { 241 | let request = 242 | self.transport_queue 243 | .outgoing 244 | .register(R::METHOD.to_string(), params, handler); 245 | self.transport.send(request.into())?; 246 | Ok(()) 247 | } 248 | } 249 | 250 | fn parse_notification(msg: lsp_server::Notification) -> anyhow::Result 251 | where 252 | N: lsp_types::notification::Notification + 'static, 253 | N::Params: DeserializeOwned + 'static 254 | { 255 | let lsp_server::Notification { params, .. } = msg; 256 | let res: N::Params = serde_json::from_value(params)?; 257 | Ok(res) 258 | } 259 | 260 | fn parse_request( 261 | msg: lsp_server::Request 262 | ) -> Result<(RequestId, R::Params), lsp_server::Response> 263 | where 264 | R: lsp_types::request::Request + 'static, 265 | R::Params: DeserializeOwned + 'static 266 | { 267 | let lsp_server::Request { id, params, .. } = msg; 268 | let res: Result = serde_json::from_value(params); 269 | match res { 270 | Ok(params) => Ok((id, params)), 271 | Err(err) => { 272 | let response = 273 | lsp_server::Response::new_err(id, ErrorCode::InvalidParams as i32, err.to_string()); 274 | Err(response) 275 | } 276 | } 277 | } 278 | 279 | fn result_to_response( 280 | id: lsp_server::RequestId, 281 | result: anyhow::Result 282 | ) -> lsp_server::Response 283 | where 284 | R: lsp_types::request::Request + 'static, 285 | R::Params: DeserializeOwned + 'static, 286 | R::Result: Serialize + 'static 287 | { 288 | match result { 289 | Ok(resp) => lsp_server::Response::new_ok(id, &resp), 290 | Err(e) => match e.downcast::() { 291 | Ok(e) => e.into_response(id), 292 | // Er(e) if is_canceled(e) => {ErrorCode::ContentModified} 293 | Err(e) => lsp_server::Response::new_err( 294 | id, 295 | lsp_server::ErrorCode::InternalError as i32, 296 | e.to_string() 297 | ) 298 | } 299 | } 300 | } 301 | 302 | pub fn server_capabilities(client_caps: &ClientCapabilities) -> ServerCapabilities { 303 | use lsp_types::{ 304 | ExecuteCommandOptions, OneOf, TextDocumentSyncCapability, TextDocumentSyncKind, 305 | TextDocumentSyncOptions, WorkDoneProgressOptions 306 | }; 307 | ServerCapabilities { 308 | text_document_sync: Some(TextDocumentSyncCapability::Options( 309 | TextDocumentSyncOptions { 310 | open_close: None, 311 | change: Some(TextDocumentSyncKind::Full), 312 | will_save: None, 313 | will_save_wait_until: None, 314 | save: None 315 | } 316 | )), 317 | selection_range_provider: None, 318 | hover_provider: None, 319 | completion_provider: None, 320 | signature_help_provider: None, 321 | definition_provider: None, 322 | type_definition_provider: None, 323 | implementation_provider: None, 324 | references_provider: None, 325 | document_highlight_provider: None, 326 | document_symbol_provider: None, 327 | workspace_symbol_provider: None, 328 | code_action_provider: None, 329 | code_lens_provider: None, 330 | document_formatting_provider: Some(OneOf::Left(true)), 331 | document_range_formatting_provider: None, 332 | document_on_type_formatting_provider: None, 333 | rename_provider: None, 334 | document_link_provider: None, 335 | color_provider: None, 336 | folding_range_provider: None, 337 | declaration_provider: None, 338 | execute_command_provider: None, 339 | workspace: None, 340 | call_hierarchy_provider: None, 341 | semantic_tokens_provider: None, 342 | moniker_provider: None, 343 | linked_editing_range_provider: None, 344 | experimental: None 345 | } 346 | } 347 | 348 | fn supports_utf8(caps: &lsp_types::ClientCapabilities) -> bool { 349 | caps.offset_encoding 350 | .as_deref() 351 | .unwrap_or_default() 352 | .iter() 353 | .any(|it| it == "utf-8") 354 | } 355 | 356 | // web-browser-lsp:// 357 | // first version of buffer spec 358 | // respects CommonMark or Github Flavored but position is dynamic like browsh 359 | // ``` 360 | // [url] 361 | // blank line 362 | // body 363 | // ``` 364 | // * single tab 365 | // * goto url 366 | // * reload 367 | // * text 368 | // * click 369 | // - a, [onclick] 370 | // - [input=radio], [input=checkbox] 371 | // - select 372 | // * input 373 | // - [type=text], textarea, [contenteditable] 374 | --------------------------------------------------------------------------------