|  | 
| 1 | 1 | use std::borrow::Cow; | 
| 2 | 2 | 
 | 
| 3 | 3 | use tower_lsp_server::lsp_types::*; | 
|  | 4 | +use tree_sitter::{QueryCursor, Tree}; | 
| 4 | 5 | 
 | 
| 5 | 6 | use crate::prelude::*; | 
| 6 | 7 | 
 | 
| 7 | 8 | use crate::backend::Backend; | 
| 8 | 9 | use crate::backend::Text; | 
| 9 |  | -use crate::index::JsQuery; | 
|  | 10 | +use crate::index::{_G, JsQuery}; | 
|  | 11 | +use crate::model::PropertyKind; | 
|  | 12 | +use crate::utils::{ByteOffset, MaxVec, RangeExt, span_conv}; | 
|  | 13 | +use tracing::instrument; | 
|  | 14 | +use ts_macros::query; | 
|  | 15 | + | 
|  | 16 | +query! { | 
|  | 17 | +	#[lang = "tree_sitter_javascript"] | 
|  | 18 | +	OrmCallQuery(OrmObject, CallMethod, ModelArg, MethodArg); | 
|  | 19 | +	// Match this.orm.call('model', 'method') | 
|  | 20 | +	(call_expression | 
|  | 21 | +		function: (member_expression | 
|  | 22 | +			object: (member_expression | 
|  | 23 | +				object: (this) | 
|  | 24 | +				property: (property_identifier) @ORM_OBJECT (#eq? @ORM_OBJECT "orm")) | 
|  | 25 | +			property: (property_identifier) @CALL_METHOD (#eq? @CALL_METHOD "call")) | 
|  | 26 | +		arguments: (arguments | 
|  | 27 | +			. (string) @MODEL_ARG | 
|  | 28 | +			. "," | 
|  | 29 | +			. (string) @METHOD_ARG)) | 
|  | 30 | +} | 
| 10 | 31 | 
 | 
| 11 | 32 | impl Backend { | 
| 12 | 33 | 	pub fn on_change_js( | 
| @@ -116,4 +137,139 @@ impl Backend { | 
| 116 | 137 | 
 | 
| 117 | 138 | 		Ok(None) | 
| 118 | 139 | 	} | 
|  | 140 | + | 
|  | 141 | +	#[instrument(skip_all)] | 
|  | 142 | +	pub async fn js_completions( | 
|  | 143 | +		&self, | 
|  | 144 | +		params: CompletionParams, | 
|  | 145 | +		ast: Tree, | 
|  | 146 | +		rope: RopeSlice<'_>, | 
|  | 147 | +	) -> anyhow::Result<Option<CompletionResponse>> { | 
|  | 148 | +		let uri = ¶ms.text_document_position.text_document.uri; | 
|  | 149 | +		let position = params.text_document_position.position; | 
|  | 150 | +		let Ok(ByteOffset(offset)) = rope_conv(position, rope) else { | 
|  | 151 | +			return Err(errloc!("could not find offset for {}", uri.path().as_str())); | 
|  | 152 | +		}; | 
|  | 153 | + | 
|  | 154 | +		let contents = Cow::from(rope); | 
|  | 155 | +		let contents = contents.as_bytes(); | 
|  | 156 | +		let query = OrmCallQuery::query(); | 
|  | 157 | +		let mut cursor = QueryCursor::new(); | 
|  | 158 | + | 
|  | 159 | +		// Find the orm.call node that contains the cursor position | 
|  | 160 | +		for match_ in cursor.matches(query, ast.root_node(), contents) { | 
|  | 161 | +			let mut model_arg_node = None; | 
|  | 162 | +			let mut method_arg_node = None; | 
|  | 163 | + | 
|  | 164 | +			for capture in match_.captures { | 
|  | 165 | +				match OrmCallQuery::from(capture.index) { | 
|  | 166 | +					Some(OrmCallQuery::ModelArg) => { | 
|  | 167 | +						model_arg_node = Some(capture.node); | 
|  | 168 | +					} | 
|  | 169 | +					Some(OrmCallQuery::MethodArg) => { | 
|  | 170 | +						method_arg_node = Some(capture.node); | 
|  | 171 | +					} | 
|  | 172 | +					_ => {} | 
|  | 173 | +				} | 
|  | 174 | +			} | 
|  | 175 | + | 
|  | 176 | +			// Check if cursor is within the model argument | 
|  | 177 | +			if let Some(model_node) = model_arg_node { | 
|  | 178 | +				let range = model_node.byte_range(); | 
|  | 179 | +				if range.contains(&offset) { | 
|  | 180 | +					// Extract the current prefix (excluding quotes) | 
|  | 181 | +					let inner_range = range.shrink(1); | 
|  | 182 | +					let prefix = String::from_utf8_lossy(&contents[inner_range.start..offset]); | 
|  | 183 | +					let lsp_range = span_conv(model_node.range()); | 
|  | 184 | +					let mut items = MaxVec::new(100); | 
|  | 185 | +					self.index.complete_model(&prefix, lsp_range, &mut items)?; | 
|  | 186 | + | 
|  | 187 | +					return Ok(Some(CompletionResponse::List(CompletionList { | 
|  | 188 | +						is_incomplete: false, | 
|  | 189 | +						items: items.into_inner(), | 
|  | 190 | +					}))); | 
|  | 191 | +				} | 
|  | 192 | +			} | 
|  | 193 | + | 
|  | 194 | +			// Check if cursor is within the method argument | 
|  | 195 | +			if let Some(method_node) = method_arg_node { | 
|  | 196 | +				let range = method_node.byte_range(); | 
|  | 197 | +				if range.contains(&offset) { | 
|  | 198 | +					// Extract the model name from the first argument | 
|  | 199 | +					if let Some(model_node) = model_arg_node { | 
|  | 200 | +						let model_range = model_node.byte_range().shrink(1); | 
|  | 201 | +						let model_name = String::from_utf8_lossy(&contents[model_range]).to_string(); | 
|  | 202 | + | 
|  | 203 | +						// Extract the current method prefix (excluding quotes) | 
|  | 204 | +						let inner_range = range.clone().shrink(1); | 
|  | 205 | +						let prefix = String::from_utf8_lossy(&contents[inner_range.start..offset]); | 
|  | 206 | + | 
|  | 207 | +						let byte_range = ByteOffset(range.start)..ByteOffset(range.end); | 
|  | 208 | + | 
|  | 209 | +						let mut items = MaxVec::new(100); | 
|  | 210 | +						self.index.complete_property_name( | 
|  | 211 | +							&prefix, | 
|  | 212 | +							byte_range, | 
|  | 213 | +							model_name, | 
|  | 214 | +							rope, | 
|  | 215 | +							Some(PropertyKind::Method), | 
|  | 216 | +							true, | 
|  | 217 | +							&mut items, | 
|  | 218 | +						)?; | 
|  | 219 | + | 
|  | 220 | +						return Ok(Some(CompletionResponse::List(CompletionList { | 
|  | 221 | +							is_incomplete: false, | 
|  | 222 | +							items: items.into_inner(), | 
|  | 223 | +						}))); | 
|  | 224 | +					} | 
|  | 225 | +				} | 
|  | 226 | +			} | 
|  | 227 | + | 
|  | 228 | +			// Check if cursor is in a position where we should start a new string argument | 
|  | 229 | +			// This handles cases where the user is typing after the comma but hasn't started the string yet | 
|  | 230 | +			if let Some(model_node) = model_arg_node { | 
|  | 231 | +				let model_end = model_node.byte_range().end; | 
|  | 232 | +				// Look for comma after model argument | 
|  | 233 | +				let mut i = model_end; | 
|  | 234 | +				while i < contents.len() && contents[i].is_ascii_whitespace() { | 
|  | 235 | +					i += 1; | 
|  | 236 | +				} | 
|  | 237 | +				if i < contents.len() && contents[i] == b',' { | 
|  | 238 | +					i += 1; | 
|  | 239 | +					// Skip whitespace after comma | 
|  | 240 | +					while i < contents.len() && contents[i].is_ascii_whitespace() { | 
|  | 241 | +						i += 1; | 
|  | 242 | +					} | 
|  | 243 | +					// If cursor is at or after this position and before any method argument | 
|  | 244 | +					if offset >= i | 
|  | 245 | +						&& (method_arg_node.is_none() || offset < method_arg_node.unwrap().byte_range().start) | 
|  | 246 | +					{ | 
|  | 247 | +						// We're completing the method name | 
|  | 248 | +						let model_range = model_node.byte_range().shrink(1); | 
|  | 249 | +						let model_name = String::from_utf8_lossy(&contents[model_range]).to_string(); | 
|  | 250 | + | 
|  | 251 | +						let synthetic_range = ByteOffset(i)..ByteOffset(offset.max(i)); | 
|  | 252 | + | 
|  | 253 | +						let mut items = MaxVec::new(100); | 
|  | 254 | +						self.index.complete_property_name( | 
|  | 255 | +							"", | 
|  | 256 | +							synthetic_range, | 
|  | 257 | +							model_name, | 
|  | 258 | +							rope, | 
|  | 259 | +							Some(PropertyKind::Method), | 
|  | 260 | +							true, | 
|  | 261 | +							&mut items, | 
|  | 262 | +						)?; | 
|  | 263 | + | 
|  | 264 | +						return Ok(Some(CompletionResponse::List(CompletionList { | 
|  | 265 | +							is_incomplete: false, | 
|  | 266 | +							items: items.into_inner(), | 
|  | 267 | +						}))); | 
|  | 268 | +					} | 
|  | 269 | +				} | 
|  | 270 | +			} | 
|  | 271 | +		} | 
|  | 272 | + | 
|  | 273 | +		Ok(None) | 
|  | 274 | +	} | 
| 119 | 275 | } | 
0 commit comments