Browse Source

Merge pull request #5196 from vnen/similarity-code-completion

Improve code completion search
Rémi Verschelde 9 years ago
parent
commit
3668768463
5 changed files with 104 additions and 43 deletions
  1. 44 0
      core/ustring.cpp
  2. 2 0
      core/ustring.h
  3. 4 0
      core/variant_call.cpp
  4. 16 0
      doc/base/classes.xml
  5. 38 43
      scene/gui/text_edit.cpp

+ 44 - 0
core/ustring.cpp

@@ -2810,6 +2810,50 @@ bool String::_base_is_subsequence_of(const String& p_string, bool case_insensiti
 	return false;
 }
 
+Vector<String> String::bigrams() const {
+	int n_pairs = length() - 1;
+	Vector<String> b;
+	if(n_pairs <= 0) {
+		return b;
+	}
+	b.resize(n_pairs);
+	for(int i = 0; i < n_pairs; i++) {
+		b[i] = substr(i,2);
+	}
+	return b;
+}
+
+// Similarity according to Sorensen-Dice coefficient
+float String::similarity(const String& p_string) const {
+	if(operator==(p_string)) {
+		// Equal strings are totally similar
+		return 1.0f;
+	}
+	if (length() < 2 || p_string.length() < 2) {
+		// No way to calculate similarity without a single bigram
+		return 0.0f;
+	}
+
+	Vector<String> src_bigrams = bigrams();
+	Vector<String> tgt_bigrams = p_string.bigrams();
+
+	int src_size = src_bigrams.size();
+	int tgt_size = tgt_bigrams.size();
+
+	float sum = src_size + tgt_size;
+	float inter = 0;
+	for (int i = 0; i < src_size; i++) {
+		for (int j = 0; j < tgt_size; j++) {
+			if (src_bigrams[i] == tgt_bigrams[j]) {
+				inter++;
+				break;
+			}
+		}
+	}
+
+	return (2.0f * inter)/sum;
+}
+
 static bool _wildcard_match(const CharType* p_pattern, const CharType* p_string,bool p_case_sensitive) {
 	switch (*p_pattern) {
 	case '\0':

+ 2 - 0
core/ustring.h

@@ -123,6 +123,8 @@ public:
 	bool ends_with(const String& p_string) const;
 	bool is_subsequence_of(const String& p_string) const;
 	bool is_subsequence_ofi(const String& p_string) const;
+	Vector<String> bigrams() const;
+	float similarity(const String& p_string) const;
 	String replace_first(String p_key,String p_with) const;
 	String replace(String p_key,String p_with) const;
 	String replacen(String p_key,String p_with) const;

+ 4 - 0
core/variant_call.cpp

@@ -249,6 +249,8 @@ static void _call_##m_type##_##m_method(Variant& r_ret,Variant& p_self,const Var
 	VCALL_LOCALMEM1R(String,ends_with);
 	VCALL_LOCALMEM1R(String,is_subsequence_of);
 	VCALL_LOCALMEM1R(String,is_subsequence_ofi);
+	VCALL_LOCALMEM0R(String,bigrams);
+	VCALL_LOCALMEM1R(String,similarity);
 	VCALL_LOCALMEM2R(String,replace);
 	VCALL_LOCALMEM2R(String,replacen);
 	VCALL_LOCALMEM2R(String,insert);
@@ -1281,6 +1283,8 @@ _VariantCall::addfunc(Variant::m_vtype,Variant::m_ret,_SCS(#m_method),VCALL(m_cl
 	ADDFUNC1(STRING,BOOL,String,ends_with,STRING,"text",varray());
 	ADDFUNC1(STRING,BOOL,String,is_subsequence_of,STRING,"text",varray());
 	ADDFUNC1(STRING,BOOL,String,is_subsequence_ofi,STRING,"text",varray());
+	ADDFUNC0(STRING,STRING_ARRAY,String,bigrams,varray());
+	ADDFUNC1(STRING,REAL,String,similarity,STRING,"text",varray());
 
 	ADDFUNC2(STRING,STRING,String,replace,STRING,"what",STRING,"forwhat",varray());
 	ADDFUNC2(STRING,STRING,String,replacen,STRING,"what",STRING,"forwhat",varray());

+ 16 - 0
doc/base/classes.xml

@@ -37268,6 +37268,13 @@ This method controls whether the position between two cached points is interpola
 			Return true if the strings begins with the given string.
 			</description>
 		</method>
+		<method name="bigrams">
+			<return type="StringArray">
+			</return>
+			<description>
+			Return the bigrams (pairs of consecutive letters) of this string.
+			</description>
+		</method>
 		<method name="c_escape">
 			<return type="String">
 			</return>
@@ -37627,6 +37634,15 @@ This method controls whether the position between two cached points is interpola
 			<description>
 			</description>
 		</method>
+		<method name="similarity">
+			<return type="float">
+			</return>
+			<argument index="0" name="text" type="String">
+			</argument>
+			<description>
+			Return the similarity index of the text compared to this string. 1 means totally similar and 0 means totally dissimilar.
+			</description>
+		</method>
 		<method name="split">
 			<return type="StringArray">
 			</return>

+ 38 - 43
scene/gui/text_edit.cpp

@@ -3851,8 +3851,14 @@ void TextEdit::undo() {
 		}
 	}
 
-	cursor_set_line(undo_stack_pos->get().from_line);
-	cursor_set_column(undo_stack_pos->get().from_column);
+	if (undo_stack_pos->get().type == TextOperation::TYPE_REMOVE) {
+		cursor_set_line(undo_stack_pos->get().to_line);
+		cursor_set_column(undo_stack_pos->get().to_column);
+		_cancel_code_hint();
+	} else {
+		cursor_set_line(undo_stack_pos->get().from_line);
+		cursor_set_column(undo_stack_pos->get().from_column);
+	}
 	update();
 }
 
@@ -3995,28 +4001,19 @@ void TextEdit::set_completion(bool p_enabled,const Vector<String>& p_prefixes) {
 
 void TextEdit::_confirm_completion() {
 
-	String remaining=completion_current.substr(completion_base.length(),completion_current.length()-completion_base.length());
-	String l = text[cursor.line];
-	bool same=true;
-	//if what is going to be inserted is the same as what it is, don't change it
-	for(int i=0;i<remaining.length();i++) {
-		int c=i+cursor.column;
-		if (c>=l.length() || l[c]!=remaining[i]) {
-			same=false;
-			break;
-		}
-	}
+	begin_complex_operation();
 
-	if (same)
-		cursor_set_column(cursor.column+remaining.length());
-	else {
-		insert_text_at_cursor(remaining);
-		if (remaining.ends_with("(") && auto_brace_completion_enabled) {
-			insert_text_at_cursor(")");
-			cursor.column--;
-		}
+	_remove_text(cursor.line, cursor.column - completion_base.length(), cursor.line, cursor.column);
+	cursor_set_column(cursor.column - completion_base.length(), false);
+	insert_text_at_cursor(completion_current);
+
+	if (completion_current.ends_with("(") && auto_brace_completion_enabled) {
+		insert_text_at_cursor(")");
+		cursor.column--;
 	}
 
+	end_complex_operation();
+
 	_cancel_completion();
 }
 
@@ -4119,30 +4116,29 @@ void TextEdit::_update_completion_candidates() {
 	completion_index=0;
 	completion_base=s;
 	int ci_match=0;
+	Vector<float> sim_cache;
 	for(int i=0;i<completion_strings.size();i++) {
-		if (completion_strings[i].begins_with(s)) {
+		if (s.is_subsequence_ofi(completion_strings[i])) {
 			// don't remove duplicates if no input is provided
-			if (completion_options.find(completion_strings[i]) != -1 && s != "") {
+			if (s != "" && completion_options.find(completion_strings[i]) != -1) {
 				continue;
 			}
-			completion_options.push_back(completion_strings[i]);
-			int m=0;
-			int max=MIN(completion_current.length(),completion_strings[i].length());
-			if (max<ci_match)
-				continue;
-			for(int j=0;j<max;j++) {
-
-				if (j>=completion_strings[i].length())
-					break;
-				if (completion_current[j]!=completion_strings[i][j])
-					break;
-				m++;
-			}
-			if (m>ci_match) {
-				ci_match=m;
-				completion_index=completion_options.size()-1;
+			// Calculate the similarity to keep completions in good order
+			float similarity = s.similarity(completion_strings[i]);
+			int comp_size = completion_options.size();
+			if (comp_size == 0) {
+				completion_options.push_back(completion_strings[i]);
+				sim_cache.push_back(similarity);
+			} else {
+				float comp_sim;
+				int pos = 0;
+				do {
+					comp_sim = sim_cache[pos++];
+				} while(pos < comp_size && similarity <= comp_sim);
+				pos--; // Pos will be off by one
+				completion_options.insert(pos, completion_strings[i]);
+				sim_cache.insert(pos, similarity);
 			}
-
 		}
 	}
 
@@ -4155,7 +4151,8 @@ void TextEdit::_update_completion_candidates() {
 
 	}
 
-	completion_current=completion_options[completion_index];
+	// The top of the list is the best match
+	completion_current=completion_options[0];
 
 #if 0	// even there's only one option, user still get the chance to choose using it or not
 	if (completion_options.size()==1) {
@@ -4167,8 +4164,6 @@ void TextEdit::_update_completion_candidates() {
 
 	}
 #endif
-	if (completion_options.size()==1 && s==completion_options[0])
-		_cancel_completion();
 
 	completion_enabled=true;
 }